react-singleton-state
v1.0.4
Published
The library helps you providing easy data sharing between components by using a principle of angular singleton services
Downloads
2
Maintainers
Readme
react-singleton-state 1.0.4
Author: Oleg Mukhov
Description:
Библиотека помогает быстро внедрять данные в React-компоненты путем записи и чтения объектов-синглтонов. Библиотека для тех, кто хочет быстро "пересесть" с Angular 1.x на React
Launch v1.0.0:
Так как при установке в node_modules приходит исходный каталог, необходимо добавить в webpack.config Вашего проекта следующую настройку js-модуля:
{
test: /\.js$/,
loader: 'babel-loader',
query: {
presets: ['es2017', 'react'],
plugins: ['transform-object-rest-spread', 'transform-class-properties']
}
}
дополнительно установите: npm install --save-dev babel-preset-es2017 babel-plugin-transform-class-properties babel-plugin-transform-object-rest-spread Проблема решена с версии 1.0.1.
Launch Sample App (since v1.0.2)
start index.html in browser node_modules/react-singleton-state/public/index.html You also need material-ui package to run the sample app.
There you can compare react-singleton-state and redux and choose which one is easier and more flexible for you.
Использование:
Библиотека состоит из следующих исполняемых классов и функций:
- Класс Provider - синглтон, основное хранилище приложения.
import { Provider, providerExporter } from 'react-singleton-state';
- Класс Service - от данного класса наследуются все сервисы приложения. После агрегации в Provider, они также становятся синглтонами.
import { Service } from 'react-singleton-state';
- Класс Component - обертка библиотеки для React-компонентов, связывает state-компонентов с инстансами Service.
import { Component } from 'react-singleton-state';
Provider
Класс Provider является синглтоном.
Использование Provider в приложении:
- наследуем класс, например, AppProvider от Provider;
class AppProvider extends Provider { }
- описываем метод defineServices() класса AppProvider, где просто агрегируем сервисы нашего приложения;
defineServices() {
this.UserService = new UserService('UserService');
this.TaskService = new TaskService('TaskService');
}
- в случае необходимости можно описать необязательный метод defineUrls(), который сохранит все url'ы в константы по каскадному принципу;
defineUrls() {
return {
ROOT: {
url: 'http://localhost:8090',
REST: {
url: '/rest',
TASKS: {
url: '/tasks'
},
FOLLOWERS: {
url: '/followers'
}
},
CREDENTIALS: {
url: '/security/v1'
}
}
};
}
/*
* this.URLS = {
* ROOT: 'http://localhost:8090',
* REST: 'http://localhost:8090/rest',
* TASKS: 'http://localhost:8090/rest/tasks',
* FOLLOWERS: 'http://localhost:8090/rest/followers',
* CREDENTIALS: 'http://localhost:8090/security/v1'
* }
*/
- Последним шагом экспортируем AppProvider в проект используя библиотечную функцию providerExporter(). Во вермя импорта произойдет вызов функции, которая вернет new AppProvider(), в результате чего результат такого импорта можно сразу представить в виде объекта URLS и необходимых сервисов.
export default providerExporter(AppProvider);
// ==========================================
import AppProvider from 'src/AppProvider';
const { URLS, UserService, TaskService } = AppProvider;
Методы, которые должны или могут быть описаны у наследника класса Provider:
- Обязательный метод defineServices(). Не ожидает никаких аргументов. В теле метода необходимо присвоить полям класса, которые в последствии станут сервисами-синглтонами приложения, экземпляры классов-наследников класса Service. Метод вызывается в конструкторе класса Provider.
- Необязательный метод defineUrls(). Не ожидает никаких аргументов. В теле метода необходимо вернуть объект, представляющий собой каскадируемые урлы. Поля url являются конечными полями в итерациях.
Принцип работы класса Provider
Provider является самовызывающейся функцией, которая возвращает класс. В конструкторе этого класса, сперва, происходит проверка на существование экземпляра этого класса, если такой имеется, то конструктор всегда вернет этот экземпляр. Такая проверка возможна, так как экземпляр класса и сам класс всегда находятся в одном замыкании. Если же экземпляра класса не обнаруживается, то последовательно вызываются методы defineServices() и defineUrls(). Затем происходит присвоение экземпляра класса в переменную внутри замыкания.
Service
Service - это бин. В сервисе есть только его поля, а также геттеры и сеттеры для обращения к ним.
Использование Service в приложении
- Наследуем новый бин от Service, ~~определяем его поля~~, геттеры и сеттеры. (Начиная с версии 1.0.3, приватные поля определяются автоматически из defaultValues. Доступ к ним осуществляется через Symbol.for()).
export default class UserService extends Service {
static defaultValues = {
userName: 'DefaultName'
};
get userName() { return this[Symbol.for('userName')]; }
set userName(val) { this[Symbol.for('userName')] = val; }
}
~~2. Определяем значение полей по умолчанию через статическую переменную defaultValues, и сохраняем ссылку на класс используя метод getClass(). Ключи defaultValues должны совпадать с названиями геттеров и сеттеров.~~ Метод getClass() перестал поддерживаться с версии 1.0.1 и будет полностью отменен с версии 1.1.0
/*
* Шаг не имеет смысла после обновлений 1.0.2 и 1.0.3
*/
export default class UserService extends Service {
static defaultValues = {
userName: 'admin'
};
constructor(sn) {
super(sn);
this.getClass(UserService);
//other variables
}
//getters and setters
}
- Агрегируем UserService в AppProvider'e.
class AppProvider extends Provider {
defineServices() {
this.UserService = new UserService('UserService');
}
}
- Дальнейшее использование Service тесно связано с использованием класса ComponentService
Методы класса Service:
- getClass(classType: class) [DEPRECATED] - метод принимает в качестве аргумента класс и записывает его в поле this.classType. В каждом наследнике класса Service необходимо вызывать этот метод в конструкторе, передавая в него ссылку на себя. Данная процедура необходима для доступа к статической переменной, описанного в классе Service.
- toDefault(prop: string) - метод переводит указанное поле (по имени сеттера) к значению данного поля в статической переменной defaultValues.
- defaultAll() - переводит все сеттеры к их значениям в defaultValues.
Принцип работы класса Service
Класс Service содержит только одно приватное поле serviceName. Поле заполняется при создании экземпляра класса. Поле необходимо для заполнения this.state React-компонента. Далее поле доступно только через геттер. Автор настоятельно рекомендует передавать в конструктор Service'ов такое же строковое значение как и название класса этого сервиса! Поскольку экземпляры всех сервисов в приложении агрегированы в синглтон-Provider, то все они сами выступают синглтонами.
Component
Класс Component связывает наши React-компоненты с экземплярами Service'ов.
Использование Component в приложении:
- Наследуем новый statefull-компонент от Component'a и инжектим Service'ы в его this.state через метод this._injectServices(). Метод не помешает использовать компонентный (локальный) state.
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
localStateField1: 'default',
localStateField2: undefined
};
this._injectServices([RouteService, UserService], InjectMerging.AFTER);
}
}
- Для чтения данных из Service'ов нам абсолютно не нужен this.state, поэтому данные также легко вставлять и в stateless-компоненты.
const Stateless = props => <p>{props.paragraph}</p>;
//render of some class
render() {
return (
<div>
<Stateless paragraph={TextService.paragraph} />
</div>
);
}
- Для записи данных в Service есть два способа:
Первый способ необходим для изменения данных в Service без ререндеринга компонента. Для этого нужно использовать сеттер самого Service'a
export default class TaskItem extends Component {
constructor(props) {
super(props);
this.state = {
TaskService
};
}
onTaskChange(e) {
TaskService.taskText = e.target.value;
}
render() {
return (
<div>
<p>{TaskService.taskText}</p>
<input value={TaskService.taskText} onChange={this.onTaskChange} />
</div>
);
}
}
В данном примере значение внутри Service'a будет изменяться, однако ни в input, ни в p, новое значение появляться не будет. Данный код можно оптимизировать, отображая значение в input, но не изменяя его в p. Для этого добавим поле компонентного state и метод lifecycle - componentWillUpdate() и componentWillUnmount. И поменяем данные в input.props.value.
export default class TaskItem extends Component {
constructor(props) {
super(props);
this.state = {
TaskService,
taskText: TaskService.taskText
};
}
componentWillUpdate(nextProps, nextState) {
this.state.taskText = TaskService.taskText;
}
componentWillUnmount() {
TaskService.taskText = this.state.taskText;
}
onTaskChange(e) {
this.setState({taskText: e.target.value});
}
render() {
return (
<div>
<p>{TaskService.taskText}</p>
<input value={this.state.taskText} onChange={this.onTaskChange.bind(this)} />
</div>
);
}
}
Из примера видно, каким мощным эффектом оптимизации без использования shouldComponentUpdate() обладают сервисы-синглтоны.
Второй способ изменения данных в сервисе влечет за собой ререндеринг компонента. Для его осуществления необходимо вызвать метод reRender(), который унаследован от Component
export default class TaskItem extends Component {
constructor(props) {
super(props);
this.state = {
TaskService
};
}
onTaskChange(e) {
this.reRender(TaskService)('taskText').set(e.target.value);
}
render() {
return (
<div>
<p>{TaskService.taskText}</p>
<input value={TaskService.taskText} onChange={this.onTaskChange.bind(this)} />
</div>
);
}
}
Теперь при изменении данных внутри input будет происходить ререндеринг компонента, в результате чего в p и input будут отображаться актуальные значения.
Методы класса Component
- _injectService() внедряет Service'ы в state компонента. Принимает два аргумента:
- Массив сервисов, которые будут внедрены в state компонента;
- enum InjectMerging с одним из значений: InjectMerging.BEFORE - внедрит сервисы перед значениями локального state, _InjectMerging.AFTER - после. Данный аргумент необязательный, по умолчанию используется InjectMerging.BEFORE
//InjectMerging.BEFORE
this.state = {
//your services here
first: 'first',
second: 'second'
};
///InjectMerging.AFTER
this.state = {
first: 'first',
second: 'second',
//your services here
};
Кроме того, можно обойтись и без метода _injectServices() при внедрении сервисов в state
constructor(props) {
super(props);
this.state = {
UserService,
first: 'first',
second: 'second',
RouteService
};
}
- _bindMethods() - метод принимает строковые наименования методов компонента, к которым применится .bind(this). Кроме того, каждый аргумент может быть массивом: ['имя_метода','аргумент1','аргумент2'].
constructor(props) {
super(props);
this._bindMethods('onTextChange', 'onSelectChange', 'onButtonClick');
//======or=======
this._bindMethods(
['onTextChange', 'newValue', SomeFilter.filter('Val')],
['onSelectChange', 1]
);
}
- reRender() - метод необходим для изменения значения Service'a с последующим ререндеринго компонента. Метод устроен довольно непросто, так что разберем его подробно.
- reRender принимает один аргумент - экземпляр сервиса либо его поле this.serviceName и возвращает функцию...
this.reRender(UserService) // return serviceProps => { ... }
this.reRender('UserService') // or this.reRender(UserService.serviceName) - return serviceProps => { ... }
- возвращаемая функция принимает один необязаетльный аргумент - массив строковых названий полей сервиса или строковое название одного поля сервиса, и возвращает объект методов.
this.reRender(UserService)('login') //return { set: val => {...}, setDefault: () => {...} }
this.reRender(UserService)(['login', 'followers', 'dateOfBirth']) //return { set: values => {...}, setDefault: () => {...} }
this.reRender(UserService)() // return { set: obj => {...}, setDefault: () => {...} }
- Метод set() вызывает сеттер Service'a и делает forceUpdate: * Если возвращен по одному переданному имени поля, то принимает одно значение - новое значение этого поля в Servic'e; * Если возвращен по массиву имен полей, то принимает массив новых значений этих полей по соответствию индексов; * Если возвращен по пустому значению, то принимает объект c ключами - именами полей Service'a, и значениями - новыми значениями этих полей.
this.reRender(UserService)('login').set('MyName');
this.reRender(UserService)(['login', 'dateOfBirth']).set(['MyName', '22.06.1941']);
this.reRender(UserService)().set({login: 'MyName', dateOfBirth: '22.06.1941'});
- Метод setDefault() вызывает Service.toDefault() в первых двух случаях и Service.defaultAll() в третьем, после чего вызывает метод forceUpdate.