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

TL;DR: базовая логика redux помещается в 7 строк JS кода.

О redux вкратце (вольный перевод заголовка на гитхабе):
Redux — библиотека управления состоянием для приложений, написанных на JavaScript.

Она помогает писать приложения, которые ведут себя стабильно/предсказуемо, работают на разных окружениях (клиент/сервер/нативный код) и легко тестируемы.
Я склонировал репозиторий redux, открыл в редакторе папку с исходниками (игнорируя docs, examples и прочее) и взялся за ножницы клавишу Delete:

  • Удалил все комментарии из кода
    Каждый метод библиотеки задокументирован с помощью JSDoc весьма подробно
  • Убрал валидацию и логирование ошибок
    В каждом методе жёстко контролируются входные параметры с выведением очень приятных глазу подробных комментариев в консоль
  • Убрал методы bindActionCreators, subscribe, replaceReducer и observable.

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

А теперь давайте разберём то, что осталось



Пишем redux за 7 строк


Весь базовый функционал redux умещается в малюсенький файлик, ради которого вряд ли кто-нибудь будет создавать github репозиторий :)

function createStore(reducer, initialState) {
    let state = initialState
    return {
        dispatch: action => { state = reducer(state, action) },
        getState: () => state,
    }
}

Всё. Да, серьёзно, ВСЁ.

Так устроен redux. 18 страниц вакансий на HeadHunter с поисковым запросом «redux» — люди, которые надеются, что вы разберетесь в 7 строках кода. Всё остальное — синтаксический сахар.

С этими 7 строками уже можно писать TodoApp. Или что угодно. Но мы быстренько перепишем TodoApp из документации к redux.

// Инициализация хранилища
function todosReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ]
    case 'TOGGLE_TODO':
      return state.map(todo => {
        if (todo.id === action.id) {
          return { ...todo, completed: !todo.completed }
        }
        return todo
      })
    default:
      return state
  }
}

const initialTodos = []

const store = createStore(todosReducer, initialTodos)

// Использование
store.dispatch({
  type: 'ADD_TODO',
  id: 1,
  text: 'Понять насколько redux прост'
})

store.getState() 
// [{ id: 1, text: 'Понять насколько redux прост', completed: false }]

store.dispatch({
  type: 'TOGGLE_TODO',
  id: 1
})

store.getState() 
// [{ id: 1, text: 'Понять насколько redux прост', completed: true }]

Уже на этом этапе я думал бросить микрофон со сцены и уйти, но show must go on.
Давайте посмотрим, как устроен метод.

combineReducers


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

Используется он так:

// здесь мы переиспользуем метод todosReducer из прошлого примера

function counterReducer(state, action) {
  if (action.type === 'ADD') {
    return state + 1
  } else {
    return state
  }
}

const reducer = combineReducers({
  todoState: todoReducer,
  counterState: counterReducer
})

const initialState = {
  todoState: [],
  counterState: 0,
}

const store = createStore(reducer, initialState)

Дальше использовать этот store можно так же, как предыдущий.

Разница моего примера и описанного в той же документации к TodoApp довольно забавная.

В документации используют модный синтаксис из ES6 (7/8/?):

const reducer = combineReducers({ todos, counter })

и соответственно переименовывают todoReducer в todos и counterReducer в counter. И многие в своём коде делают то же самое. В итоге разницы нет, но для человека, знакомящегося с redux, с первого раза эта штука выглядит магией, потому что ключ части состояния (state.todos) соответствует функции, названной также только по желанию разработчика (function todos(){}).

Если бы нам нужно было написать такой функционал на нашем micro-redux, мы бы сделали так:

function reducer(state, action) {
  return {
    todoState: todoReducer(state, action),
    counterState: counterReducer(state, action),
  }
}

Этот код плохо масштабируется. Если у нас 2 «под-состояния», нам нужно дважды написать (state, action), а хорошие программисты так не делают, правда?
В следующем примере от вас ожидается, что вы не испугаетесь метода Object.entries и Деструктуризации параметров функции
Однако реализация метода combineReducers довольно простая (напоминаю, это если убрать валидацию и вывод ошибок) и самую малость отрефакторить на свой вкус:

function combineReducers(reducersMap) {
  return function combinationReducer(state, action) {
    const nextState = {}
    Object.entries(reducersMap).forEach(([key, reducer]) => {
      nextState[key] = reducer(state[key], action)
    })
    return nextState
  }
}

Мы добавили к нашему детёнышу redux ещё 9 строк и массу удобства.

Перейдём к ещё одной важной фиче, которая кажется слишком сложной, чтобы пройти мимо неё.

applyMiddleware


middleware в разрезе redux — это какая-то штука, которая слушает все dispatch и при определенных условиях делает что-то. Логирует, проигрывает звуки, делает запросы к серверу,… — что-то.

В оригинальном коде middleware передаются как дополнительные параметры в createStore, но если не жалеть лишнюю строчку кода, то использование этого функционала выглядит так:

const createStoreWithMiddleware = applyMiddleware(someMiddleware)(createStore)
const store = createStoreWithMiddleware(reducer, initialState)

При этом реализация метода applyMiddleware, когда ты потратишь 10 минут на ковыряние в чужом коде, сводится к очень простой вещи: createStore возвращает объект с полем «dispatch». dispatch, как мы помним (не помним) из первого листинга кода, — это функция, которая всего лишь применяет редюсер к нашему текущему состоянию (newState = reducer(state, action)).
Так вот applyMiddleware не более чем переопределяет метод dispatch, добавляя перед (или после) обновлением состояния какую-то пользовательскую логику.

Возьмём, например, самый популярный middleware от создателей redux — redux-thunk

Его смысл сводится к тому, что можно делать не только

store.dispatch({type: 'SOME_ACTION_TYPE', some_useful_data: 1 })

но и передавать в store.dispatch сложные функции

function someStrangeAction() {
  return async function(dispatch, getState) {
    if(getState().counterState % 2) {
       dispatch({
         type: 'ADD',
       })
    }
    await new Promise(resolve => setTimeout(resolve, 1000))
    dispatch({
      type: 'TOGGLE_TODO',
      id: 1
    })
  }
}

И теперь, когда мы выполним команду

dispatch(someStrangeAction())

то:

  • если значение store.getState().counterState не делится на 2, оно увеличится на 1
  • через секунду после вызова нашего метода, todo с id=1 переключит completed true на false или наоборот.

Итак, я залез в репозиторий redux-thunk, и сделал то же самое что и с redux — удалил комментарии и параметры, которые расширяют базовый функционал, но не изменяют основной

Получилось следующее:

const thunk = store => dispatch => action => {
  if (typeof action === 'function') {
    return action(store.dispatch, store.getState)
  }
  return dispatch(action)
}

я понимаю, что конструкция
const thunk = store => dispatch => action
выглядит жутковато, но её тоже просто нужно вызвать пару раз с произвольными параметрами и вы осознаете, что всё не так страшно, это просто функция, возвращающая функцию, возвращающую функцию (ладно, согласен, страшно)

Напомню, оригинальный метод createStore выглядел так

function createStore(reducer, initialState) {
    let state = initialState
    return {
        dispatch: action => { state = reducer(state, action) },
        getState: () => state,
    }
}

То есть он принимал атрибуты (reducer, initialState) и возвращал объект с ключами { dispatch, getState }.

Оказалось, что реализовать метод applyMiddleware проще, чем понять, как он работает.
Мы берём уже реализованный метод createStore и переопределяем его возвращаемое значение:

function applyMiddleware(middleware) {
  return function createStoreWithMiddleware(createStore) {
    return (reducer, state) => {
      const store = createStore(reducer, state)

      return {
        dispatch: action => middleware(store)(store.dispatch)(action),
        getState: store.getState,
      }
    }
  }
}

Вывод


Под капотом redux содержатся очень простые логические операции. Операции на уровне «Если бензин в цилиндре загорается, давление увеличивается». А вот то, сможете ли вы построить на этих понятиях болид Формулы 1 — уже решайте сами.

P.S.


Для добавления в мой «micro-redux» упрощённого метода store.subscribe потребовалось 8 строк кода. А вам?

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


  1. mayorovp
    06.02.2019 05:43

    Ваша реализация combineReducers нарушает главный инвариант redux "ссылочная эквивалентность равносильна отсутствию изменений". Этот инвариант важен, к примеру, для оптимизации рендера.


    Например, составной редьюсер должен выглядеть вот так:


    function reducer(state, action) {
      const next = {
        todoState: todoReducer(state.todoState, action),
        counterState: counterReducer(state.counterState, action),
      }
      if (next.todoState === state.todoState && next.counterState === state.counterState)
        return state;
      else
        return next;
    }


    1. serf
      06.02.2019 08:07

      Я не работал с redux и react, но разве там нет селекторов с мемоизацией или подобия RxJS оператора distinctUntilChanged?


      1. mayorovp
        06.02.2019 08:42

        Подобие distinctUntilChanged есть в React, называется PureComponent. Но чтобы оно работало — нужно избегать лишних пересозданий объекта, чего код автора не делает.


    1. v1vendi Автор
      06.02.2019 09:40

      Этот момент я и правда упустил. Но мой пример настолько вырожденный, что, надеюсь, читатели мне его простят :)


      1. mayorovp
        06.02.2019 09:43

        Тем не менее, к вашей реализации combineReducers придётся добавить еще 3-5 строк. Учитывая общее количество строк, это важно.


  1. artalar
    06.02.2019 07:33

    2019 год, несмотря на опыт индустрии, в попытках понять редакс пришлось проинспектировать весь его код, что бы написать его сломанную копию. Простите, но это забавно.

    Вообще редакс простой да, мне больше всего понравилось его объяснение как «EE с мидлварами и единственным атомом на конце». Проблема в том что современные джуны не знают ни про мидлвары, ни про EE (event emmiting), ни про атомы.

    P.S. в свое время это сделало мой день github.com/reduxjs/redux/commit/9276ff0af6400633d6731b15fed6e70c3561887e


    1. serf
      06.02.2019 08:09

      Нужно было так написать: Redux itself is very simple until it's not.


    1. justserega
      06.02.2019 08:32
      +1

      У меня другой вопрос — а зачем вообще нужен редакс? Действительно ли нужны иммьютабельные состояния, чтобы отреагировать на нажатие кнопки? Несколько раз пытался изучить и внедрить его и каждый раз больше проблем, чем пользы.
      Сейчас используем vue и наконец-то разработка похожа на работу c UI, а не запуск космического корабля.


      1. artalar
        06.02.2019 08:37
        +1

        Не любой инструмент применим к любой задаче. Конечно редакс не нужен для реакции на нажатие кнопки. Редакс, как single store, особенно удобен в монолитных приложениях, где есть сложная логика связанных данных и особенно важно иметь SSOT.

        Лично мое мнение, редакс — это фреймворк для реализации flux паттерна.


        1. justserega
          06.02.2019 10:10

          Ну тут сложилась странная ситуация: на голом react много не напишешь, разработчик react не дает никакого фреймворка, а только концепцию flux. Redux самая популярная реализация flux — при этом довольно костыльная как по мне: например ajax запросы очень странно делаются, заморочки с иммьютабельностью. И может я что-то не понимаю, но мне сложно писать на нем даже простые приложения, как на нем делают сложные вообще не представляю.


          1. artalar
            06.02.2019 10:51

            Архитектура редакса очень хороша и к ней стоит прислушиваться Вот проблема в бойлерплейте, это да.


            1. anprs
              06.02.2019 13:44

              1200 строк кода чтобы сказать «store > дай мне user'a > нету? > сходи на бэк»


              1. artalar
                06.02.2019 14:05

                Не понимаю о каких 1200 строках речь


            1. serf
              06.02.2019 18:51

              При использовании хорошо подобранных интсрументов бойлерплейт вполне приемлемый.


          1. xitt
            06.02.2019 23:35
            +1

            Как раз простые на нем писать не надо. Он оправдан когда количество компонент зашкаливает, скажем, за 100.


            1. Druu
              07.02.2019 14:35
              +1

              Как раз простые на нем писать не надо. Он оправдан когда количество компонент зашкаливает, скажем, за 100.

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


              Кроме того — искаробки не идет никаких методов для управления эффектами и отдельно — запросами. Стандартный подход (по три экшена на запрос) — не масштабируется от слова совсем, т.к. уже в среднего размера приложениях количество этих экшенов начинает измеряться тысячами, а любая нетривиальная логика становится очень сложной для отслеживания — во-первых она размывается по многим местам, во-вторых — ее отслеживание не поддерживается на уровне ide (в отличии от обычных функциональных вызовов).
              кроме того — экшоны не composable.


              1. faiwer
                07.02.2019 15:05
                +1

                количество этих экшенов начинает измеряться тысячами

                Мы уже очень долго обсуждали это ранее на хабре (это невозможно забыть) :-)


                Это проблема именно вашего подхода к redux (а точнее к actionCreator-ам). Там где у всех 10 универсальных экшнов, у вас целая 1000 копипастных с бизнес-логикой.


                1. Druu
                  07.02.2019 16:05

                  Это проблема именно вашего подхода к redux (а точнее к actionCreator-ам). Там где у всех 10 универсальных экшнов

                  Я же специально указал, что это про "чистый редакс" и "Стандартный подход (по три экшена на запрос)".


                  Если же использовать редакс в качестве набора примитивов для построения своей системы стейт-менеджмента (где будет и изоляция, и архитектура, и абстракции и нормальная работа с асинхронщиной), то, конечно, это решаемо.


                  Ну и да, судя по гитхабам (да и по тому что я видел в своей практике) — как раз все и плодят эти десятки экшенов, которые превращаются в сотни (и, в перспективе — в тысячи), а исключение скорее как раз, когда над редаксом делают свою кастомную надстройку с генераторами экшенов/редьюсеров/саг и т.д.


                  1. xitt
                    07.02.2019 17:40

                    Это и есть набор примитивов. О чем и статья.


                    1. Druu
                      08.02.2019 08:01

                      Это и есть набор примитивов.

                      Именно про это я и говорил. К сожалению, большинство этого не понимает и использует этот набор примитивов как есть.


                      О чем и статья.

                      Нет, статья совсем не о том.


                      1. xitt
                        08.02.2019 16:19

                        =К сожалению, большинство этого не понимает и использует этот набор примитивов как есть.= Может быть потому что большинству просто не надо его использовать, как его нет? Я имею в виду нет необходимости чтото выдумывать, потому что он действительно простой как пробка.


                1. xitt
                  07.02.2019 17:50

                  А можно ссылку на дискуссию, если не затруднит? Благодарю заранее.


                  1. faiwer
                    07.02.2019 18:16

                    Нашёл. Забавно, но разговор начался с фразы "Просто скажите — сколько у вас тысяч экшенов?" :)


                    1. xitt
                      07.02.2019 22:41
                      +1

                      Спасибо, все ясно )))


              1. xitt
                07.02.2019 17:39

                А что предоставляет?


                1. Druu
                  08.02.2019 08:06

                  Обычные функции, представьте себе. Они и composable, и нормально ищутся по коду, имеют нативные средства для группировки/изоляции и вообще имеют полную поддержку со стороны всей языковой инфраструктуры напрямую.


                  Вообще, при работе с редаксом следует заменить с самого начала одну главную вещь — если вы создали action, то вы потом его диспатчите в стор. Ни для чего другого action'ы не используются, нет других сценариев. С-но, вместо того, чтобы иметь ф-и, которые создают экшены, вы можете создавать ф-и, которые создают экшон и сразу его диспатчат в стор. И потом работать с этими ф-ми вместо экшенов и экшен криэйторов. Это сразу все упрощает и делает на порядок удобнее.


                  1. xitt
                    08.02.2019 16:16
                    +1

                    Да я уже понял, пишите на обычных функциях свой велосипед каждый раз, если никто не мешает, сочиняйте каждый раз свою архитектуру, абстракции и изоляции, а потом обучайте свою команду ему, раз уж редаксом пользоватья не получается. Вопрос же в том что у вас не получается, это не значит что редакс не подходит для больших проектов. Люди используют его во всему миру, и мне проще человеку дать ссылку почитать и он начнет писать редусеры — эпики — саги. Да, не идеально, но ничего другого вы лично кроме ванилла жс предложить не можете. Ну и потом, кто вообще вас заставляет пользоваться экшен криейторами, сделайте фактори и диспатчте на здоровье один акшен и свои фунции в пейлоаде, если так хочется, в чем проблема? Пара движений мышью и «главная вещь» ушла. Экшен криеторы это не редакс, никто не заставляет именно так создавать и диспетчить.


      1. serf
        06.02.2019 08:43
        +1

        Для крупных развивающихся приложений идея центрального стора довольно полезена. Особенно принцип single source of truth. Несколько раз бывало делал по-быстрому без стора некоторую часть функционала и каждый раз в итоге приходилось потом переделывать на стор (что сложнее чем изначально сделать на сторе), тк со временем стало сложно развивать и поддерживать штуковины где состояние изолированно хранилось в самих компонентах или где-то в разбросанных сервисах.


        1. justserega
          06.02.2019 10:17

          Вы сейчас больше про flux говорите, в общем-то все верно. Но redux — это реализация, которая к этому добавляет:
          1. редьюсеры чистые функции (а значит мы не можем использовать внутри грязные функции типа ajax запросов)
          2. состояние иммьютебельно и едино (библиотеки и combineReducers конечно помогают, но код читать и отлаживать довольно тяжело)


          1. serf
            06.02.2019 10:22

            2. состояние иммьютебельно и едино (библиотеки и combineReducers конечно помогают, но код читать и отлаживать довольно тяжело)
            C github.com/mweststrate/immer можно менять стейт императивно, как-будто напрямую, а библиоетка сделают грязную работу за вас обрабатывая прокси дифф.

            Иммутабельность помимо прочего позволяет выполнять сравнение по ссылке при ченж детекшене, что при правильном использовани позволяет избежать лишних отрисовок и в целом значительно увеличить производительность.


          1. artalar
            06.02.2019 10:50

            — «мы не можем использовать внутри грязные функции типа ajax запросов» — для этого есть мидлвары. Разделение ответственности — это прекрасно.
            — «но код читать и отлаживать довольно тяжело» — мутабельный код отлаживать на порядок тяжелее. Я помню этот ужас, когда логируешь стейт в Alt.js, а он отображается текущим в консоле, а не на момент лога, это совсем не явно, потом приходилось глубокую копию делать, что бы продебажить нормально. Я бы и без редакса использовать иммутабельность — она не такая дорогая, но дебажить ее в разы проще.


            1. justserega
              06.02.2019 11:31
              +1

              Наверное я просто не умею готовить редакс, но вот прям совсем не сложились отношения.

              > Разделение ответственности — это прекрасно.
              Разделение ответственности по функционалу прекрасно, а что прекрасного в разделение логически цельной функции на несколько? В чем преимущество здесь делать запрос отдельно?


              1. Gemorroj
                06.02.2019 12:06

                вот тоже, пока что только слышал восторженные отзывы, но когда сам пытался писать на react/redux то мозг вскипал на простых казалось бы штуках.


                1. serf
                  06.02.2019 14:00

                  Взять что-то вроде github.com/pelotom/unionize для экшенов и их матчинга + github.com/mweststrate/immer для имутабельности (и то и другое дружит с TypeScript) + что там у вас в риактовом мире для сайд эффектов и асинхронности используется и должно быть проще.



        1. rgs350
          06.02.2019 20:38

          изначально сделать на сторе

          Вот меня напрягяет когда простые вещи, имеющие отношение только к V начинают запихивать в store.
          Нам нужен сайдбар с одним единственным свойством collapsed. Для этого нам нужно (если следовать документации к redux):

          1. Придумать константу с названием действия.
          2. Написать ничего не делающий actionCreator.
          3. Добавить в reducer обработку действия.
          4. Написать компонент-контейнер подключающий Sidebar к Redux.

          И всё это в разных файлах. Пример конечно несколько утрированный, но это же программирование ради программирования.


          1. serf
            06.02.2019 20:49

            redux в каническом виде я никогда не использовал, преполагаю если следовать каким-то поверхностным гайдам, то бойлерплейта действительно может получиться много. Константы на экшены не нужны, так же как и классы, с помощью github.com/pelotom/unionize можно лаконично описать весь бандл экшенов в одном месте и этот же бандл испольщзовать для матчинга в ридьюсере вместо свичей + поддержка TypeScript (очень помогает контролировать сложность).

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


            1. rgs350
              07.02.2019 10:52

              Не использовал unionize, но выглядит не очень читаемо КМК.

              Константы на экшены не нужны
              Вроде бы да, но нет. В большом приложении будет очень много actionCreatoro-в и, рано или поздно, захочется разделить их на насколько файлов. При добавлении нового действия придется просмотреть все эти файлы и убедиться что действий с таким же action.type не существует.
              Бывает нужно например переключать это поле collapsed из самых разных мест...
              А в каком-нибудь, не любимым всеми, ExtJS это бы выглядело приблизительно так:
              function onTriggerClick() {this.down('sidebar').toggle()}
              И больше ничего.


              1. serf
                07.02.2019 11:03

                Не использовал unionize, но выглядит не очень читаемо КМК.

                Дело не только в читаемости, но в поддежке TS. Есть много аналогов, но именнно эта библиотека сама по себе небольшая, но гибкая. Делать отдельные файлы на каждый экшен это действительно плодить много бойлерплейта. Мне удобнее объединять экшены в unionize бандлы группируя их по назначению. Правда бибиотека пока что не поддеривает префиксы для бандлов, но ее можно форкнуть и добавить.

                function onTriggerClick() {this.down('sidebar').toggle()}
                Полагаю вызов down ресолвит вложенный компонент и явно вызывает у компонента метод toggle. Но это ведь жесткая привязка к структуре дерева компонентов. Кроме того строгую типизацию такого сделать будет сложновато. Ну и в скорости ресолвинга не уверен.


                1. rgs350
                  07.02.2019 11:29

                  Делать отдельные файлы на каждый экшен
                  Ну не так же радикально. Логически разбить по несколько экшенов в файл (в каком-нибудь TODO это действия относящиеся к пользователю и действия относящиеся к задачам).
                  Полагаю вызов down ресолвит вложенный компонент
                  Это только для примера. Перемещаться по дереву можно было в любом направлении.


        1. Druu
          07.02.2019 14:42
          +1

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

          Это верно только для очень маленьких приложений. Вообще же изоляция стейта ведет к снижению coupling, то есть полезна почти всегда. Вынос стейта в глобальную переменную — это всегда грязное решение, которое удобно в краткосрочной перспективе, но ведет к драматическим последствиям в долгой. Просто на долгую перспективу во фронетенде в 2019 обычно не работают.
          Правильное решение в случае проблем с взаимодействием модулей — переработка связей между модулями.


      1. rgs350
        06.02.2019 18:14

        Действительно ли нужны иммьютабельные состояния
        Да это больше похоже на костыль для реакта ибо
        — передавая в компоненты реакта мутабельные объекты вы не сможете нормально реализовать PureComponent.
        — Это приведет к реконсилированию (так оно кажется называется) практически всей ветки компонентов.
        — А это приведет к тормозам в приложениях сложнее HelloWorld.


        1. serf
          06.02.2019 18:57

          Наоборот. Иммутабельность позволяет сравнивать данные просто по ссылке, вместо глубокого сравнения, что позволяет быстро пропускать ненужные тяжелые операции (как правило это отрисовка). Пересоздавать объекты на пути от дочернего поля до самой верхушки объекта нужно чтобы ссылки на объект поменялись, тк потребители стора / компоненты могут подписываться на изменения любого уровня вложенности на пути к дочернему изменяемому значению, а все что вложено в объект это его часть. Очевидно очень желательно делать структуру стора как можно более плоской.


          1. rgs350
            06.02.2019 19:08

            Хм… А я о чём? Об одном и том же говорим. Иммутабельность нужна в первую очередь реакту.


            1. serf
              06.02.2019 19:11

              Ну почему только реакту, это общее понятие. Дропнуть тяжелую операцию много где полезно может быть. В ангуляре тоже для компонентов можно включить «on push» режим и использовать полезность иммутабельности.


              1. rgs350
                06.02.2019 19:18
                +1

                это общее понятие
                Понятно что общее, но мы же только про Redux говорим.


            1. PaulMaly
              07.02.2019 11:03

              Я не фанат react/redux, но стало интересно как вы предлагаете «понимать» что какое-то свойство объекта изменилось без иммутабильности?


              1. serf
                07.02.2019 11:10
                +1

                Вариантов не много:
                — Использовать иммутабельные данные, лучше простых типов (примитивы + объект и массив), тогда можно просто сравнивать по ссылке. Вывод: дешевое сравнение.
                — Делать deep checking. Вывод: дорогое сравнение, тормоза.
                — Использовать обертку с методами get/set или что-то вроде прокси или Object.defineProperty и внутри творить магию. Вывод: магия, вероятно большее потребление памяти, хакнутая структура данные и тд.


                1. PaulMaly
                  07.02.2019 17:20

                  Вообще, вопрос бы адресован rgs350 на тему, что иммутабельность нужна только реакту. Но все равно спасибо, что просветили)))


              1. rgs350
                07.02.2019 11:48

                И тут мы плавно подошли к мысли, что один store, оповещающий все компоненты не такое уж и хорошее решение :)


                1. serf
                  07.02.2019 11:56

                  Почему если стор иммутабельный?


                  1. mayorovp
                    07.02.2019 13:02
                    +1

                    Потому что оповещающий все компоненты.


                    1. serf
                      07.02.2019 14:00
                      +1

                      Ну так то стор глобальный, можно ведь придумать как не использовать один глабальный. Но в целом какая здесь проблема? Пускай оповещает все компоненты, реагировать логикой будут только те которые должны, остальные просто пропустят сигнал сравнив данные по ссылке что недорого.

                      Именно один глобальный стор и дает single source of truth бенефит.


                1. faiwer
                  07.02.2019 13:35

                  один store, оповещающий все компоненты

                  Всё так, но мне кажется, тут важно отметить, что это фишка react-redux. И никто нас не сковывает в этом деле. Можно нарисовать свою древовидную систему обновлений, или воспользоваться уже какой-нибудь готовой.


                  Т.е. это не проблема подхода как такового. Это проблема конкретного решения.


                  1. artalar
                    08.02.2019 11:32

                    Работать со списками элегантно всеравно не получится


                    1. faiwer
                      08.02.2019 12:29

                      Особенно если это списки на сотни элементов без virtualScroll-а :)


                1. PaulMaly
                  07.02.2019 17:24

                  Причем тут мой вопрос и стор, который кого-то там оповещает? Я спросил чисто про иммутабельность. Вы сказали она нужна только для работы реакта, вот я и хочу узнать, может вы знаете другой способ понять изменилось ли свойство объекта.


                  1. rgs350
                    07.02.2019 19:28
                    -1

                    Мы ведь по прежнему говорим про иммутабельность применительно к редаксу?

                    вы знаете другой способ понять изменилось ли свойство объекта
                    А вы ответьте себе на вопрос зачем вам понимать что свойство изменилось? и сразу станет понятно
                    Причем тут стор, который кого-то там оповещает


                    1. PaulMaly
                      07.02.2019 21:00

                      Мы ведь по прежнему говорим про иммутабельность применительно к редаксу?

                      Нет, вы сказали про иммутабильность по отношению к реакт:

                      Иммутабельность нужна в первую очередь реакту.

                      Redux независимая библиотека, которую используют далеко не только в React проектах.

                      А вы ответьте себе на вопрос зачем вам понимать что свойство изменилось? и сразу станет понятно

                      Очевидно, чтобы применить это изменение к DOM. А вы знаете иной способ синхронизации стейта и его представления в DOM дереве?


                      1. rgs350
                        08.02.2019 12:15
                        +1

                        Оффтоп
                        Redux независимая библиотека, которую используют далеко не только в React проектах.
                        Если кто-то притащил эту либу в проект не связанный с реактом, то это совсем не означает, что она является оптимальным выбором. Просто у многих frontend-разработчиков на текущий момент наблюдается эффект Даннига-Крюгера совместно с тяжелейшим религиозным повреждением головы. Для эксперимента попробуйте порекомендовать С++/JAVA-разработчику использовать подходы из редакс в своей повседневной деятельности. Только наденьте средства индивидуальной защиты, поскольку его ответ будет находится где-то в интервале между плевком в лицо и нанесением тяжких телесных повреждений.


                        1. faiwer
                          08.02.2019 12:36

                          На самом деле так можно придти к тому что у вас вообще все скалярные значения будут отдельными сторами. И по сути вы переизобретёте observer :) (redux это по сути 1 observable c заморочками)


                        1. PaulMaly
                          08.02.2019 19:00
                          -1

                          Без обид, но под спойлером флуд, даже не оффтоп, поэтому пусть там и остается. Если вы считаете что я выгораживаю Redux или React, то вы ошибаетесь. Ни то, ни другое мне не близко.

                          Однако ваши примеры отражают не понимание вопроса. Вот мой пример:
                          — Есть объект с данными а N-ой вложенности.
                          — Есть один компонент А, который его использует.
                          — Изменяется свойство объекта а на неком уровне вложенности (пусть будет a.b.c).
                          — Как нам оптимально применить это изменение в DOM?

                          Ту банальщину, которую вы написали давайте обсуждать не будем.


                          1. mayorovp
                            08.02.2019 20:01

                            Неужели нужно подписаться на изменения объектов a, a.b и a.b.c?


                          1. rgs350
                            08.02.2019 20:29

                            У вас какое-то свое, очень странное, представление о связи сторов, иммутабельности, компонентов и DOM-а. Поделились бы что-ли истиной, доступной почему-то только вам. ;)

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


      1. Druu
        07.02.2019 14:27
        +1

        У меня другой вопрос — а зачем вообще нужен редакс?

        Редакс — это просто набор примитивов, из которых можно собрать решение для менеджмента стейта вашей системы (для применения сам по себе, без обработки напильником, редакс никогда не предназначался). Безусловно, можно все эти по-5-строчек написать и самому, и каждый раз переписывать, или иметь свой набор и из проекта в проект тянуть. Но поскольку реалии современного фронтенда таковы, что команды/проекты меняются чуть не по нескольку раз в год — желательно использовать библиотеку, которую все знают.


        1. justserega
          08.02.2019 09:04

          Вопрос был не в том, что такое редакс, а в том, что его подход крайне странный и избыточный в контексте управления UI. Я понимаю, что js был на переднем крае прогресса и было много разного рода поиска новых подходов и экспериментов — и это очень круто (без иронии)!
          Редакс предлагает очень привлекательные принципы rajdee.gitbooks.io/redux-in-russian/content/docs/introduction/ThreePrinciples.html. Но в жизни оказывается много бойлерплейта, ломания головы над правильной структурой стейта, странные костыли и т.д.


          1. Druu
            08.02.2019 11:34

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

            Так я же вам и говорю — бойлерплейт возникает от того, что используются низкоуровневые примитивы сами по себе. Если оборачивать их в абстракции — тогда он уходит.


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


            1. justserega
              08.02.2019 12:19

              Я имел ввиду redux по сравнению с другими подходами к программирования UI (например Angular, Vue или из других областей WinForms, WPF, да даже другие реализации Flux)

              Там бойлерплейта в разы меньше, стейт не надо так продумывать, о чистоте обработчиков (редьюсеров) заботиться, об иммьютабельности и т.д.

              И вопрос заключается в том перекрывают ли плюсы редакса те неудобства, что он создает по сравнению с другими подходами.


  1. i360u
    06.02.2019 16:14

    Как человек давно и вполне успешно занимающийся велосипедостроением, я не могу понять как такую простую вещь как стейт-менеджмент уровня Redux можно было реализовать настолько сложно. Блин, такие вещи как модуль с хранилищем и паб-сабом пишутся на коленке и прекрасно работают без кучи этих безумных свитчей, экшенов и редьюсеров. При работе со сложными графоподобными данными с Redux вы быстро упираетесь в ограничения такой модели, в простых — делаете кучу лишних телодвижений. Зачем?


    1. v1vendi Автор
      06.02.2019 18:22

      Вы можете заметить даже по этой статье, что не так уж сложно реализован стейт-менеджмент у Redux. По ограничениям — на нашем проекте достаточно комфортно себя чувствует себя модель в 70кб данных и ~300 условиями в reducers. А реализация redux по моему скромному опыту для больших состояний оказывается удобнее, чем паб-саб


      1. i360u
        06.02.2019 18:36

        Боюсь сложность данных определяется не размером в килобайтах, а отношениями между сущностями их составляющими (пересекающиеся связи, различные источники и т.д.). И, говоря про сложность Redux, я говорю не о сложности технической реализации, а о привнесенных абстракциях, и о работе с ним непосредственно. Для сложных случаев я использую графы, для простых — паб-саб.


        1. faiwer
          06.02.2019 19:47
          +1

          Всё решаемо. У меня довольно тесно переплетённый граф данных сейчас в приложении (редактор расписаний в школе, очень много взаимосвязей). Поэтому есть деревья селекторов (это такие кеш-функции для просчёта чего-угодно на основе store-данных). Всё "летает", несмотря на то, что некоторые штуки вычисляются на лету, скажем, при drag-n-drop. Но приходится использовать обильно мозг при построении архитектуры этих самых селекторов и работы со store-ом в целом. Всё, разумеется, иммутабельно. В принципе удобно. И очень легко дебажится, работает очень предсказуемо. Тут самое главное в архитектуре стора не ошибиться сильно. Любой серьёзный рефакторинг в этом деле — боль. Забыл добавить, данные в сторе нормализованы. Плюс используются мемоизация на основе weakMemoze, включая вложенные weakMemoize-ы. А для reducer-ов пишется "аля-мутабельный" код с proxy (т.е. по факту иммутабельный). В общем никакого криминала, когда уже набил руку на предыдущих проектах.


          Вначале я попробовал реализовать этот же проект на Vue. Я столкнулся с очень серьёзной проблемой в tracking dependencies механизме и в итоге отказался от Vue. Думаю что для проектов с большим графом данных я Vue больше выбирать не буду. Были варианты остаться и использовать вместо computed скажем watchers, или вообще притащить туда что-нибудь типа rxJS… но зачем мне тогда Vue? :)


          1. serf
            06.02.2019 20:45

            Я столкнулся с очень серьёзной проблемой в tracking dependencies механизме и в итоге отказался от Vue.
            Если обобщить во Vue слишком много неподконтрольной «магии»?


            1. faiwer
              06.02.2019 20:59

              Попробую кратко описать ту проблему:


              • есть 5000+ observable values
              • есть computed A, который их перебирает
              • есть computed B, который делает "A + 1"

              Что происходит в случае KnockoutJS, если A уже был просчитан ранее, а B нет?


              • knockout дёргает закешированное значение от A
              • knockout связывает напрямую B с A, чтобы работала реактивная магия

              Что происходит в случае с Vue?


              • vue дёргает закешированное значение от А
              • vue связывает B с A.dependecies.*, коих 5000

              Итого O(1) vs O(n). Такое происходит с любыми computed какие только в проекте есть и завязаны на эти A. А у меня дерево таких. Плюс в моём случае это всё рендерится в таблице, где может быть до 160 ячеек разом. И каждая ячейка помимо render-method-computed может содержать ещё всякие другие computed. И все они непременно дёрнут те самые 5000. Думаю излишне говорить, что это начинает просто нещатно тормозить даже на самых малых выборках. А проект предполагал куда более сложные связи.


              В итоге я заменил Vuex на Redux, Vue\Vuex Computed на Reselect, Vue Components на React Components. По сути практически та же кодовая база, но всё работает молниеносно. Ну и очевиднее в разы :) И ещё есть куда лихо заоптимизировать если будет мало.


              За точный механизм работы Vue и Knockout не ручаюсь. Уже успел всё подзабыть. Пишу по старой памяти. Если чего нахимичил — прошу сильно не пинать :)


              1. mayorovp
                06.02.2019 21:01

                Если не секрет, то почему вы с vue в таком случае переходили на react+redux, а не на react+mobx?


                1. faiwer
                  06.02.2019 21:07

                  Ответ прост: дедлайны наступали на пятки. Взял то, что умею хорошо готовить. В режиме copy-paste понадёргал с прошлых проектов что надо и просто методично шаг-за-шагом переписал. Интерес к mobX питаю, но пока ещё с ним не работал. Уверен, там как и везде, есть свои тонкости. А я итак исчерпал все резервы времени к тому времени )


              1. bgnx
                07.02.2019 15:00
                +2

                В mobx-е описанной вами проблемы с компьютедами нет. Там компьютеды не только вычисляются иерархически но и решают проблему "ромба" (дублирующие вычисления при ромбовидных зависимостях) и условных подписок (когда зависимости меняются во время вычисления и могут быть лишние запуски). В общем используемый алгоритм гарантирует что при любых ромбовидных или условных зависимостях запуск компьютеда произойдет только один раз в цепочке вычислений (и также дополнительно можно заврапить в транзакцию чтобы множественные изменения внутри функции схлопнулись в один запуск цепочки вычислений). Подробнее про алгоритм можете почитать в этой статье и посмотреть примерную реализацию тут или тут или с дополнительными оптимизациями тут


                1. justserega
                  08.02.2019 09:05
                  -2

                  А можно сразу взять vue, вместо связки mobx + react


                  1. mayorovp
                    08.02.2019 12:39
                    +1

                    Разумеется. Переходя с vue на что-то ещё из-за проблем с производительностью, нужно переходить именно на vue :-)


                  1. faiwer
                    08.02.2019 12:57

                    Вы комментарии читаете в случайном порядке? :)


          1. i360u
            06.02.2019 22:34

            Понятно что все решаемо, непонятно зачем колоться об этот кактус если все можно сделать проще и красивее.


            1. faiwer
              06.02.2019 22:43

              Не ну если знать как и уметь, то какие проблемы? :) Скажем я пока не научился решать такие вещи эффективно используя observer и без глобального стейта. Какие-то мутные путанные клубки получаются. Технический долг неистово накапливается. Тут практика нужна. Понятно что существуют тысячи вариаций как сделать это хорошо. Но вот без опыта они не приходят.


              По redux/flux есть много учебных материалов (скажем "пятиминутка React"), которые показывают и рассказывают, что да как. Спустя 4-5 проектов на Redux мне всё ещё кажется, что с ним всё довольно очевидно делается. Не приходится ломать голову. Этот деревянный простой как грабли подход сам вынуждает делать удобную архитектуру.


              ИМХО


          1. alloky
            07.02.2019 01:30

            А как быть, если есть большая связность данных, но при этом чуть ли не на каждое действие нужно делать запрос к api и менять store?


            1. faiwer
              07.02.2019 08:04

              Примерно так:


              • Нормализация данных в с store
              • redux-thunk | redux-saga | custom middleware | 100500 других решений
              • DRY

              В любом случае решения без внешнего store и с observable будут проще. Т.к. всё расположено прямо по месту. Flux заставляет разделять виды действий\команды, их реализацию и места применения. Redux ещё рекомендует держать конечные View предельно тупыми. Это всё неизбежно приводит к разбуханию кода. Но в средне и долгосрочной перспективе это сильно помогает. Это как строгая типизация — добровольные кандалы, которые дают определённые преимущества. Вопрос лишь в том, дают ли они именно вам больше, чем отнимают.


    1. serf
      06.02.2019 19:02

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


      1. i360u
        06.02.2019 19:36

        Во первых, собственное решение отнюдь не означает отсутствия стандартизации и документации, и описать 2 метода типа State.publish(path value) и State.subscribe(path, handler) — совсем не сложно. И на то чтобы понять, как это работает у любого новичка уйдет ровно одна минута, в отличие от Redux. Во вторых, я не то чтобы призываю всех к тотальному велосипедостроению, я просто недоумеваю почему наиболее популярным стало именно такое, нелепое на мой взгляд, решение как Redux.


        1. v1vendi Автор
          06.02.2019 21:47

          Вы своим решением предлагаете компонентам-пользователям состояния следить за изменениями в состоянии самостоятельно — через subscribe на изменения. Именно от этого уходил React, например. React компонентам плевать на то, откуда прилетело изменение, они знают только о факте изменения состояния и перерисовываются на основании свежего изменения. Но вообще subscribe у redux есть, если очень хочется


          1. i360u
            06.02.2019 22:31
            +2

            Определить в одном месте функцию-обработчик — это, по вашему, следить самостоятельно? А Redux стало быть магическим образом значения привязывает и для этого ничего делать не надо? Ох.


          1. rgs350
            07.02.2019 00:07

            Вы видимо хорошо разбираетесь в Redux-e. У меня к вам есть небольшой вопрос:
            Представьте себе приложение со сложным интерфейсом и огромным количеством возможных действий пользователя, построенное на обсерверах, которое при каждом действии этого пользователя оповещает все компоненты на странице (даже те компоненты, которые текущее изменение состояния не затрагивает).
            Как бы вы оценили архитектуру такого приложения?


            1. serf
              07.02.2019 07:39

              Правильные компоненты просто не будут реагировать на неревантные сигналы просто сравнив текущие и новые данные по ссылки тк иммутабельный стор это позволяет сделать. Сравнив используя встроенное в мемоизированные селекторы сравнение или в явном виде используя distinctUntilChanged-like подход.

              PS не пишу именно о Redux, но в целом о подходе.


              1. faiwer
                07.02.2019 08:15

                Тут главная засада — не допустить через чур большого кол-ва connect-утых компонент. Т.к. mapStateToProps будут вызываться для всех из них всегда. Приходится включать голову и как-минимум группировать такие вещи.


                1. serf
                  07.02.2019 08:17

                  В реакте вроде принято коннектить только контейнер и в нем уже делать «роутинг» данных на компоненты нижнего уровня через атрибуты/свойства?


                  1. faiwer
                    07.02.2019 08:20

                    В двух словах примерно так и есть. Просто часто возникает соблазн на уровнях ниже тоже понаклепать своих собственных контейнеров. Далеко не всегда это приемлемый путь. А проброс всего чего надо в достаточно глубоком дереве — штука тоже… неудобная. Частично может выручить context. В любом случае, когда мы имеем достаточно сложный UI с сотнями и тысячами всяких элементов, тогда нужно хорошо продумывать эти вот сочленения со store-ом. Иначе оно высоко и далеко не полетит.


                1. v1vendi Автор
                  07.02.2019 11:46

                  Где-то года 2 назад в репозитории react-redux ребятки писали, что не нужно стесняться использовать connect. Раньше и правда рекомендовалось использовать несколько «контейнеров», подключенных черех connect, и пробрасывать дочерние пропсы через React компоненты. Сейчас создатели заявляют, что connect достаточно производителен, чтобы использовать его (без фанатизма) в больших количествах


                  1. faiwer
                    07.02.2019 13:42
                    +1

                    Узким местом является асимптотика такого решения. То что могло отнимать O(1) с тяжёлой константой (observer), отнимает O(n) с лёгкой константой (immutable + shallow comparions). Вопрос в N (число коннектов). Сами shallowComparison то быстрые. Но это не отменяет того, что:


                    • когда 99% времени библиотека считает что-нибудь за-зря, мы как минимум съедаем аккумуляторы конечных устройств
                    • всегда есть очень медленные девайсы, и узкие случаи, которыми мы пренебрегли, не заметили и т.д., которые могут выродиться во что-то ну совсем несуразное
                    • не всегда это можно быстро и легко исправить.

                    Т.е. я бы всё же стеснялся использовать connect всякий раз когда вижу какие-нибудь вещи в циклах и предполагаю там 50+ элементов. По правде я "стесняюсь" и при куда меньших масштабах. Но это уже моя паранойя )


    1. jakobz
      07.02.2019 13:39
      +1

      Потому что в юношеской, нонконформистской, среде фронта, библиотеки выбирают сердцем, а не головой. А Ден Абрамов — он же рок-звезда.

      Если аккуратно перенести всю концепцию Elm Architecture на JS, а не ее треть, и аккуратно продумать как удобно положить на JS — была бы хорошая, хотя и довольно нишевая, вещь. Но Денис написал 7 строк кода, и побежал пиарить это на весь мир. А нам теперь каждому первому джуну объяснять почему мы не пишем на Redux-е.


  1. Hypuk
    06.02.2019 16:34

    Есть хороший видео курс на egghead от автора redux — egghead.io/courses/getting-started-with-redux
    Пара уроков там посвящено тому как реализовать свой redux


  1. Keyten
    06.02.2019 20:14

    Точно так же изучал в своё время redux — просто прочитав исходники и написав свою совместимую версию на ts github.com/keyten/redux-ts/blob/master/index.ts

    Только вот жаль, это не очень прояснило обычные паттерны его использования.


    1. serf
      07.02.2019 08:13

      } catch(e){}
      Как-то такое не выглядит готовым для продакшена.

      Листенеры в таком виде не особенно полезны, было бы добно подписаться на изменение определенного участка стора. Можно сделать гибкую штуку на BehaviorSubject из RxJS и кода тоже будет мало не считая конечно саму библиотеку RxJS.

      О том что возможности TypeScript совсем не используются писать не буду в деталях, тема широкая. Но в целом использование нетипизированных Object и Function нивелирует практически все преимущества TS, лучше тогда уже просто JS взять.


      1. v1vendi Автор
        07.02.2019 11:48
        +1

        Вы не поверите, но именно это написано в коде Redux :)

        try {
            isDispatching = true
            currentState = currentReducer(currentState, action)
        } finally {
            isDispatching = false
        }


        1. serf
          07.02.2019 11:51

          Дикость какая-то. А как понять что в ридьюсере возникла ошибка, в явном виде ставить try/catch в самих ридьюсерах?


        1. justserega
          07.02.2019 12:34
          +2

          > Вы не поверите, но именно это написано в коде Redux
          Написано не это, тут catch вообще нет — следовательно и перехвата ошибки нет. Только гарантированное действие в finally


          1. serf
            07.02.2019 13:54

            И я проглядел как-то. Раз они ошибки не скрывают тогда все нормально.


  1. anfield343
    07.02.2019 14:32

    Вот прочитал статью, прочитал комменты и не понимаю куда движется современный фронт! Точнее наоборот, понимаю, в какую-то з… дницу!!! Как-то приходилось даже пробовать изучать Redux-Saga, от этого кошмара до сих пор отойти не могу.


    1. justserega
      08.02.2019 09:23
      +1

      Это было время бурного развития фронтенда, время поиска нового. Библиотек и подходов было огромное количество, redux просто завирусился. Как сказал Jacobs молодежь выбирает сердцем, а не головой habr.com/ru/post/439104/#comment_19724582

      Сейчас уже можно выбрать более адекватные инструменты. Мне лично очень нравится vue (и vuex — если нужен централизованный стейт). Он очень похож на react+mobx. Мне кажется, таким и должен был быть react изначально.