image

Или Охота на Кракена. В предыдущих заметках (тут и тут) я делился своим Braindump на тему различных архитектурных стилей, в частности Model-View-Controller и Flux.

Я отметил, что не увидел в лице Flux какой-то революции, этот шаблон не что-то новое. Я увидел в нем схожесть с Reenskaug-MVC 1979 года. Также, я упомянул, что решил убрать из своего кода Redux (одна из реализаций Flux). Мне кажется, эти моменты необходимо пояснить более развернуто. Моей целью не было убедить читателя в том, что Flux надо называть MVC, так же я не хотел сказать, что redux-модуль плох и от него нужно полностью отказаться.

Так как же относится тогда к Flux?


Для начала надо определится что же такое Flux. Во-первых это определенно архитектурный стиль, при чем на на данный момент, уже не только для клиентских web-приложений. Во-вторых это набор четко определенных компонентов и терминов.

Реализаций этой концепции уже с дюжину, это только самые популярные: Facebook/Flux, Redux, Fluxxor, Reflux, Vuejs/Vuex, Mobx. Есть экзотика в виде SAM.js (State-Action-Model).

Но важно то, что термины Диспетчер (Dispatcher), Действие (Action), Хранилище (Store) и Состояние (State) плотно заняли место в современных архитектурах клиентских приложений. Подобно тому, как раньше разработчик, пришедший в новую систему, искал слой Контроллера, теперь он ищет Хранилище или Диспетчер.

Так как же относиться к этому? Относиться определенно хорошо. Для меня Flux это SoC (Separation of Concerns) второго уровня, разделение ответственности в рамках конкретного MVC компонента. Это не новая идея, это то, что пытались сделать в Taligent-MVP, когда слой Контроллер был переосмыслен и разделен на несколько других слоев.

Это не замена, это следующий виток, это эволюция архитектурных шаблонов.

image

Из заметки “Все новое это хорошо забытое старое”

image

Из презентации И.Панина “Why the domain first?”

Мотивация


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

Однако, как я упомянул ранее, каждая концепция или шаблон начинаются с проблемы для которой они были придуманы. Facebook это крупная компания, это web-приложение колоссальных масштабов, объем интерактивности на высочайшем уровне. Именно для этого и был придуман Flux. Но и именно поэтому эта концепция была так сложно воспринята сообществом. Сколько компаний разрабатывают подобное ПО?

Задумайтесь, в скольких приложениях, которые вы разрабатывали, действительно необходимо несколько хранилищ? При разработке скольких ваших приложений и вправду помог бы этот шаблон?

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

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

В. М. Турский (из книги Ф.Брукса “Мифический человеко-месяц”)

Я видел группы разработчиков, которые теряли головы от разрекламированных стандартов и забывали о своей главной задаче: реализовать интересы клиента

Р. Мартин

Redux полюбился сообществу. Именно эта реализация — это то, что нужно было среднему приложению. Когда я пересел на этот стек технологий, то первые приложения писались, можно сказать, стандартно. Использовалась связка React-Redux-Thunk(Middleware). Были и Создатели Действий (Action Creators), и строковые константы для типов Действий (Actions), были и так называемые Связанные Создатели Действий (Bound Action Creators). Все по гайду официального сайта.

Но в определенный момент стал вопрос, а как же эта пара магических модулей работает под капотом. Для разработчика вроде меня, который имел дело с кодовой базой Documentum Foundation Classes, читал исходники ExtJS 3.1, интегрированного в Webtop, разобраться с парой-тройкой функций redux-модуля не составит труда. Как и для большинства других опытных программистов. Но ни для кого не секрет, что на проектах работают разработчики разных уровней. Как добиться того, чтобы даже самые юные смогли разобраться, что происходит под этим самым “капотом”? Ведь программист должен знать свой инструмент.

Я столкнулся с тем, что не все разработчики, окружающие меня, понимали или хотя бы просто задумывались, как же работает эта магия, каким образом кролик появляется из шляпы. Это пожалуй был первый пункт, который послужил катализатором к тому, чтобы написать свою реализацию Flux/Redux. В конце концов лучший способ разобраться в чем-либо, переосмыслить что-либо, это самому написать это. Код будет доступен всей команде, каждый сможет что-то привнести или убрать, пройтись отладчиком и понять назначение любой строчки.

Пройдемся по остальным мотивирующим пунктам. Концепция Unidirectional Data Flow должна быть чистой. Однонаправленный поток данных это то, что позволит убить монстра со щупальцами. И, наверное, самый неопределенный момент при этом подходе — это асинхронность JavaScript и то, как она ложится на концепцию однонаправленного потока данных.

Есть несколько вариантов, которые люди выбирают, чтобы решить этот момент. Один из примеров это Thunk. Лично для меня, middleware (промежуточные вычисления) выглядит как некий побочный эффект в рамках архитектурного стиля Flux. Попытка заткнуть дырку, которая не позволяет использовать шаблон в чистом виде. Это как классы Utils, Helper или Service в ООП. Когда разработчик не может вынести логику в какой-то конкретный бизнес-класс, то у него появляется это волшебное место, куда он начинает складывать всю эту побочную (полезную?) функциональность. На самом деле этот побочный эффект, это ни что иное, как особенность нашего Хранилища. Хранилище персистентно, и это то, что мешает нам думать о данных через призму односложных CRUD-операций.

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

Thunk и bindActionCreators — это побочное явление для меня. Я выбираю подход при котором второй аргумент функции connect, а именно mapDispatchToProps, мне не понадобится. Кстати, кто-нибудь когда-нибудь использовал в своих проектах третий аргумент функции connect? Может таинственный флаг pure?

Второй пункт который послужил катализатором это избыточность используемых инструментов.

К слову, одна из любимых фраз Никлауса Вирта:

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

Третий пункт это “потому что могу”. Да-да, именно так, мы тоже программисты, мы тоже можем писать свои инструменты. Я не призываю переписать под себя, к примеру, React или Spring-фреймворк. Но и тащить весь Spring в проект, которому это не нужно, не имеет смысла. Мне, к примеру, абсолютно не лень потратить один субботний вечер на свою маленькую реализацию Flux, которая будет удовлетворять потребностям моих приложений. Мне это даже в удовольствие.

Велосипед или, пускай у него будет имя, Artux


image

Redux написан в функциональном стиле, в вопросе парадигм программирования я за золотую середину, JavaScript удивительный язык, который открывает нам мультипарадигменный занавес. Я за то самое равновесие темной и светлой стороны.

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

Ниже как раз интерфейс моей реализации Хранилища. Он очень похож на вариант Redux, за исключением того, что в публичных методах появилась функция unsubscribe. В redux-модуле она замкнута в функции subscribe.

image

Конструктор Хранилища принимает на вход карту Моделей (Models) с соответствующими этим моделям Обработчиками Действий (Reducers/Receivers).

Модель это логическое именованное подразделение Хранилища, которое соответствует определенной бизнес-сущности, части домена. В моем представлении Хранилище содержит только бизнес информацию, никаких состояний графических виджетов, типа флажков или переключателей.

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

Ниже интерфейс класса StoreProvider (Провайдер Хранилища), он почти идентичен оригиналу и просто ложит экземпляр Хранилища в контекст приложения.

image

Далее описание интерфейса компонента высшего порядка (Higher-order Component/HoC), который порождает функция subscribeToStore, замена функции connect.

image

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

И последний компонент моей скромной реализации. Интерфейс Диспетчера.

image

Основное назначение компонента — это отсылка сообщений Хранилищу. Интерфейс предоставляет метод позволяющий создавать объект-действие, а также методы по диспетчеризации действия, которые “хоронят” в себе асинхронность персистентных операций. Обращу внимание читателя, что обещание не выбрасывается наружу, любые реакции в системе происходят только на почве изменения состояния определенной модели.

Действие представляет из себя JSON-объект, содержащий следующие поля:

  • Тип (Type) — строковый литерал, наименование действия пользователя;
  • Полезная Информация (Payload) — это может быть скалярное значение или структура данных;
  • Мета или Побочная Информация (Meta) — через это свойство можно пробросить дополнительные данные, например входные аргументы, необходимые для выполнения операции сопоставимой действию;
  • Индикатор Ошибки (Error) — булево значение, отражающее успех или провал операции сопоставимой действию;

Также, с использованием es6-объектов Proxy и Reflect, был добавлен механизм логирования в консоль изменения состояния для более удобной отладки.

image

Устройство приложения


На верхнем уровне представления находятся Страницы (Pages). Это, можно сказать, компоненты-контейнеры, которые являются родителем для всего дерева графических виджетов.

Страницы это как раз те самые HoC-компоненты, которые подписываются на изменение состояния Хранилища, пробрасывая поток данных вниз по дереву. Также, каждый такой объект имеет собственный слой Взаимодействия (Interactors). Этот слой сочетает в себе два типа операций: переходы между Страницами (Pages) или Якорями (Anchors) и инициализация выполнения Действий (Actions / Commands).

Класс взаимодействия это, по сути, мост, позволяющий объединить в одном месте коммуникацию с Диспетчером и Маршрутизатором (Router). Ниже пример интерфейса такого класса.

image

Каждой модели в системе соответствует ее Обработчик (Reducer/Receiver), именно он на основе полученного действия меняет или не меняет состояние модели.

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

Мой вариант обработчика — это Редукторы (Reducers). Меня всегда раздражали тонны констант, которые приходилось писать для вилки типов действий (PENDING, SUCCESS, FAIL). Во-первых я решил остановиться на литералах прямо в обработчиках. Во-вторых, я считаю, что объект-действие представляет из себя очень гибкий инструмент, который мог бы нести в себе всю необходимую информацию. В-третьих, лично мне, как-то сложно воспринимать действие пользователя, которое называется GET_ITEMS FAIL. Согласитесь, это не то действие, которое конечный пользователь хотел выполнить, он выполнял GET_ITEMS, и именно это действие и должно доходить до модели в хранилище.

image

image

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

image

#DMP_END


Как и любая схема, эта архитектура может тоже превратиться в осьминога со щупальцами. Самое “толстое” место в этом варианте — это представление. Но тут нам призван помочь сам React, библиотека сосредоточенная именно на реализации слоя представления. Также существует ряд техник, к примеру, компонентное наследование (HoC and OOP Inherits). Задача примерно проста — не допустить рождение нового чудовища — FSUV (Fat Stupid Ugly View).

Я должен уточнить, что при описании, я ссылался на модуль redux версии 3.6.0. Буду честен, я особо не выиграл в производительности, рендеринг страниц происходит за тоже время. Но если вдруг этот модуль и вовсе пропадет из npm, то как вы уже поняли, сильно грустить я не буду.

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

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

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

Ссылки


+ Flux Standard Actions
+ Redux
+ Flux
+ I.Panin “Why the domain first?”
Поделиться с друзьями
-->

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


  1. Alex_ME
    07.05.2017 01:05
    +1

    Оффтоп:
    бщая концепция Flux кажется довольно привлекательной, но, попробовав react-redux, я остался недоволен — куча boilerplate-кода, вызов событий действий через switch по строкам??? Нет, это явно не лучший вариант, имхо


    1. boolive
      07.05.2017 03:09

      Лучший — это свой )


    1. marshinov
      07.05.2017 14:29

      Мне тоже очень не понравился boilerplate в switch'ах. Смог избавить с помощью фабрик редюсеров.

      const reducerFactory = (moduleRef, initialState, next = null, method = GET) => {
        if (!moduleRef) {
          throw Error('You must provide valid module ref')
        }
      
        if (!initialState) {
          throw Error('You must provide valid initialState')
        }
      
        return (state = initialState, action) => {
          if (!action.type.startsWith(moduleRef))return state
      
          if (action.type === combinePath(moduleRef, GET)) {
            return {...state, params: {...action.params}, [IS_FETCHING]: true}
          }
      
          if (action.type === combinePath(moduleRef, GET + SUCCEEDED)) {
            return {...state, ...action.payload, [IS_FETCHING]: false, [IS_INITIALIZED]: true}
          }
      
          if (action.type === combinePath(moduleRef, GET + FAILED)) {
            return {...state, ...action.payload, [IS_FETCHING]: false, [IS_INITIALIZED]: true}
          }
      
          if (action.type === combinePath(moduleRef, 'SetState')) {
            return action.newState
          }
      
      
          return typeof(next) == 'function' ? next(state, action) : state;
        }
      }
      


      1. staticlab
        07.05.2017 16:08
        +3

        Присмотритесь: вы заменили switch на цепочку if-ов. Более красивое решение предлагает, например, https://github.com/acdlite/redux-actions:


        const increment = createAction('INCREMENT');
        const decrement = createAction('DECREMENT');
        
        const reducer = handleActions({
          [increment]: (state, action) => ({
            counter: state.counter + action.payload
          }),
        
          [decrement]: (state, action) => ({
            counter: state.counter - action.payload
          })
        });


        1. boolive
          07.05.2017 23:11

          А такое решение как вам?


          // utils.js
          export default function createReducer(initState, handlers){
              return (state = initState, action = {}) => {
                  if (handlers[action.type]){
                      return handlers[action.type](state, action);
                  }
                  return state;
              };
          }

          // xx/actions.js
          export const types = {
              LOAD: Symbol('LOAD'),
              SOME_ACTION: Symbol('SOME_ACTION'),
          };
          export default {
          
              load: () => {
                  return {
                      type: types.LOAD, 
                      data: []
                  }
              },
          
              someAction: () => {
                  return {
                      type: types.SOME_ACTION
                  }
              }
          };

          // xx/reducer.js
          import {createReducer} from '../../utils';
          import {types} from './actions';
          
          export default createReducer(initState, {
          
              [types.LOAD]: (state, action) => {
                  return {
                      ...state,
                      list: action.data
                  };
              },
          
              [types.SOME_ACTION]: (state, action) => {
                return {...state };
              }
          }


          1. staticlab
            08.05.2017 03:02

            Да практически то же самое, только в DevTools придётся типы экшенов как Symbol(LOAD), Symbol(SOME_ACTION) видеть.


            1. raveclassic
              08.05.2017 04:17
              +2

              Символы в качестве экшен-тайпов лучше не использовать, так как они не сериализуются. (ну, если это вам нужно, конечно)


          1. raveclassic
            08.05.2017 04:26
            +2

            Что только люди не придумают, лишь бы не писать switch/case


        1. marshinov
          08.05.2017 10:50

          Моя проблема не в swtich (он меня полностью устраивает), а в boilerplate в редюсерах.


  1. Envek
    07.05.2017 10:25
    +4

    Обращу внимание читателя, что обещание не выбрасывается наружу

    Вот тут не соглашусь. Я нашёл для себя очень удобным, когда вызов action'а возвращает Promise, и я через .then в компоненте меняю локальный state компонента вещами, которые относятся только к компоненту (например ставлю флажок inProgress по которому блокирую кнопку, которую нажал пользователь). Потому что считаю неприемлимым загрязнять глобальный store сотнями булевых флажков, задача каждого из которых — заблокировать всего одну кнопку. А кнопок много, очень.


    1. Akuma
      07.05.2017 11:28
      +1

      Поддержу. Тоже так делаю и это очень удобно.

      А флажки в глобальном store нужны только когда этот isProgress может влиять на несколько независимых компонентов.


      1. marshinov
        07.05.2017 14:31
        +1

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


        1. mayorovp
          07.05.2017 15:09
          -1

          Только аккуратнее с такими фичами — иногда сброс состояния является ожидаемым поведением :-)


          Например, все в том же примере с нажатой кнопкой.


    1. faiwer
      07.05.2017 11:33
      -1

      Если я правильно понял о чём речь, то ещё возврат Promise в actionCreator-е удобен в тестах.


    1. archik
      07.05.2017 15:24

      Если вы реагируете на асинхронные действия и в then и в редукторах, тогда это уже не однонаправленный поток данных (Unidirectional Data Flow). Это уже что-то другое, будет два вектора направления данных.

      Про «флажки» я полностью с вами солидарен и я упомянул об этом выше в статье:

      «В моем представлении Хранилище содержит только бизнес информацию, никаких состояний графических виджетов, типа флажков или переключателей.»


      Значения подобных флажков вычисляется в компоненте, методах жизненного цикла, на основе полученного состояния (сравнивая prevProps и nextProps).


      1. raveclassic
        08.05.2017 04:28
        +1

        Более того, если на процесс загрузки данных компонента остальное приложение никак не реагирует, то не нужно выносить его (этого процесса) результаты в стор. Это все вместе прекрасно уживается в контейнере — и статус процесса (тот самый флаг), и его результат (ошибка или данные).


    1. Odrin
      10.05.2017 10:38

      Проходили такое. Удобно только до тех пор, пока во время выполнения запроса компонент не может быть unmounted (например, переходом пользователя на другой экран приложения), а потом начинаются ошибки:

      setState(...): Can only update a mounted or mounting component


      1. TheShock
        10.05.2017 15:14
        +1

        Отменять запрос на onmount. Вообще это неплохая практика в любом подходе — если данные вдруг перестали быть нужны — не загружать их


        1. Odrin
          10.05.2017 15:30

          Если данные перестали быть нужны в данный момент, это не значит, что данные не могут понадобится позднее — тут уже все зависит от конкретной ситуации.
          В любом случае, отменять запрос в componentWillUnmount — это дополнительный код, в котором может быть ошибка и про который можно забыть. И как уже отмечали выше, изменение состояния компоненты через .then — нарушение принципа однонаправленного data flow.


          1. TheShock
            10.05.2017 16:13
            +2

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

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

            1. Ну вот когда они понадобятся, тогда их и загрузите
            2. Ведь все-равно грузить будете — вы ведь не пишете код, что если данные уже загружены, то их не надо грузить
            3. А если пишете, то как инвалидируете кеш? Как знаете, что загруженные когда-то данные — актуальные?
            4. А если так волнуетесь, что данные могут понадобится позднее — почему бы заранее не грузить все данные которые могут понадобится позднее? Почему вы больший приоритет отдаёте тем данным, которые пользователь решил не загружать, а не каким-либо другим?
            5. И вообще — могут понадобится, а могут и не понадобится, так зачем грузить, если можно не грузить?
            6. Из-за глючного запроса, который по какой-то причине сейчас висит и который точно не нужен сейчас пользователю вы хотите занять место в канале и лимите ajax-запросов и тем самым оттянуть загрузку страницы, которая пользователю действительно нужна?

            В любом случае, отменять запрос в componentWillUnmount — это дополнительный код, в котором может быть ошибка и про который можно забыть

            Так может необходимо иметь хорошую библиотеку, которая это делает сама, и в которой код протестирован? Тогда нет ни дополнительного кода, ни ошибки)

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

            И как уже отмечали выше, изменение состояния компоненты через .then — нарушение принципа однонаправленного data flow.

            Да, я сперва тоже так подумал и хотел было отнести это к очередному минусу Редакс, но потом понял, что это просто заблуждение, потому написал так:

            Использовать setState — крайне не-редакс вей. То есть, по сути, не вынес значение флажка в глобальный стор — нарушил философию редакса

            Аргументирую. Подписка на then — нарушение принципов Редакса, и даже, возможно, Флакса, но не принципа однонаправленного флоу, т.к. флоу вполне однонаправленный, хоть и идет по малому кругу. Вот он:

            1. Вы пишете в стейт (это наше хранилище)
            2. Вы рендерите данные на базе данных стейта (реакт сам посылает событие „Change“ при их изменении)
            3. При нажатии на кнопку мы создаем действие
            4. Результатами действия становится внесение изменений в хранилище (а не внесение изменений в готовый рендер, как происходит в двонаправленном флоу).

            Видите? [1 ? 2 ? 3 ? 4 ? 1]
            Просто поклонение Редаксу затуманивает разум и мешает мыслить критически, как результат — все, что не соответствует Редаксу — не комильфо.


            1. Envek
              11.05.2017 11:17
              +1

              Опять соглашусь.
              В моём случае результат promise'а обрабатывается И в редюсерах (через dispatch в action'е) И в then в компоненте. Редюсеры меняют store ответом от сервера, then в компоненте меняет внутренний флажок, который разблокирует кнопку. Поток данных остаётся однонаправленным, но разветвляется по дороге.
              Компонент при этом по максимуму остаётся переиспользуемым чёрным ящиком: его не надо завязывать ни на какой флажок в store вдобавок к action'у, который он должен дёрнуть (т.е. в props ему передаём только одну вещь — action). Своё внутреннее состояние он прячет в себе.


  1. dolphin4ik
    07.05.2017 10:36
    -5

    Хорошая идея! Но сейчас на Хабре шапками закидают, так как вы «ноунейм».


  1. marshinov
    07.05.2017 14:41

    Скажите, а вы рассматриваете проект только в качестве лучшего понимания внутреннего устройства Redux или еще в каком-то виде?

    Ваша архитектура идеально ложится на redux:

    1. ваши action'ы полностью аналогичны action'ам Redux. Константы можно не использовать
    2. dispatcher = передать функцию store.dispatch во все «законекченные компоненты» и подкючить thunk
    3. router = подключить react router
    4. HoC = компоненты/контейнеры — паттерн реакта
    5. unsubscribe реализуется через replaceReducer

    Т.е. все, что вы хотите реализуется на существующем стеке. Не лучше ли инвестировать время на то, чтобы хорошо настроить существующие инструменты, чем создавать новые?


    1. archik
      07.05.2017 14:52

      Реализации взаимозаменяемы. Но если вы сделали такое умозаключение, то не прониклись текстом или же я не смог донести мысль. Три мотивирующих фактора я указал в заметке.


      1. staticlab
        07.05.2017 16:02
        +1

        Первый пункт — написать redux самостоятельно, чтобы разобраться, как он работает. Второй пункт — убрать избыточность, но что-то непонятно, где именно вы её убрали. Больше похоже, что вы просто заменили функциональный подход на объектный. Третий пункт — NIH.


    1. TheShock
      08.05.2017 00:47

      Не лучше ли инвестировать время на то, чтобы хорошо настроить существующие инструменты, чем создавать новые?

      Редакс — просто не очень удачная реализация MVC. Так почему бы не пользоваться нормальными подходами вместо слабых подделок?


      1. staticlab
        08.05.2017 01:47
        +1

        Redux — это самый обыкновенный state machine. Мы посылаем сообщение машине состояний, и она это состояние меняет. Всё! Остальное — это маркетинговая шелуха из Фейсбука, пришедшая из их Флакса.


      1. raveclassic
        08.05.2017 04:30

        Странно, у меня складывалось впечатление, что вы понимаете, о чем, собственно, редакс.


        1. TheShock
          08.05.2017 04:59

          Это ж ирония была, в ответ на довольно смешной комментарий из разряда «зачем использовать автомобили, если есть такие удобные костыли. Вот смотрите, я на костыль одеваю резиновый набалдашник и он у меня совсем как настоящие колеса».

          Ну вот к примеру почему вы аналогично про понимание не отреагировали на это?

          Ваша архитектура идеально ложится на redux:

          3. router = подключить react router

          Потому и сказал, что Редакс — поделка в стиле MVC. Но я понимаю, что буквально — это не так.


          1. raveclassic
            08.05.2017 05:09

            А, теперь понятно :)

            Ну, тут долго можно рассуждать на тему состыковки идеологий. К моему величайшему сожалению, мало кто может внятно описать понимание redux. Увы и ах. Отсюда и все эти thunk'и, promise'ы, react router'ы (честно говоря, вот это — вообще капец), и прочее. Видимо, действительно, не хватает дельной литературы, разжевывающей что к чему.


            1. TheShock
              08.05.2017 05:54
              +3

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

              Но вот как написать универсальную библиотеку для редакса? Тот же аккордеон. Да, можно хранить все значения в стейте, но тогда зачем редакс? Я вот посмотрел на redux-accordion — и это тихий ужас. Иерархия просто отсутствует. Вот у нас есть какой-либо список и вместо того, чтобы данные аккордеона лежали там, рядом со списком — есть отдельное дерево для всех аккордеонов нашего приложения. Я знаю, что свидетели прихода святого Редакса скажут, что это круто.

              И как добавить в эту деревянную структуру новые изменения? Вот если мне необходим статус «disabled» для листьев аккордеона? Я рядом создаю новое дерево с теми же ключами в виде «uniqId», где значения будут массив дизейбленых значений.

              То есть вместо более логичной структуры

              music: {
               data: { ... },
               accordionStatus: {
                 0: { disabled: true, opened: false },
                 1: { disabled: false, opened: true }
               },
              }
              videos: {
                data: { ... },
                Accordion: {
                  0: { disabled: false, opened: false },
                  1: { disabled: false, opened: true }
                }
              }
              


              Я получаю типичный говнокод в нашем стейте:

              data: {
               music: {...}
               video: {...}
              }
              accordion: {
                music: { 0: false, 1: true },
                videos: { 0: true, 1: false } 
              },
              accordionDisables: {
                music: { 0: true, 1: false },
                videos: { 0: false, 1: false } 
              },
              


              Что за дикое говнище? А потом на это все еще подписываться нужно.

              А я уж молчу, что для accordion и accordionDisables нам необходимо приблизительно одинаковые редюсеры, но вменяемого способа для реюза подобного кода в редаксе нету — придется

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

              Looking a few years back, one of my biggest mistakes was my obsession with not repeating myself.
              DRY is not free. You pay with a deeper abstraction stack and a higher price for changing individual cases later. I love boring code now.
              Good boilerplate code makes modules disposable.


              Я это все к чему
              Видимо, действительно, не хватает дельной литературы, разжевывающей что к чему.

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


              1. marshinov
                08.05.2017 10:56
                +2

                Ну на самом деле там и правда есть темные пятна на этом редаксе. Вот даже из того, что тут обсуждалось. Использовать setState — крайне не-редакс вей.
                

                Разработчик redux с вами не согласен. Вообще, Абрамов во всех комментариях очень дельно пишет о cargo cult в JS. Все пытаются найти серебряную пуля и всегда использовать только ее. Разработка так не работает. Есть задачи, есть инструменты. В комментах выше уже написали, что для всевозможных флажков (эфемерное состояние) вполне себе подходит использование setState.


                1. TheShock
                  08.05.2017 11:16

                  А если открытость-закрытость должна сохраняться на сервере и получатся с сервера?


                  1. marshinov
                    08.05.2017 11:19

                    Тогда я храню ее в redux store и получаю с сервера. Разделение же очень простое — если после повторного рендера компонента на эту часть состояния наплевать — храним ее в локальном стейте компонента. Если нужно сохранять/получать из вне — храним в redux store.


                    1. TheShock
                      08.05.2017 11:23

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

                      Ну и все-равно никуда не делись те проблемы, которые я описал в большом комментарии.

                      И кстати, на счет «Разработчик redux с вами не согласен». Все туда же:

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


                      1. staticlab
                        08.05.2017 12:16

                        https://github.com/reactjs/redux/issues/1385#issuecomment-187727168


                        My biggest pet peeve is it is harder to compose, reuse, nest, and generally move around container components because there are two independent hierarchies at the same time (views and reducers). It is also not entirely clear how to write reusable components that either use Redux as implementation detail or want to provide Redux-friendly interface. (There are different approaches.) I’m also not impressed about every action having to go “all the way” upwards instead of short-circuiting somewhere. In other words, I would like to see something like React local state model but backed by reducers, and I would like it to be highly practical and tuned to real use cases rather than a beautiful abstraction.


                      1. marshinov
                        08.05.2017 13:03

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

                        Ну так и нормально. Одно состояние персистентно, другое — нет. Для них разные хранилища. Все логично же, не?


              1. Envek
                08.05.2017 11:39
                +2

                О, как вы хорошо описали мои ощущения от redux'а :-)


                Всё пихать в глобальный store хорошо, пока приложение маленькое. А когда оно очень большое и долгоживущее (подразумевается, что открыто у пользователя весь рабочий день и активно используется), то уже надо писать специальные action'ы и логику в reducer'ах, единственная цель которых — очищать куски глобального стора, когда пользователь уходит со страницы/раздела/АРМ/придумайте-своё. Со state'ом компонента проще — как только он пропал из DOM, то все его данные пропали. Как снова появится в DOM — загрузим заново.


                1. vtvz_ru
                  09.05.2017 02:29
                  +1

                  Конечно, redux не имеет смысла использовать всегда и везде. Но что касается меня, по началу я пытался сделать все внутри самих компонентов, как это сделано в туториале реакта. Получилась просто ужасная каша вложенных друг в друга компонентов, которые передают друг другу обработчики и состояние. Когда дело дошло до того, чтобы отправить данные на сервер, мне пришлось решать эту проблему немного извращено: создавать скрытые инпуты, а потом сериализовать форму с помощью jQuery. Плохо, но на тот момент работало. Узнал про redux, изучил, нашел redux-actions, переписал все под него. Компоненты стали значительно проще, код более элегантным, читаемость намного выше. Остальные проблемы решились сами собой

                  Не вижу смысла писать все, как говорит redux. Если компонент используется в одном месте, коннекчу его прямо на месте. Если вдруг он понадобился ещё где-то — отделяю в отдельный файл. Проблем с переиспользованием редюсеров не было: если это набор компонентов, то в meta параметр можно запихать индекс или идентификатор объекта, а в map применить нужный редюсер к нужному элементу; или можно написать обёртку над экшеном для конкретного элемента для использования нескольких компонентов на странице.

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


                  1. Envek
                    11.05.2017 11:10
                    +1

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


              1. raveclassic
                08.05.2017 13:34
                +3

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

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

                Redux — это не готовое спасение от всех болезней, как активно продвигают авторы всех этих бесполезных путающих библиотек. Это идея, паттерн.


            1. marshinov
              08.05.2017 11:17

              Отсюда и все эти thunk'и, promise'ы, react router'ы (честно говоря, вот это — вообще капец), и прочее.

              1. Чем вам не нравится react router?
              2. Как без thunk'а или redux-saga (или аналогичного middleware) работать с асинхронностью и другими эффектами, если reducer'ы — это чистые функции?



              1. staticlab
                08.05.2017 11:56

                Лично мне не нравится react-router своей дубовостью: я не могу добавить в роут свои данные, если писать роуты объектами, то нельзя добавить ни индексы, ни редиректы.


                1. marshinov
                  08.05.2017 13:01

                  Свои данные можете добавить через замыкания. Объектами можно добавить индексы вот так:

                  route.indexRoute = {
                          component: component
                        }
                  


              1. raveclassic
                08.05.2017 13:30
                +1

                react-router (ну окей, версии до 4) достаточно плохо уживается с редаксом, так как тянет одеяло руления стейтом на себя — это раз. Далее, всеми правдами и неправдами поощряется складывать логику всевозможных редиректов и загрузку данных прямо в компонентах — например, любые вариации проверок на авторизацию — это чистой воды бизнес-логика, и мне хотелось бы видеть ее в подобающем месте, а не где-то внутри каких-то там guard'ов посередине дерева компонентов роутера или его конфига (о боги).

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


                1. marshinov
                  08.05.2017 14:18

                  Ну да сага почище чуток. Но в итоге то один фиг — в майдлвейр попадает action / thunk, который приведет к запросу на сервер. Если контейнеры и компоненты разделены, то для целевого презентационного компонента вызов будет выглядеть одинаково this.props.fetch({...}. И вот тут уже возникает вопрос как обработать континюейшн по завершению fetch'а. Сага дает возможность написать takeEvery / takeLatest. Я себе для загрузки данных написал вот такую штуку:

                  const query = (moduleRef, url, params = undefined) => {
                      if(typeof (url) == 'object'){
                        params = url
                        url = DATA
                      }
                  
                      dispatch({
                        type: combinePath(moduleRef, GET),
                        params
                      })
                  
                      return new Promise(resolve => {
                        dispatch(function () {
                          get(url, params).then(response => {
                            const error = 'ok' in response && !response.ok
                            const data = error
                              ? {ok: response.ok, status: response.status}
                              : response
                  
                            dispatch({
                              type: combinePath(moduleRef, GET + (error ? FAILED : SUCCEEDED)),
                              payload: data
                            })
                  
                            resolve(data)
                          })
                        })
                      })
                  

                  Сами компоненты не знают, что они вызывают и какие там континюейшны. Главное, что придет либо GetSucceeded либо GetFailed. На это уже реагируют редюсеры. Функцию можно чейнить, чтобы вызывать цепочку загрузок. Обошелся в итоге без саги и без ручных вызовов then. Понятно, что я обрабатываю только один сценарий асинхронности: загрузка данных с сервера по цепочке и падение при любой ошибке. Для моих задач пока подходит, нигде не уперся.

                  Да, в крайнем случае придется написать ручной then, но это все-равно проще, чем саги. Да и фиг его знает, когда генераторы будут поддерживаться всеми браузерами и не внесут ли изменений в стандарт.


                  1. raveclassic
                    08.05.2017 14:42
                    +1

                    Само собой, все должно применяться по мере надобности. Если все что нужно, это дернуть апи и выгрузить данные для рендеринга какого-нибудь чартика, то не нужно для этого тащить саги. С другой стороны, не надо пытаться в thunk'ах уместить ту же обработку протухания сессии и редиректа на логин. Это банально проще и удобнее делать в саге.

                    А по поводу генераторов, не так там все плохо с поддержкой. Рано или поздно сафари допинают (даже уже), а IE наконец подохнет. Пока что, почти аналогичные процессы (отлько более уродливые) можно делать на redux-observable.


      1. marshinov
        08.05.2017 10:59

        Покажите, где в подходе автора стало лучше, чем в redux и более MVC? Я вижу знакомую картину, вид в профиль.


        1. TheShock
          08.05.2017 11:21
          +1

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

          Меня лишь крайне огорчают заявления, что редакс — стандарт, на котором стоит останавливаться и не искать далее.


          1. marshinov
            08.05.2017 13:06
            +1

            Как вы предлагаете работать с состоянием в react? Выбор то не большой: redux и mobx.