Простой в использовании контейнер состояния для React приложения под названием Xstore

в 14:50, , рубрики: React, ReactJS, state

Уважаемые коллеги, представляю вашему вниманию и на ваше осуждение контейнер для управления состоянием 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js