Управляем состоянием приложения без шаблонного кода и магии

    image

    Хочу поделиться с сообществом своей реализацией концепции flux как единого источника данных и видением построения веб-приложений. Мотивом к созданию своего решения послужило желание избавиться от большого количества шаблонного кода и сделать взаимодействие с источником данных удобным. Я работал над большим приложением (10 команд + 1 архитектурная) с использованием связки React + Redux как архитектор и как лид команды разработки и вынес для себя моменты, которые доставляли большие неудобства в процессе написания кода:

    • большое количество шаблонного кода
    • как следствие многословности — перенос небольших кусков логики в представление
    • сложность динамического добавления/удаления бизнес-логики модулей
    • возможность подписаться только на обновления всего стора (утомительные селекторы + возможны неожиданные перерисовки)

    3 пункт особенно важен в контексте архитектуры микро-фронтендов, которая используется на проекте (и на многих других проектах).

    Решение


    Библиотека называется falx.

    Создание бизнес логики модуля


    const reducer = {
        state: [],
        actions: {
            add(state, text) {
              const todo = {
                id: getNextId(),
                done: false,
                text
              }
              return state.concat(todo)
            },
            done(state, id) {
                return state.map(todo => {
                  if (todo.id == id) {
                    return {
                      ...todo,
                      done: !todo.done
                    }
                  }
                  return todo
                })
            },
            remove(state, id) {
                return state.filter(todo => todo.id != id)
            }
        }
    }
    

    При таком подходе проще будет использовать экшены редюсера чем стейт react компонента демо.

    Регистрация в сторе


    import {register} from 'falx'
    register('todos', reducer);
    

    Подписка на обновления


    import {subscribe} from 'falx'
    subscribe('todos', state => {
        const html = state.todos.map(todo => `
            <li ${todo.done ? 'class="completed"' : ''} >
              <div class="view">
                <input class="toggle" type="checkbox" id="${todo.id}" ${todo.done ? 'checked' : ''} />
                <label>${todo.text}</label>
                <button class="destroy" id="${todo.id}"></button>
              </div>
              <input class="edit" value="${todo.text}" />
            </li>
       `);
       todoList.innerHTML = html.join('')
    });
    

    Доступ к бизнес-логике через стор


    import {store} from 'falx'
    const input = document.querySelector('#todo-text');
    const todos = document.querySelector('#todos');
    
    input.addEventListener('keyup', event => {
       if (event.which == 13 && event.target.value) {
      	store.todos.add(event.target.value);
        event.target.value = ''
      }
    });
    todos.addEventListener('change', event => {
    	store.todos.done(event.target.id)
    });
    todos.addEventListener('click', event => {
      if (event.target.className == 'destroy') {
      	store.todos.remove(event.target.id)
      }
    });
    

    Удаление модуля из стора


    import {remove} from 'falx'
    remove('todos')
    

    Живой пример

    Middleware


    Так же есть слой middleware для таких вещей как централизованная обработка ошибок, валидация и т.п.

    import {use} from 'falx'
    const middleware = (store, statePromise, action) => {
        console.log('action', action);
        return statePromise.then(state => {
            console.log('next state', state);
            return state
        })
    }
    use(middleware);
    //...
    unuse(middleware)
    

    Использование с React


     Для React есть HOC для подписки на изменения:

    import React, {PureComponent} from 'react'
    import {subscribeHOC} from 'falx-react'
    
    
    const reducer = {
        state: {
            value: 0
        },
        actions: {
            up(state) {
                return {
                    ...state,
                    value: state.value + 1
                }
            },
            down(state) {
                return {
                    ...state,
                    value: state.value - 1
                }
            }
        }
    };
    
    const COUNTER = 'counter';
    
    register(COUNTER, reducer);
    
    @subscribeHOC(COUNTER)
    class Counter extends PureComponent {
        render() {
            return (
                <div>
                    <div id="value">
                        {this.props.counter.value}
                    </div>
                    <button id="up" onClick={this.props.up} >up</button>
                    <button id="down" onClick={this.props.down} >down</button>
                </div>
            )
        }
    }
    

    Живой пример

    Дебаг


    Есть коннектор для Redux devtools:

    import {connectDevtools} from 'falx-redux-devtools'
    
    
    connectDevtools(
        window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
    );
    

    Заключение


    Надеюсь кому-нибудь такой подход покажется удобным и спасет от тонн шаблонного кода при создании нового приложения или добавления единого источника данных в существующее.
    • +10
    • 5,1k
    • 9
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 9
    • 0

      Плюс за метод remove!


      Почему структура reducer'а "уплощается" в компоненте? В чью пользу разрешится конфликт имен, если у меня будут два одинаковых свойства в state и в actions?

      • 0
        при подписке входным параметром будет объект вида
        { [branchName]: branchState, action1, action2, ...actionN }
        
      • 0
        • 0
          не совсем
          • MST подразумевает создание стора при инициализации приложения (иначе это уже не центральный стор)
          • (поправьте если это не так) подписка происходит на весь стор
          • 156кб может быть критично для большого приложения (против 3кб)
        • 0
          Внешне получилось очень даже похоже на Vue с его компонентами, как мне кажется.
          • 0
            возможность подписаться только на обновления всего стора (утомительные селекторы + возможны неожиданные перерисовки)

            В примере из статьи компоненты по прежнему подписываются практически на весь стор — например при обновлении одной единственной тодошки будет оповещен весь список тодошек
            хотя изменения касаются только одного компонента и нет смысла оповещать все остальные. Дальше неясно какую организацию стора предполагает библиотека чтобы работали подписки. Если это вложенные структуры то как библиотека предлагает обновлять данные которые находятся глубоко внутри объекта? В примере в статье предполагается иммутабельное обновление стора то как в этом случае предлагается работать с такой структурой — главный объект состояния хранит объект юзера, в нем хранится массив объектов folder, в каждой папке хранится массив объектов project, в каждом проекте массив объектов task в каждом задаче массив объектов comment и каждый комментарий может хранить вложенные объекты других комментариев и эти комментарии могут неограниченно вкладываться друг в друга — как библиотека предлагает обновлять текст глубоко вложенного в этом случае комментария? А если все же предполагается нормализация стора когда вместо объектов в массиве храним айдишники а все объекты будут храниться в хеше где айдишнику соотвествует объект то как будут работать в этом случае подписки?
            А вообще тему подписок только на часть состояния без каких либо проблем с обновлением глубоко вложенных объектов (и также без необходимости нормализировать стор) я подробно разобрал в этой статье а в этой статье я разобрал разные по эффективности алгоритмы оповещения подписчиков

            • 0
              Если при обновлении одного todo не вызывать слушателей списка, то в данном случае не будет работать мета редюсер.
              Что касается вложенности, такая структура легко преобразуется в плоский список — демо; это упростит структуру приложения и подписки будут работать ожидаемо
            • +1

              Это не Flux, у вас нет диспетчера и Action не как событие. Сегодня на фронтэнде без этого никак, иначе слишко будет легко. А вообще такой подход можно использовать в связке mobx/immer и получить как бонус точечное обновление компонентов.

              • 0
                Стоит отметить что dispatch есть — описано в секции api, и объект Action создается — он не прокидывается в другие редюсеры, но передается в middleware для возможности создания «сайд-эффектов»

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое