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__()
);

Заключение


Надеюсь кому-нибудь такой подход покажется удобным и спасет от тонн шаблонного кода при создании нового приложения или добавления единого источника данных в существующее.

Комментарии (9)


  1. rumkin
    21.02.2018 14:41

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


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


    1. one_more Автор
      21.02.2018 15:12

      при подписке входным параметром будет объект вида

      { [branchName]: branchState, action1, action2, ...actionN }
      


  1. Veikedo
    21.02.2018 15:04

    1. one_more Автор
      21.02.2018 16:18

      не совсем

      • MST подразумевает создание стора при инициализации приложения (иначе это уже не центральный стор)
      • (поправьте если это не так) подписка происходит на весь стор
      • 156кб может быть критично для большого приложения (против 3кб)


  1. valerqch
    21.02.2018 15:12

    Внешне получилось очень даже похоже на Vue с его компонентами, как мне кажется.


  1. bgnx
    21.02.2018 21:03

    возможность подписаться только на обновления всего стора (утомительные селекторы + возможны неожиданные перерисовки)

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


    1. one_more Автор
      21.02.2018 23:32

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


  1. xadd
    21.02.2018 21:32

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


    1. one_more Автор
      21.02.2018 23:36

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