Уважаемые коллеги, представляю вашему вниманию и на ваше осуждение контейнер для управления состоянием React приложения xstore. Он определенно является таким маленьким детским велосипедом рядом с большим и сверкающим мотоциклом Redux. Все мы программисты JavaScript являемся такой большой и не сбавляющей обороты фабрикой по производству велосипедов.
Для более менее просто начинающих или начинающих свое знакомство с React JavaScript программистов Redux может показаться несколько сложной штукой, которая иногда непонятно как работает и к которой сложно "законнектиться", хочется чего-то попроще, чего-то похожего на данный маленький велосипед.
Давайте рассмотрим его поближе.
Установка
npm install --save xstore
Использование
Для начала нам нужно добавить в хранилище "обработчики".
(Пример обработчика представлен в блоке ниже)
index.js
import React from 'react'
import ReactDOM from 'react-dom'
import Store from 'xstore'
import App from './components/App'
import user from './store_handlers/user'
import dictionary from './store_handlers/dictionary'
// Имя обработчика не должно содержать символ "_".
// В данном случае мы имеем два обработчика: user и dictionary.
Store.addHandlers({
user,
dictionary
});
ReactDOM.render(
<App/>,
document.getElementById('root')
);
Далее идет пример обработчика "user".
В нем содержится редьюсер "init", который нужен, чтобы определить начальное состояние хранилища "user".
./store_handlers/user.js
import axios from 'axios'
const DEFAULT_STATE = {
name: 'user',
status: 'alive'
}
/**
===============
Reducers
===============
*/
// Будет автоматически вызван для инициализации начального состояния.
// Если данный редьюсер не существует, начальное состояние будет пустым объектом.
// Вызывайте этот редьюсер, чтобы сбросить состояние до начального.
// Для вызова используйте this.props.dispatch('USER_INIT').
const init = () => {
return DEFAULT_STATE;
}
// this.props.dispatch('USER_CHANGED', {name: 'NewName'})
const changed = (state, data) => {
return {
...state,
...data
}
}
/**
===============
Actions
===============
*/
// this.props.doAction('USER_CHANGE', {data})
const change = ({dispatch}, data) => {
// {dispatch, doAction, getState, state}
dispatch('USER_CHANGED', data);
}
// this.props.doAction('USER_LOAD', {id: userId})
const load = ({dispatch}, data) => {
// {dispatch, doAction, getState, state}
axios.get('/api/load.php', data)
.then(({data}) => {
dispatch('USER_CHANGED', data);
});
}
// Этот объект конечно же должен быть обязательно такого вида
export default {
actions: {
load,
change
// remove, save, add_item, remove_this_extra_item, .....
},
reducers: {
init,
changed
// removed, saved, item_added, this_extra_item_removed, .....
}
}
Далее пример того, как подключить компонент к хранилищу.
./components/ComponentToConnect/index.js
import React from 'react'
import Store from 'xstore'
class ComponentToConnect extends React.Component {
render() {
// Свойства user и dictionary будут получены из хранилища.
let {user, dictionary} = this.props;
return (
<div className="some-component">
....
</div>
)
}
loadUser(userId) {
// Вызов экшена в компоненте.
this.props.doAction('USER_LOAD', {id: userId});
}
setUser(userData) {
// Вызов редьюсера в компоненте.
// Но лучше так не делать и вызывать только экшены.
this.props.dispatch('USER_CHANGED', userData);
}
}
// Непосредственно подключение к хранилищу.
const params = {
has: 'user, dictionary'
}
export default Store.connect(ComponentToConnect, params);
Возможные опции "params" для подключения:
{
// Названия хранилищ, к которым будет подключен компонент:
has: 'user, dictionary',
// или по-другому:
has: ['user', 'dictionary'],
// Если необходимы только некоторые поля из хранилища:
has: 'user:name|status, dictionary:userStatuses',
// или
has: ['user:name|status', 'dictionary:userStatuses'],
// Компонент будет ждать содержимое данных хранилищ, и только тогда отрисуется.
shouldHave: 'user,dictionary',
// или
shouldHave: ['user', 'dictionary'],
// Чтобы извлечь данные из нескольких хранилищ на верхний уровень, установите в true.
// В результате компонент получит пропсы "name", "status", "userStatuses" вместо "user" и "dictionary"
flat: true,
// Нужен, чтобы добавить префикс к извлеченным данным, работает только если flat = true.
// В результате компонент получит пропсы "user_name", "user_status", "dictionary_userStatuses"
withPrefix: true,
// Вы можете добавить обработчики непосредственно здесь.
// Если в этом списке содержатся все необходимые, параметр "has" можно не передавать.
handlers: {
user,
dictionary
}
}
Список публичных методов хранилища:
import Store from 'xstore'
// Возвращает клонированный объект содержащий в себе данные всех хранилищ:
let state = Store.getState();
// Возвращает клонированный объект содержащий в себе данные хранилища "user":
let userState = Store.getState('user');
// Возвращает поле "name" из хранилища "user":
let userName = Store.getState('user.name');
// Возвращает поле с индексом 0 из поля "items" из хранилища "user":
let someItem = Store.getState('user.items.0');
// Добавление обработчиков:
Store.addHandlers({
user: userHandler,
dictionary: dictionaryHandler
})
// Вызов экшена "load" хранилища "user":
// Название экшена будет приведено в нижний регистр, так что не зависит от регистра.
Store.doAction('USER_LOAD', {id: userId});
// Вызов редьюсера "loaded" хранилища "user":
Store.dispatch('USER_LOADED', data);
// Подписка компонента на изменения хранилища:
Store.connect(Component, params);
А теперь о том как это работает:
Метод "connect" создает новый HOC класс XStoreConnect, который скрывает в себе всю логику по взаимодействию компонента и хранилища. Данный класс подписывается на изменения хранилища и, когда там происходят какие-то изменения, им вызывается метод setState защищенный от вызова извне (например через this.refs.xStoreConnect.setState(...)), после чего данный компонент перерисовывается, тем самым обновляя пропсы в обёрнутом компоненте.
Прямое изменение состояния компонента-обёртки this.refs.xStoreConnect.state = something тоже ни к чему не приведет, данный класс умеет находить внедренные данные и удалять их.
// .... Здесь еще много функционала хранилища
const LOCAL_OBJECT_CHECKER = {};
const connect = (ComponentToConnect, connectProps) => {
let ready = false;
let {
has,
handlers,
shouldHave: shouldHaveString,
flat,
withPrefix
} = connectProps;
if (!has && handlers instanceof Object) {
has = Object.keys(handlers);
}
let shouldHave = [];
if (typeof shouldHaveString == 'string') {
shouldHaveString = shouldHaveString.split(',');
for (let item of shouldHaveString) {
if (item) {
shouldHave.push(item.trim());
}
}
}
let doUnsubscribe,
doCleanState,
stateItemsQuantity;
return class XStoreConnect extends React.Component {
constructor() {
super();
const updater = (state) => {
stateItemsQuantity = Object.keys(state).length;
if (ready) {
this.setState(state, LOCAL_OBJECT_CHECKER);
} else {
this.state = state;
}
}
doUnsubscribe = () => {
unsubscribe(updater);
}
doCleanState = () => {
cleanStateFromInjectedItems(updater, this.state);
}
subscribe(updater, {has, handlers, flat, withPrefix});
}
setState(state, localObjectChecker) {
if (state instanceof Object && localObjectChecker === LOCAL_OBJECT_CHECKER) {
super.setState(state);
}
}
componentWillMount() {
ready = true;
}
componentWillUnmount() {
ready = false;
doUnsubscribe();
}
render() {
let {props, state} = this;
let newStateKeysQuantity = Object.keys(state).length;
if (stateItemsQuantity != newStateKeysQuantity) {
doCleanState();
}
for (let item of shouldHave) {
if (state[item] === undefined) {
return null;
}
}
let componentProps = {
...props,
...state,
doAction,
dispatch
};
return <ComponentToConnect {...componentProps}/>
}
}
}
Генерация файлов обработчиков из командной строки:
npm install -g xstore
xstore create-handler filename
Также можно прописать в "scripts" в "package.json":
{
scripts: {
"create-handler": "node node_modules/xstore/bin/exec.js"
}
}
npm run create-handler filename
Эта команда создаст файл filename.js (если такого не существует) с шаблонным кодом обработчика.
Вот и всё, совсем просто не так ли? А теперь можете пинать. Буду рад советам и разумной критике, уважаемые коллеги.
Автор: bushstas