Идиоматичный Redux: Дао Redux’а, Часть 1 — Реализация и Замысел

в 18:41, , рубрики: Dan Abramov, immutability, javascript, mvc, open source, ReactJS, redux, Разработка веб-сайтов, функциональное программирование

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

Введение

Я потратил много времени, обсуждая онлайн паттерны использования Redux, была ли это помощь тем, кто изучает Redux в Reactiflux каналах, дискуссии о возможных изменениях в API библиотеки Redux на Github'е, или обсуждение различных аспектов Redux'а в комментариях к тредам на Reddit'е или HN (HackerNews). С течением времени, я выработал свое собственное мнение о том, что представляет собой хороший, идиоматичный Redux код, и я хотел бы поделиться некоторыми из этих мыслей. Несмотря на мой статус мейнтейнера Redux'а, это всего лишь мнения, но я предпочитаю думать, что они являются достаточно хорошими подходами :)

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

Несмотря на эту простоту, или, возможно, вследствие ее, существует широкий спектр походов, мнений и взглядов о том, как использовать Redux. Многие из этих подходов широко расходятся с концепциями и примерами из документации.

В то же время, продолжаются жалобы на то, как Redux «заставляет» вас делать вещи определенными способами. Многие из этих жалоб на самом деле включают концепции связанные с тем, как Redux обычно используется, а не фактическими ограничениями наложенными самой библиотекой Redux. (Например, только в одном недавнем HN треде я видел жалобы: «слишком много шаблонного кода», «константы action'ов и action creator'ы не нужны», «я вынужден редактировать слишком много файлов чтобы добавить одну фичу», «почему я должен переключаться между файлами чтобы добраться до своей логики?», «термины и названия слишком сложны для изучения или запутанны», и слишком много других.)

По мере того, как я исследовал, читал, обсуждал и изучал разнообразие способов использования Redux'а и идей, разделяемых в сообществе, я пришел к выводу, что важно различать то, как Redux на самом деле работает, задуманные способы его концептуального использования, и почти бесконечное количество способов возможного использования Redux. Я хотел бы затронуть несколько аспектов использования Redux и обсудить, как они вписываются в эти категории. В целом, я надеюсь объяснить, почему существуют конкретные паттерны и практики использования Redux, философию и замысел Redux'а, и то, что я считаю «идиоматичным» и «неидиоматичным» использованием Redux.

Этот пост будет разделен на две части. В «Часть 1 — Реализация и Замысел» мы рассмотрим фактическую реализацию Redux, какие конкретные ограничения он накладывает, и почему эти ограничения существуют. Затем, мы рассмотрим первоначальный замысел и проектные цели для Redux, основываясь на обсуждениях и заявлениях авторов (особенно на ранней стадии процесса разработки).

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

Изложение основ

Изучение Трех принципов

Начнем со взгляда на теперь известные Три Принципа Redux'а

  • Единый источник истины: Состояние всего вашего приложения хранится в дереве объектов внутри единственного хранилища.
  • Состояние доступно только для чтения: Единственный способ изменить состояние — выпустить action — объект, описывающий что произошло.
  • Изменения выполняются чистыми функциями: Чтобы указать как дерево состояния трансформируется action'ами, вы пишите чистые reducer'ы.

В самом прямом смысле, каждое из этих заявлений — ложь! (или, заимствуя классическую реплику из «Возвращение джежая» — «они верны… с определенной точки зрения.»)

  • «Единый источник истины» — неверно, потому что (по FAQ Redux'а) вам не нужно помещатьвсе в Redux, состояние хранилища не обязано быть объектом, и вам даже не обязательно иметь единственный store.
  • «Состояние доступно только для чтения» — неверно, так как на самом деле ничто не мешает остальной части приложения модифицировать текущее дерево состояния.
  • И «Изменения выполняются чистыми функциями» неверно, потому что функции-reducer'ы могут также мутировать дерево состояния напрямую или запускать другие побочные эффекты.

Но если эти заявления не полностью правдивы, зачем вообще они нужны? Эти принципы не фиксированные правила или буквальные заявления о реализации Redux'а. Скорее они формируют заявление о замысле того, как Redux следует использовать.

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

«Язык» или «Мета-язык»

В своей речи на ReactConf 2017 «Приручение Мета-языка» Ченг Лу описывает, что только исходный код является «языком», а все остальное, наподобие комментариев, тестов, документации, туториалов, блог постов, и конференций, является «мета-языком». Другими словами, исходный код сам по себе может передать только определенную часть информации. Много дополнительных слоев передачи информации на уровне человека требуется, чтобы люди понимали «язык».

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

«Язык» (в этом случае основная библиотека Redux) имеет минимальную экспрессивность, и следовательно концепции, нормы и идеи, окружающие Redux, все находятся на уровне «мета-языка». (Фактически, пост Понимание «Приручения Мета-языка», который раскладывает по полочкам идеи из выступления Ченга Лу, называет Redux конкретным примером этих идей.) В конечном счете, это означает, что понимание того, почему определенные практики существуют вокруг Redux'а, и решения о том, что является и не является «идиоматичным» будут включать мнения и обсуждения, а не просто определение, основанное на исходном коде.

Как Redux работает на самом деле

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

Ядро Redux: createStore

Функция createStore центральная часть функциональности Redux'а. Если мы отсечем комментарии, проверку ошибок, и код для пары продвинутых возможностей, таких как store enhancers (усилители хранилища — функции, расширяющие возможности store — примечание переводчика) и observables, вот как выглядит createStore (пример кода позаимствован из «построй-мини-Redux» туториала под названием «Взламывая Redux»):

function createStore(reducer) {
    var state;
    var listeners = []

    function getState() {
        return state
    }
    
    function subscribe(listener) {
        listeners.push(listener)
        return unsubscribe() {
            var index = listeners.indexOf(listener)
            listeners.splice(index, 1)
        }
    }
    
    function dispatch(action) {
        state = reducer(state, action)
        listeners.forEach(listener => listener())
    }

    dispatch({})

    return { dispatch, subscribe, getState }
}

Это примерно 25 строк кода, но все же они включают ключевую функциональность. Код отслеживает текущее значение состояния и множество подписчиков, обновляет значение и уведомляет подписчиков когда action диспатчится, и предоставляет API для store.

Взгляните на все те вещи которые этот фрагмент не включает:

  • Иммутабельность
  • «Чистые функции»
  • Middleware (промежуточный программный слой между «отправкой» action и reducer'ами — примечание переводчика)
  • Нормализация
  • Селекторы
  • Thunks
  • Sagas (саги)
  • Должны ли типы action'ов быть строками или Символами, и должны ли обозначаться константами или быть инлайновыми
  • Должны ли вы использовать action creator'ы (функции, создающие action'ы — примечание переводчика) для констуирования этих action'ов
  • Должен ли store содержать несериализуемые элементы, такие как промисы или инстансы классов
  • Должны ли данные хранится нормализованными или иерархичными
  • Где должна жить асинхронная логика

В этом ключе стоит процитировать pull-request Дэна Абрамова для примера «классический счетчик»:

Новый пример «классический счетчик» направлен на то, чтобы развеять миф о том, что Redux требует Webpack, React, горячую перезагрузку, саги, action creator'ы, константы, Babel, npm, CSS модули, декораторы, отличное знание латыни, подписки на Egghead, научную степень, или степень С.О.В. Нет, это всего лишь HTML, некоторые кустарные скрипт-тэги и старые добрые манипуляции с DOM. Наслаждайтесь!

Функция dispatch внутри createStore просто вызывает функцию-reducer и сохраняет любое возвращаемое значение. И все же, несмотря на это, элементы в том списке идей широко расцениваются, как концепции, о которых должно заботиться хорошее приложение на Redux.

Изложив все те вещи до которых createStore нет дела, важно отметить, что на самом деле эта функция требует. Настоящая функция createStore навязывает два конкретных ограничения: action'ы, которые доходят до store, обязаны быть простыми объектами, и action'ы обязаны иметь поле «type» не равное undefined.

Оба этих ограничения происходят от оригинальной концепции «Flux архитектуры». Цитируя секцию Flux Action'ы и Диспатчер из документации Flux:

Когда новые данные попадают в систему, как через человека, взаимодействующего с приложением, так и через web api вызов, эти данные упаковываются в action — объект, содержащий новые поля данных и конкретный action тип. Мы зачастую создаем библиотеку вспомогательных методов называемых ActionCreators которые не только создают объект action, но и передают action диспатчеру. Различные действия идентифицируются аттрибутом «type». Когда все store получают action, они обычно используют этот аттрибут для определения, следует ли им реагировать на него и каким образом. В приложении Flux, stor'ы и view контролируют сами себя; на них не воздействуют внешние объекты. Action'ы поступают в stor'ы через функции обратного вызова которые они определяют и регистрируют, а не методами установки (сеттерами).

Изначально Redux не требовал специальное поле «type», но позже была добавлена проверка валидности, чтобы помочь отловить возможные опечатки или неправильный импорт констант action'ов, и для избежания бесполезных споров о базовой структуре объектов.

Встроенная утилита: combineReducers

Здесь мы начинаем наблюдать некоторые ограничения, знакомые большему количеству людей. combineReducers ожидает, что каждый reducer среза, переданный в него, будет «корректно» реагировать на неизвестный action, возвращая свое состояние по-умолчанию и никогда не вернет undefined. Она также ожидает, что значением текущего состояния является простой JS объект, и что имеется точное соответствие между ключами в объекте текущего состояния и в объекте функции-reducer'а. И наконец, combineReducers выполняет проверку на равенство по ссылке, для определения все ли срез-reducer'ы вернули свое предыдущее значение. Если все вернувшиеся значения выглядят как предыдущие значения, combineReducers полагает, что ничего нигде не изменилось и, в качестве оптимизации, возвращает исходный корневой объект состояния.

Изначальное преимущество: инструменты разработчика Redux (DevTools)

Инструменты разработчика Redux состоят из двух основных частей: enhancer'а (усилителя) для store который реализует перемещение во времени путем отслеживания диспатченных action'ов, и пользовательского интерфейса, позволяющего просматривать и управлять историей. Сам по себе store enhancer не заботится о содержимом action'ов или состояния, он просто хранит action'ы в памяти. Изначально интерфейс инструментов разработчика нужно было рендерить внутри дерева компонентов вашего приложения, и он также не заботился о содержимом action'ов или состояния. Тем не менее, расширение Redux DevTools работает в отдельном процессе (по крайней мере в Chrome), и, следовательно, требует сериализуемости всех action'ов и состояния, для того, чтобы все возможности перемещения во времени работали корректно и быстро. Возможность импорта и экспорта состояния и action'ов также требует чтобы они были сериализуемыми.

Другое полу-требование для отладки с помощью перемещения во времени — иммутабельность и чистые функции. Если функция-reducer мутирует состояние, тогда переход между acton'ами в отладчике приведет к неконсистентным значениям. Если у reducer'а есть побочные эффекты, тогда эти побочные эффекты будут проявляться каждый раз когда DevTools повторяет action. В обоих случаях, отладка путем перемещения во времени не будет работать полностью как ожидается.

Основные связки с UI: React-Redux и connect

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

В частности, каждый раз, когда action диспатчится и подписчики уведомляются, connect проверяет, изменился ли корневой объект состояния. Если нет, connect предполагает что ничего в состоянии не изменилось, и пропускает дальнейшую работу по рендерингу (Вот почему combineReducers пытается, по мере возможности, вернуть тот же самый корневой объект состояния). Если же корневой объект состояния изменился, connect вызовет предоставленную функцию mapStateToProps, и выполнит неглубокую проверку на равенство между текущим результатом и предыдущим, для выявления изменились ли props, рассчитанные от данных store. Опять же, если содержимое данных выглядит одинаково, connect не будет ререндерить обернутый компонент. Эти проверки на равенство в connect'е являются причиной того, почему случайные мутации состояния не приводят к ререндерингу компонентов, это из-за того, что connect предполагает, что данные не изменились и ререндеринг не нужен.

Сопутствующие библиотеки: React и Reselect

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

Также, хотя React'овский компонент может реализовать shouldComponentUpdate, используя любую логику какую захочет, самая распространенная реализация полагается на неглубокие проверки равенства текущих props и новых входящих props, например:

return !shallowEqual(this.props, nextProps)

В любом случае, мутация данных обычно приводит к нежелательному поведению. Запоминающие функции-селекторы не вернут правильные значения, и оптимизированные компоненты React'а не будут ререндерится когда должны.

Подводя итог техническим требованиям Redux

Центральная функция Redux'а createStore сама по себе накладывает только два ограничения на то, как вы должны писать свой код: action'ы должны быть простыми объектами, и должны содержать определенный type. Ее не заботит иммутабельность, сериализуемость, побочные эффекты или какое на самом деле принимает значение поле type.

С учетом вышесказанного, широко используемые части вокруг этого ядра, включая Redux DevTools, React-Redux, React и Reselect, действительно полагаются на правильное использование иммутабельности, сериализуемости action'ов/состояния и чистых функций-reducer'ов. Основная логика приложения может работать нормально если эти ожидания проигнорированы, но, с большой долей вероятности, отладка перемещением во времени и ререндеринг компонентов сломаются. Они также повлияют и на любые другие случаи использования, связанные с постоянством.

Важно отметить, что иммутабельность, сериализуемость и чистые функции никаким образом не навязываются Redux'ом. Функция-reducer вполне может мутировать свое состояние или выполнять AJAX-вызов. Любая другая часть приложения вполне может вызывать getState() и модифицировать содержимое дерева состояния напрямую. Полностью возможно помещать промисы, функции, Символы, инстансы класса или другие не сериализуемые значения в action'ы или дерево состояний. Вам не следует делать ничего из этого, но это возможно.

Замысел и дизайн Redux

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

Влияние на Redux и его цели

Раздел «Введение» в документации Redux содержит несколько основных идей, повлиявших на разработку и концепции Redux, в темах Мотивация, Ключевые концепции и Предшественники. В качестве краткого итога:

  • Основной целью Redux'а является предсказуемость мутаций состояния, путем наложения ограничений на то, как и когда изменения могут происходить. Redux заимствует идею «Архитектуры Flux» о разделении логики изменения от остальной части приложения, и использовании простых объектов «action'ов» для описания изменений, которые должны произойти.
  • Возможности для разработчиков, такие как отладка перемещением во времени, являются одними из ключевых вариантов использования Redux. Следовательно, такие ограничения как иммутабельность и сериализуемость существуют в основном для того, чтобы подобные возможности были доступны при разработке, а также для упрощения отслеживания потока данных и логики изменения.
  • Redux хочет чтобы реальная логика обновления состояния была синхронной, а асинхронное поведение было отделено от обновления состояния.
  • Архитектура Flux предлагает несколько индивидуальных «stores» (хранилищ) для различных типов данных. Redux объединяет несколько таких «stor'ов» в единое дерево состояния для облегчения работы с отладкой, сохранением состояния и возможностями наподобие отменить/повторить.
  • Единственная корневая функция-reducer может сама состоять из множества меньших функций-reducer'ов. Это позволяет в явном виде контролировать как обрабатываются данные, в том числе порядок зависимостей, когда обновление одного среза состояния требует предварительного вычисления другого среза, в отличие от механизмов наподобие Flux'овского эмиттера событий store.waitFor(), выстраивающего цепочки зависимостей.

Также стоит взглянуть на заявленные проектные цели в ранней версии README Redux'a.

Философия и проектные цели

  • Вам не нужна книга по функциональному программированию для того, чтобы использовать Redux.
  • Всё (Stores, Action Creator'ы, конфигурация) способно на «горячую» перезагрузку.
  • Сохраняет преимущества Flux'а, но добавляет другие полезные свойства благодаря своей функциональной природе.
  • Предотвращает некоторые анти-паттерны распространенные в коде Flux.
  • Отлично работает в изоморфных приложениях, потому что не использует синглтоны, и данные могут быть «увлажнены».
  • Не имеет значения как вы храните ваши данные: вы можете использовать JS объекты, массивы, ImmutableJS и т.д.
  • Под капотом, Redux держит все ваши данные в виде дерева, но вам не нужно об этом думать.
  • Позволяет эффективно подписываться на меньшие обновления, чем обновления индивидуальных Stor'ов.
  • Предоставляет зацепки для реализации мощных инструментов разработчика (например, переходы по времени, запись/проигрывание) без обязательной их установки
  • Предоставляет точки расширения, чтобы можно было легко поддержать промисы или генерировать константы вне ядра Redux.
  • Никаких оберточных вызовов в ваших stor'ах и action'ах. Ваши вещи — это ваши вещи.
  • Невероятно просто тестировать в изоляции без моков (mocks).
  • Можно использовать «плоские» Stor'ы, или композировать и переиспользовать Stor'ы также как композируются компоненты.
  • Поверхность API минимальна.
  • Я уже упоминал «горячую» перезагрузку?

Проектные принципы и подход

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

Redux был создан как реализация архитектуры Flux

Redux изначально задумывался «всего лишь» как еще одна библиотека реализующая архитектуру Flux. В результате она уследовала многие коцепции от Flux: идею «отправки (dispatching) action'ов», то что action'ы — это простые объекты с полем type, использование «функций создания» action'ов (action creators), то, что «логика обновления» должна быть отделена от остальной части приложения и централизована, и многое другое.

Я часто вижу вопросы «Почему Redux делает ТАК», и на многие из подобных вопросов ответ: «Потому что Архитектура Flux'а и конкретные библиотеки Flux делали ТАК».

Поддерживаемость обновления состояния является главным приоритетом

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

Это значит, что разработчик должен иметь возможность посмотреть отправленный action, увидеть какие изменения состояния получились в результате, и вернуться к местам в кодовой базе, где этот action был диспатчен (особенно в зависимости от его типа). Если в store неверные данные, должно быть возможно отследить, какой диспатченный action привел к неправильному состоянию, и отмотать оттуда назад.

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

История action'ов должна иметь семантическое значение

Хотя ядро Redux'а не заботит какие конкретные значения содержатся в поле type ваших action'ов, довольно очевидно, что типы action'ов должны нести некоторый смысл и информацию. Redux DevTools и другие утилиты логирования отображают поле type для каждого диспатченного action'а, так что важно иметь значения, понятные с беглого взгляда.

Это значит, что строки полезнее Символов или чисел, с точки зрения передачи информации. Это также означает, что формулировка action типа должна быть ясной и понятной. Как правило, это означает, что наличие различных action типов будет понятнее разработчику, чем наличие только одного или двух action типов. Если во всей кодовой базе используется только один action тип (например, SET_DATA), то будет сложнее отслеживать откуда конкретный action был диспатчен, а жрунал истории действий будет менее читабельным.

Redux задуман для введения в принцины функционального программирования

Redux явно замыслен для использования с концепциями функционального программирования, и для того, чтобы помочь представить эти концепции как новым, так и опытным разработчикам. Эти концепции включают основы ФП, такие как: иммутабельность и чистые функции, а также идеи, наподобие композиции функций для решения большей задачи.

В то же время, Redux призван помочь обеспечить реальную ценность для разработчиков, пытающихся решать проблемы и создавать приложения, не подавляя при этом большим количеством абстрактных концепций ФП, и не увязая в спорах о терминах ФП, таких как «монады» или «эндофункторы». (Нужно признать, количество терминов и концепций вокруг Redux'а выросло с течением времени, и многие из них путают новичков, но цели: использования преимуществ ФП и введение в ФП для начинающих — явно были частью оригинального дизайна и философии.)

Redux поощряят тестируемый код

Наличие reducer'ов в качестве чистых функций позволяет выполнять отладку с переходом во времени, и также означает, что функция-reducer может легко быть протестирована в изоляции. Тестирование reducer'а должно требовать только его вызова с конкретными аргументами и проверки вывода — нет необходимости создавать моки для таких вещей как AJAX вызовы.

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

Функции-reducer'ы следует организовывать по срезу состояния

Redux берет концепт индивидуальных «хранилищ» из архитектуры Flux и объединяет их в единственный store. Самым прямолинейным соответствием между Flux и Redux является создание отдельного ключа верхнего уровня или «среза» в дереве состояний для каждого Flux хранилища. Если Flux приложение имеет раздельные UsersStore, PostsStore и CommentsStore, эквивалент в Redux будет иметь корневое дерево состояний, выглядящее так: { users, posts, comments }.

Можно ограничиться единственной функцией, содержащей всю логику по обновлению всех срезов состояния, но любое осмысленное приложение захочет разбить такую функцию на более мелкие функции для облегчения сопровождения. Самым очевидным способом это сделать является раздление логики в зависимости от того, какой срез состояния должен быть обновлен. Это значит, что каждый «reducer среза» должен заботиться только о своем срезе состояния, и, насколько ему известно, этот срез может быть всем состоянием. Этот паттерн «композиции reducer'ов» можно многократно повторяться для обработки обновлений иерархической структуры состояния. И утилита combineReducers включена в состав Redux'а специально для упрощения использования этого паттерна.

Если каждую функцию-reducer среза можно вызывать отдельно и предоставлять ей только собственный срез состояния в качестве параметра, то это означает, что с одним и тем же action'ом можно вызывать несколько reducer'ов среза, и каждый из них может обновлять свой срез состояния независимо от других. Основываясь на заявлениях Дэна и Эндрю, возможность одного action'а привести к обновлениям нескольких reducer'ов среза, является ключевой особенностью Redux'а. Про это часто говорят: «action'ы имеют отношение 1-ко-многим с reducer'ами.»

Логика обновления и поток данных выражены явно

Redux не содержит никакой «магии». Некоторые аспекты его реализации (такие как applyMiddleware и store enhancers) чуть сложнее понять сразу, если вы не знакомы с более продвинутыми принципами ФП, но в остальном все должно быть явным, ясным и отслеживаемым с минимальным количеством абстракций.

Redux на самом деле даже не реализует саму логику обновления состояния. Он просто полагается на любую корневую функцию-reducer, которую вы предоставите. Он имеет утилиту combineReducers, чтобы помочь в распространненом случае — независимом управлении состояниями reducer'ами среза. Но вас полностью поощряют на написание собственной логики reducer'а для удовлетворения ваших потребностей. Также это означает, что ваша логика reducer'а может быть простой или сложной, абстрактной или многословной — все зависит от того, как вы хотите ее написать.

В оригинальном диспатчере Flux'а, Stor'ам нужно было событие waitFor(), которое могло быть использовано для задания цепочек зависимостей. Если CommentsStore нуждался в данных от PostsStore для того, чтобы правильно обновить себя, он мог вызвать PostsStore.waitFor(), чтобы гарантировать, что он выполнится после того как PostsStore обновится. К сожалению, такую цепочку зависимостей было нелегко визуализировать. Тем не менее, с Redux такая последовательность операций может быть достигнута явным вызовом конкретных функций-reducer'ов в нужной последовательности.

Вот в качестве примера некоторые (немного модифицированные) цитаты и сниппеты из Дэновского гиста «Combining Stateless Stores»

В этом случае, commentsReducer не зависит полностью от состояния и action'а. Он также зависит и от hasCommentReallyBeenAdded (былЛиКомментарийДействительноДобавлен). Мы добавляем этот параметр к его API. Да, его теперь нельзя использовать «как есть», но в этом весь смысл: reducer теперь имеет явную зависимость от других данных. Он не store верхнего уровня. То, что управляет им, обязано каким-то образом предоставить ему эти данные.

export default function commentsReducer(state = initialState, action, hasPostReallyBeenAdded) {}

// в другом месте
export default function rootReducer(state = initialState, action) {
  const postState = postsReducer(state.post, action);
  const {hasPostReallyBeenAdded} = postState;
  const commentState  = commentsReducer(state.comments, action, hasPostReallyBeenAdded);
  return { post : postState, comments : commentState };
}

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

API Redux'а должно быть минимальным

Эта цель повторялась снова и снова Дэном и Эндрю на протяжении разработки Redux'а. Проще всего процитировать некоторые из их комментариев:

Andrew — #195:

Зачастую лучший API — это отсутствие API. Текущие предложения для middleware и stor'ов высшего порядка обладают огромным преимуществом в том, что они не требуют особого отношения со стороны ядра Redux — они просто обертки вокруг dispatch() и createStore() соответственно. Вы даже можете использовать их сегодня, до релиза 1.0. Это огромная победа для расширяемости и быстрых инноваций. Мы должны поддерживать паттерны и соглашения вместо жестких, привилегированных API.

Dan — #216:

Вот почему я решил написать Redux вместо использования NuclearJS:

  • Я не хочу жесткую зависимость от ImmutableJS
  • Я хочу настолько малый API насколько возможно
  • Я хочу сделать так, чтобы можно было легко спрыгнуть с Redux когда появиться что-то получше

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

Я изо всех сил старался избегать API наподобие createStore, потому что они привязывают вас к конкретной реализации. Вместо этого, для каждой сущности (Reducer, Action Creator) я попытался найти минимальный способ взаимодействия с ней без какой-либо зависимости от Redux. Единственный код, импортирующий Redux и действительно жестко зависимый от него, будет в вашем корневом компоненте и компонентах, подписанных на него.

Redux должен быть расширяем настолько, насколько возможно

Это связано с целью «минимального API». Некоторые библиотеки Flux, такие как Flummox Эндрю, имели некую форму асинхронного поведения, встроенную непосредственно в библиотеку (например, START/SUCCESS/FAILURE action'ы для промисов). И хотя наличие чего-то встроенного в ядро означало, что оно всегда доступно, это также ограничивало гибкость.

Снова проще всего процитировать комментарии из обсуждений и Hashnode AMA (вопросы/ответы) с Дэном и Эндрю:

Andrew — #55:

Для продолжения поддержки асинхронных action'ов и для предоставления точки расширения для внешних плагинов и инструментов, мы можем предоставить некий общий action middleware, вспомогательную функцию для композиции middlewar'ов, и документацию для авторов расширений о том, как легко создавать свои собственные.

Andrew — #215:

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

Как однажды сказал Дэн (не помню где… возможно в Slack) мы нацелены быть Koа для Flux библиотек. В конечном счете, когда сообщество повзрослеет, мы планируем поддерживать коллекцию «благословленных» плагинов и расширений, возможно в рамках reduxjs организации на Github'е.

Dan — Hashnode AMA:

Мы не хотели прописывать что-то подобное в самом Redux'е, потому что знаем, что многие люди не готовы изучать Rx операторы для выполнения базовых асинхронных операций. Они полезны, когда у вас сложная асинхронная логика, но мы не хотели вынуждать каждого пользователя Redux изучать Rx, так что мы намеренно поддержали гибкость middlewar'ов.

Andrew — Hashnode AMA:

Причина, по которой middleware API вообще существует, в том, что мы совершенно не хотели останавливаться на конкретном решении для асинхронности. Моя предыдущая библиотека Flux — Flummox — имела, по сути, встроенный промис-middleware. Для кого-то это было удобно, но из-за того, что она была встроенной, вы не могли изменить ее поведение или отказаться от нее. С Redux мы знали, что сообщество придумает множество лучших асинхронных решений, чем то, что мы бы могли сделать самостоятельно. Redux Thunk рекламируется в документации потому, что это абсолютно минимальное решение. Мы были уверены, что сообщество придумает нечто другое и/или лучшее. Мы были правы!

Заключительные мысли

Я потратил много времени на исследование для этих двух постов. Было невероятно интересно прочитать ранние дискуссии и комментарии, и увидеть как Redux эволюционировал в то, что мы знаем теперь. Как видно из процитированного ранее README, видение Redux'а было ясным с самого начала, и было несколько конкретных идей и концептуальных скачков, которые привели к окончательному API и реализации. Надеюсь, этот взгляд на внутренности и историю Redux поможет пролить свет на то, как Redux работает на самом деле, и почему он был построен таким образом.

Обязательно ознакомьтесь с Дао Redux'а, Часть 2 — Практика и Философия, где мы взглянем на то, почему существует множество паттернов использования Redux, и я поделюсь своими мыслями о плюсах и минусах многих «вариаций» того, как можно использовать Redux.


Источник: blog.isquaredsoftware.com/2017/05/idiomatic-redux-tao-of-redux-part-1

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

Автор: Макс Давыдов

Источник

Поделиться

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