Уважаемые коллеги, представляю вашему вниманию и на ваше осуждение контейнер для управления состоянием 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 (если такого не существует) с шаблонным кодом обработчика.
Вот и всё, совсем просто не так ли? А теперь можете пинать. Буду рад советам и разумной критике, уважаемые коллеги.
