Redux: Рекомендации по передовым методам работы с создателями действий, редюсерами и селекторами

Созданный на 22 дек. 2015  ·  106Комментарии  ·  Источник: reduxjs/redux

Моя команда уже пару месяцев использует Redux. Попутно я иногда обнаруживал, что думаю о функции и задаюсь вопросом: «Принадлежит ли она создателю действий или редюсеру?». Документация по этому поводу кажется немного расплывчатой. (Или, возможно, я просто пропустил, где это описано, и в этом случае я прошу прощения.) Но по мере того, как я писал больше кода и больше тестов, я пришел к более твердому мнению о том, где что-то _ должно_ быть, и я подумал, что это того стоит. делиться и обсуждать с другими.

Итак, вот мои мысли.

Используйте селекторы везде

Этот первый не имеет прямого отношения к Redux, но я все равно поделюсь им, поскольку он косвенно упоминается ниже. Моя команда использует Rackt / Reselect . Обычно мы определяем файл, который экспортирует селекторы для данного узла нашего дерева состояний (например, MyPageSelectors). Наши «умные» контейнеры затем используют эти селекторы для параметризации наших «глупых» компонентов.

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

Итак, моя первая рекомендация - использовать общие селекторы _everywhere_- даже при синхронном доступе к данным (например, предпочитать myValueSelector(state) state.myValue ). Это снижает вероятность ошибочного ввода переменных, которые приводят к незаметным неопределенным значениям, упрощает внесение изменений в структуру вашего магазина и т. Д.

Делайте _more_ в создателях действий и _less_ в редукторах

Я думаю, что это очень важно, хотя это может быть не сразу очевидно. Бизнес-логика принадлежит создателям действий. Редукторы должны быть тупыми и простыми. Во многих отдельных случаях это не имеет значения, но согласованность - это хорошо, поэтому лучше делать это последовательно. На это есть несколько причин:

  1. Создатели действий могут быть асинхронными за счет использования промежуточного программного обеспечения, такого как redux-thunk . Поскольку вашему приложению часто требуется асинхронное обновление вашего хранилища, некоторая «бизнес-логика» в конечном итоге будет присутствовать в ваших действиях.
  2. Создатели действий (точнее, преобразователи, которые они возвращают) могут использовать общие селекторы, потому что у них есть доступ к полному состоянию. Редукторы не могут, потому что у них есть доступ только к своему узлу.
  3. Используя redux-thunk , один создатель действия может отправлять несколько действий, что упрощает сложные обновления состояния и способствует лучшему повторному использованию кода.

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

  1. В редукторах. Каждый редуктор (добавление, редактирование, удаление) отвечает за обновление списка _, а также_ метаданных.
  2. В представлениях (контейнер / компонент). Каждое представление, вызывающее действие (добавление, редактирование, удаление), также отвечает за вызов действия updateMetadata . Этот подход ужасен по (надеюсь) очевидным причинам.
  3. В экшене-создателях. Каждый создатель действия (добавить, изменить, удалить) возвращает преобразователь, который отправляет действие для обновления списка, а затем другое действие для обновления метаданных.

С учетом вышеперечисленных вариантов вариант 3 значительно лучше. Оба варианта 1 и 3 поддерживают совместное использование чистого кода, но только вариант 3 поддерживает случай, когда обновления списка и / или метаданных могут быть асинхронными. (Например, возможно, он полагается на веб-исполнителя.)

Напишите «утиные» тесты, посвященные действиям и селекторам.

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

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

discussion

Самый полезный комментарий

@dtinth @ denis-sokolov В этом я тоже с вами согласен. Между прочим, когда я ссылался на проект redux-saga, я, возможно, не дал понять, что я против идеи того, чтобы actionCreators со временем становился все более и более сложным.

Проект Redux-saga также является попыткой сделать то, что вы описываете @dtinth, но есть

Возможно, вы можете взглянуть на этот момент исходного обсуждения, которое привело к обсуждению саги Redux: https://github.com/paldepind/functional-frontend-architecture/issues/20#issuecomment -162822909

Вариант использования для решения

Представьте, что у вас есть приложение Todo с очевидным событием TodoCreated. Затем мы просим вас написать код для адаптации приложения. Как только пользователь создает задачу, мы должны поздравить его всплывающим окном.

«Нечистый» путь:

Это то, что, кажется, предпочитает @bvaughn

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
       if ( getState().isOnboarding ) {
         dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
       }
   }
}

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

Способ «вычислить все по необработанным событиям»:

Это то, что, кажется, предпочитает @ denis- sokolov @dtinth :

function onboardingTodoCreateCongratulationReducer(state = defaultState, action) {
  var isOnboarding = isOnboardingReducer(state.isOnboarding,action);
  switch (action) {
    case "TodoCreated": 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: isOnboarding}
    default: 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: false}
  }
}

Да, вы можете создать редуктор, который знает, нужно ли отображать поздравление. Но тогда у вас есть всплывающее окно, которое будет отображаться даже без действия, говорящего о том, что всплывающее окно было отображено. По моему собственному опыту (и при этом все еще есть унаследованный код), всегда лучше сделать это очень явным: НИКОГДА не отображать всплывающее окно с поздравлением, если не было запущено действие DISPLAY_CONGRATULATION. Явный поддерживать намного проще, чем неявный.

Упрощенный способ саги.

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

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
   }
}

function onboardingSaga(state, action, actionCreators) {
  switch (action) {
    case "OnboardingStarted": 
        return {onboarding: true, ...state};
    case "OnboardingStarted": 
        return {onboarding: false, ...state};
    case "TodoCreated": 
        if ( state.onboarding ) dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
        return state;
    default: 
        return state;
  }
}

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

Немного усложняя правила:

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

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

Путь редукс-саги

С помощью redux-saga и приведенных выше правил онбординга вы должны написать что-то вроде

function* onboarding() {
  while ( true ) {
    take(ONBOARDING_STARTED)
    take(TODO_CREATED)
    put(SHOW_TODO_CREATION_CONGRATULATION)
    take(ONBOARDING_ENDED)
  }
}

Я думаю, что этот вариант использования решается намного проще, чем приведенные выше решения. Если я ошибаюсь, дайте мне более простую реализацию :)

Вы говорили о нечистом коде, и в этом случае в реализации Redux-saga нет примесей, потому что эффекты take / put на самом деле являются данными. Когда вызывается take (), он не выполняется, он возвращает дескриптор эффекта, который нужно выполнить, и в какой-то момент включается интерпретатор, поэтому вам не нужен макет для тестирования саг. Если вы функциональный разработчик, занимающийся Haskell, подумайте о монадах Free / IO.


В этом случае он позволяет:

  • Избегайте усложнения actionCreator и делайте его зависимым от getState
  • Сделайте неявное более явным
  • Избегайте привязки трансверсальной логики (например, описанной выше) к основной бизнес-области (создание задач).

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

Примеры:

  • "TIMELINE_SCROLLED_NEAR-BOTTOM" может привести к "NEXT_PAGE_LOADED"
  • «REQUEST_FAILED» может привести к «USER_DISCONNECTED», если код ошибки 401.
  • "HASHTAG_FILTER_ADDED" может привести к "CONTENT_RELOADED"

Если вы хотите добиться модульного макета приложения с утками, это может позволить избежать объединения уток вместе. Сага становится связующим звеном. Утки просто должны знать о своих сырых событиях, и сага интерпретирует эти сырые события. Это намного лучше, чем если бы duck1 напрямую отправлял действия duck2, потому что это упрощает повторное использование проекта duck1 в другом контексте. Однако можно утверждать, что точка сопряжения также может быть в actionCreators, и это то, что большинство людей делают сегодня.

Все 106 Комментарий

Любопытно, используете ли вы Immutable.js или другой. Я не мог себе представить, что из горстки редуксов, которые я построил, не использовал неизменяемый, но у меня _до_ есть довольно глубоко вложенная структура, которую Immutable помогает приручить.

Вот это да. Какая оплошность для меня _не_ упоминать об этом. Да! Мы используем Immutable! Как вы говорите, трудно представить, _не__ использовать его для чего-то существенного.

@bvaughn Одна область, с которой я

Честно говоря, мы еще не определили жесткую политику. Часто мы используем селекторы в наших «умных» контейнерах для дополнительных собственных значений из наших неизменяемых объектов, а затем передаем собственные значения нашим компонентам в виде строк, логических значений и т. Д. Иногда мы передаем неизменяемый объект, но когда мы это делаем - мы почти всегда передать тип Record чтобы компонент мог обрабатывать его как собственный объект (с геттерами).

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

1) Как вы тестируете своих создателей экшенов? Мне нравится переносить как можно больше логики на чистые синхронные функции, которые не зависят от внешних сервисов, потому что их легче тестировать.
2) Используете ли вы путешествия во времени с горячей перезарядкой? Одна из замечательных вещей с помощью react redux devtools заключается в том, что при настройке горячей перезагрузки хранилище перезапустит все действия с новым редуктором. Если бы я перенес свою логику в создателей действий, я бы ее потерял.
3) Если ваши создатели действий отправляются несколько раз, чтобы вызвать эффект, означает ли это, что ваше состояние на короткое время находится в недопустимом состоянии? (Я думаю здесь о нескольких синхронных диспетчерах, а не о тех, которые отправляются асинхронно позже)

Используйте селекторы везде

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

Используйте ImmutableJS

IMO с новым синтаксисом JS больше не так полезно использовать ImmutableJS, поскольку вы можете легко изменять списки и объекты с помощью обычного JS. Если у вас нет очень больших списков и объектов с большим количеством свойств и вам не требуется структурное совместное использование по соображениям производительности, ImmutableJS не является строгим требованием.

Делайте больше в действии

@bvaughn, вам действительно стоит взглянуть на этот проект: https://github.com/yelouafi/redux-saga
Когда я начал обсуждать саги (изначально концепция бэкенда ) в

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

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

2) Используете ли вы путешествия во времени с горячей перезарядкой? Одна из замечательных вещей с помощью react redux devtools заключается в том, что при настройке горячей перезагрузки хранилище перезапустит все действия с новым редуктором. Если бы я перенес свою логику в создателей действий, я бы ее потерял.

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

3) Если ваши создатели действий отправляются несколько раз, чтобы вызвать эффект, означает ли это, что ваше состояние на короткое время находится в недопустимом состоянии? (Я думаю здесь о нескольких синхронных диспетчерах, а не о тех, которые отправляются асинхронно позже)

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

IMO с новым синтаксисом JS больше не так полезно использовать ImmutableJS, поскольку вы можете легко изменять списки и объекты с помощью обычного JS. Если у вас нет очень больших списков и объектов с большим количеством свойств и вам не требуется структурное совместное использование по соображениям производительности, ImmutableJS не является строгим требованием.

Основные причины использования Immutable (на мой взгляд) - это не производительность или синтаксический сахар для обновлений. Основная причина в том, что он не позволяет вам (или кому-то еще) случайно_ изменить входящее состояние в редукторе. Это недопустимо, и, к сожалению, это легко сделать с простыми объектами JS.

@bvaughn, вам действительно стоит взглянуть на этот проект: https://github.com/yelouafi/redux-saga
Когда я начал обсуждать саги (изначально концепция бэкенда ) в

Я уже проверял этот проект раньше :) Хотя еще не использовал. Это действительно выглядит аккуратно.

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

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

var store = createStore();
store.dispatch(actions.startRequest());
store.dispatch(actions.requestResponseReceived({...});
strictEqual(isLoaded(store.getState());

Но как выглядит ваш тест? Что-то вроде этого?

var mock = mockFetch();
store.dispatch(actions.request());
mock.expect("/api/foo.bar").andRespond("{status: OK}");
strictEqual(isLoaded(store.getState());

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

Что делать, если код поменяют? Если я изменю редуктор, будут повторяться те же действия, но с новым редуктором. А если я изменю создателя действия, новые версии не будут воспроизводиться. Итак, рассмотрим два сценария:

С редуктором:

1) Я пробую действие в своем приложении.
2) В моем редукторе есть ошибка, приводящая к неправильному состоянию.
3) Исправляю ошибку в редукторе и сохраняю
4) Путешествие во времени загружает новый редуктор и переводит меня в то состояние, в котором я должен был быть.

В то время как с создателем действия

1) Я пробую действие в своем приложении.
2) В создателе действий есть ошибка, из-за которой создается неправильное действие
3) Исправляю ошибку в создателе действий и сохраняю
4) Я все еще нахожусь в неправильном состоянии, что потребует от меня, по крайней мере, повторной попытки действия и, возможно, обновления, если оно переведет меня в полностью неработающее состояние.

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

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

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

Что вы подразумеваете под переходным состоянием в Redux @bvaughn и @sompylasar ? Мы либо заканчиваем рассылку, либо бросаем. Если выкидывает, то состояние не меняется.

Если у вашего редуктора нет проблем с кодом, Redux имеет только состояния, согласующиеся с логикой редуктора. Каким-то образом все отправленные действия обрабатываются в транзакции: либо обновляется все дерево, либо состояние не меняется вообще.

Если все дерево обновляется, но не соответствующим образом (например, состояние, которое React не может отобразить), значит, вы не выполнили свою работу правильно :)

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

Однако я понимаю озабоченность @winstonewert, который, похоже, хочет синхронно отправить 2 действия в одной транзакции. Потому что иногда actionCreators отправляют несколько действий и ожидают, что все действия будут выполнены правильно. Если отправлено 2 действия, а затем второе не удалось, то будет применено только первое, что приведет к состоянию, которое мы можем считать «несогласованным». Возможно, @winstonewert хочет, чтобы в случае

@winstonewert Я реализовал что-то подобное в нашей внутренней структуре здесь, и до сих пор он отлично работает: https://github.com/stample/atom-react/blob/master/src/atom/atom.js
Я также хотел обрабатывать ошибки рендеринга: если состояние не может быть успешно отрисовано, я хотел, чтобы мое состояние было откатано, чтобы избежать блокировки пользовательского интерфейса. К сожалению, до следующего выпуска React будет очень плохо работать, когда методы рендеринга выдают ошибки, поэтому это было не так уж и полезно, но, возможно, в будущем.

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

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

@gaearon Мне интересно, планируете ли вы поддерживать такого рода функции и возможно ли это с текущим API.

Мне кажется, что redux-batched-subscribe не позволяет делать настоящие транзакции, а просто сокращает количество отрисовок. Я вижу, что хранилище "фиксируется" после каждой отправки, даже если прослушиватель подписки запускается только один раз в конце.

Зачем нужна полная поддержка транзакций? Я не думаю, что понимаю этот вариант использования.

@gaearon Я еще не совсем уверен, но был бы рад узнать больше о сценарии использования

Идея состоит в том, что вы могли бы сделать dispatch([a1,a2]) и если a2 завершится неудачно, мы откатимся к состоянию до того, как был отправлен a1.

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

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

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

Будет ли работать простой усилитель редуктора? например

const enhanceReducerWithTheAbilityToConsumeMultipleActions = (reducer =>
  (state, actions) => (typeof actions.reduce === 'function'
    ? actions.reduce(reducer, state)
    : reducer(state, actions)
  )
)

При этом вы можете отправить массив в магазин. Усилитель распаковывает индивидуальное действие и передает его каждому редуктору.

Да, и он существует: https://github.com/tshelburne/redux-batched-actions

Ох @gaearon, я этого не знал. Я не заметил, что есть 2 разных проекта, которые по-разному пытаются решить довольно похожий сценарий использования:

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

@gaearon Ой, плохо, что я на это не смотрю. : flhed:


Создатели действий представляют нечистый код

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

В Hacker Way: переосмысление разработки веб-приложений в Facebook, где представлен шаблон Flux, самой проблемой, которая приводит к созданию Flux, является императивный код.

В этом случае императивным кодом является Action Creator, который выполняет ввод-вывод.

Мы не используем Redux на работе, но там, где я работаю, у нас были мелкозернистые действия (которые, конечно, имеют смысл сами по себе) и запускали их в пакетном режиме. Например, при нажатии на сообщение запускаются три действия: OPEN_MESSAGE_VIEW , FETCH_MESSAGE , MARK_NOTIFICATION_AS_READ .

Затем оказывается, что эти «низкоуровневые» действия - не более чем «команда», «установщик» или «сообщение» для установки некоторого значения внутри хранилища. Мы могли бы также вернуться и использовать MVC и получить более простой код, если будем продолжать делать это так.

В некотором смысле Action Creators представляют нечистый код, а Reducers (и Selectors) представляют чистый код . Люди, работающие с Haskell, поняли, что лучше иметь менее нечистый код и более чистый код .

Например, в моем побочном проекте (использующем Redux) я использую API распознавания речи webkit. Когда вы говорите, он генерирует событие onresult . Есть два варианта - где эти события обрабатываются?

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

Я выбрал номер два: просто отправьте необработанный объект события в магазин.

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

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

Я хотел бы поддержать @dtinth. Действия должны представлять события, произошедшие в реальном мире, а не то, как мы хотим реагировать на эти события. В частности, см. CQRS: мы хотим регистрировать как можно больше деталей о реальных событиях, и, вероятно, редукторы будут улучшены в будущем и будут обрабатывать старые события с новой логикой.

@dtinth @ denis-sokolov В этом я тоже с вами согласен. Между прочим, когда я ссылался на проект redux-saga, я, возможно, не дал понять, что я против идеи того, чтобы actionCreators со временем становился все более и более сложным.

Проект Redux-saga также является попыткой сделать то, что вы описываете @dtinth, но есть

Возможно, вы можете взглянуть на этот момент исходного обсуждения, которое привело к обсуждению саги Redux: https://github.com/paldepind/functional-frontend-architecture/issues/20#issuecomment -162822909

Вариант использования для решения

Представьте, что у вас есть приложение Todo с очевидным событием TodoCreated. Затем мы просим вас написать код для адаптации приложения. Как только пользователь создает задачу, мы должны поздравить его всплывающим окном.

«Нечистый» путь:

Это то, что, кажется, предпочитает @bvaughn

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
       if ( getState().isOnboarding ) {
         dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
       }
   }
}

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

Способ «вычислить все по необработанным событиям»:

Это то, что, кажется, предпочитает @ denis- sokolov @dtinth :

function onboardingTodoCreateCongratulationReducer(state = defaultState, action) {
  var isOnboarding = isOnboardingReducer(state.isOnboarding,action);
  switch (action) {
    case "TodoCreated": 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: isOnboarding}
    default: 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: false}
  }
}

Да, вы можете создать редуктор, который знает, нужно ли отображать поздравление. Но тогда у вас есть всплывающее окно, которое будет отображаться даже без действия, говорящего о том, что всплывающее окно было отображено. По моему собственному опыту (и при этом все еще есть унаследованный код), всегда лучше сделать это очень явным: НИКОГДА не отображать всплывающее окно с поздравлением, если не было запущено действие DISPLAY_CONGRATULATION. Явный поддерживать намного проще, чем неявный.

Упрощенный способ саги.

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

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
   }
}

function onboardingSaga(state, action, actionCreators) {
  switch (action) {
    case "OnboardingStarted": 
        return {onboarding: true, ...state};
    case "OnboardingStarted": 
        return {onboarding: false, ...state};
    case "TodoCreated": 
        if ( state.onboarding ) dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
        return state;
    default: 
        return state;
  }
}

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

Немного усложняя правила:

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

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

Путь редукс-саги

С помощью redux-saga и приведенных выше правил онбординга вы должны написать что-то вроде

function* onboarding() {
  while ( true ) {
    take(ONBOARDING_STARTED)
    take(TODO_CREATED)
    put(SHOW_TODO_CREATION_CONGRATULATION)
    take(ONBOARDING_ENDED)
  }
}

Я думаю, что этот вариант использования решается намного проще, чем приведенные выше решения. Если я ошибаюсь, дайте мне более простую реализацию :)

Вы говорили о нечистом коде, и в этом случае в реализации Redux-saga нет примесей, потому что эффекты take / put на самом деле являются данными. Когда вызывается take (), он не выполняется, он возвращает дескриптор эффекта, который нужно выполнить, и в какой-то момент включается интерпретатор, поэтому вам не нужен макет для тестирования саг. Если вы функциональный разработчик, занимающийся Haskell, подумайте о монадах Free / IO.


В этом случае он позволяет:

  • Избегайте усложнения actionCreator и делайте его зависимым от getState
  • Сделайте неявное более явным
  • Избегайте привязки трансверсальной логики (например, описанной выше) к основной бизнес-области (создание задач).

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

Примеры:

  • "TIMELINE_SCROLLED_NEAR-BOTTOM" может привести к "NEXT_PAGE_LOADED"
  • «REQUEST_FAILED» может привести к «USER_DISCONNECTED», если код ошибки 401.
  • "HASHTAG_FILTER_ADDED" может привести к "CONTENT_RELOADED"

Если вы хотите добиться модульного макета приложения с утками, это может позволить избежать объединения уток вместе. Сага становится связующим звеном. Утки просто должны знать о своих сырых событиях, и сага интерпретирует эти сырые события. Это намного лучше, чем если бы duck1 напрямую отправлял действия duck2, потому что это упрощает повторное использование проекта duck1 в другом контексте. Однако можно утверждать, что точка сопряжения также может быть в actionCreators, и это то, что большинство людей делают сегодня.

@slorber Это отличный пример! Спасибо, что нашли время, чтобы четко объяснить преимущества и недостатки каждого подхода. (Я даже думаю, что это должно быть в документации.)

Я использовал подобную идею (которую я назвал «рабочие компоненты»). По сути, это компонент React, который ничего не отображает ( render: () => null ), но слушает события (например, из магазинов) и вызывает другие побочные эффекты. Затем этот рабочий компонент помещается в корневой компонент приложения. Еще один безумный способ справиться со сложными побочными эффектами. : stuck_out_tongue:

Пока я спал, здесь много дискуссий.

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

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


@winstonewert сказал: «Я полагаю, что мой
@slorber спросил: «Что вы подразумеваете под переходным состоянием в Redux @bvaughn и @sompylasar ? ошибку . Если выдает

Я почти уверен, что мы думаем о разных вещах. Когда я сказал «переходное недопустимое состояние», я имел в виду следующий вариант использования. Например, я и мой коллега недавно выпустили

Представьте, что ваш магазин приложений содержит несколько доступных для поиска объектов: [{id: 1, name: "Alex"}, {id: 2, name: "Brian"}, {id: 3, name: "Charles"}] . Пользователь ввел текст фильтра «e», поэтому промежуточное ПО поиска содержит массив идентификаторов 1 и 3. Теперь представьте, что пользователь 1 (Alex) удален - либо в ответ на действие пользователя локально, либо при обновлении удаленных данных, которые больше не содержит эту пользовательскую запись. В момент, когда ваш редуктор обновит коллекцию пользователей, ваш магазин будет временно недействителен, потому что redux-search будет ссылаться на идентификатор, который больше не существует в коллекции. Как только промежуточное ПО будет запущено снова, оно исправит недопустимое состояние. Подобные вещи могут произойти в любое время, когда один узел вашего дерева связан с другим узлом.


@slorber сказал: «Мне не нравится этот подход, потому что он делает создателя действия тесно связанным с макетом представления приложения. Он предполагает, что actionCreator должен знать структуру дерева состояний пользовательского интерфейса, чтобы принять свое решение».

Я не понимаю, что вы подразумеваете под подходом, связывающим создателя действия "с макетом представления приложения". Дерево состояний управляет (или информирует) пользовательский интерфейс. Это одна из основных целей Flux. И ваши создатели действий и редукторы по определению связаны с этим состоянием (но не с пользовательским интерфейсом).

Как бы то ни было, пример кода, который вы написали, как то, что я предпочитаю, - это не то, что я имел в виду. Может, я плохо себя объяснил. Я думаю, что трудность обсуждения чего-то подобного заключается в том, что это обычно не проявляется на простых или распространенных примерах. (Например, стандартное приложение TODO MVC недостаточно сложное для подобных обсуждений нюансов.)

Отредактировано для ясности по последнему пункту.

Кстати, @slorber, вот пример того, что я имел в виду. Это немного надумано.

Допустим, в вашем штате много узлов. Один из этих узлов хранит общие ресурсы. (Под «общими» я имею в виду ресурсы, которые кэшируются локально и к которым имеют доступ несколько страниц в вашем приложении.) Эти общие ресурсы имеют свои собственные создатели действий и редукторы («утки»). Другой узел хранит информацию для конкретной страницы приложения. У вашей страницы тоже есть своя утка.

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

import { fetchThing, thingSelector } from 'resources/thing/duck'
import { showError } from 'messages/duck'

export function fetchAndProcessThing ({ params }): Object {
  const { id } = params
  return async ({ dispatch, getState }) => {
    try {
      await dispatch(fetchThing({ id }))

      const thing = thingSelector(getState())

      dispatch({ type: 'PROCESS_THING', thing })
    } catch (err) {
      dispatch(showError(`Invalid thing id="${id}".`))
    }
  }
}

Возможно, @winstonewert хочет, чтобы в случае

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

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

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

OP, похоже, предпочитает создателей действий, которые отправляют меньшие действия, что допускает переходные недействительные состояния, которые мне не нравятся.

Не совсем. Я бы предпочел одиночные действия в отношении узла вашего создателя действия в дереве состояний. Но если одно концептуальное «действие» пользователя влияет на несколько узлов дерева состояний, вам необходимо выполнить несколько действий. Вы можете отдельно вызывать каждое действие (что, по-моему, _bad_), или вы можете иметь одного создателя действия, отправляющего действия (способ redux-thunk, который, я думаю, _ лучше_, потому что он скрывает эту информацию от вашего уровня представления).

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

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

@bvaughn Ой, извините за то, что я такой пурист!

Что ж, нечистый код связан с извлечением данных и другими побочными эффектами / вводом-выводом, тогда как чистый код не может вызвать никаких побочных эффектов. См. Эту таблицу для сравнения чистого и нечистого кода.

В лучших практиках Flux говорится, что действие должно «описывать действие пользователя, а не устанавливать параметры». Документы Flux также намекнули, откуда должны исходить эти действия:

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

По сути, действия - это факты / данные, которые описывают «что произошло», а не то, что должно произойти. Магазины могут реагировать на эти действия только синхронно, предсказуемо и без каких-либо побочных эффектов. Все остальные побочные эффекты следует обрабатывать в создателях действий (или сагах: wink :).

Пример

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

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

  • Пользователь нажимает кнопку просмотра табло.
  • Отображается табло с индикатором загрузки.
  • На сервер отправляется запрос на получение табло.
  • Ждите ответа.
  • В случае успеха отобразите табло.
  • В случае неудачи табло закрывается и появляется окно сообщения с сообщением об ошибке. Пользователь может закрыть его.
  • Пользователь может закрыть табло.

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

  • SCOREBOARD_VIEW (в результате нажатия пользователем кнопки просмотра таблицы результатов)
  • SCOREBOARD_FETCH_SUCCESS (в результате успешного ответа сервера)
  • SCOREBOARD_FETCH_FAILURE (в результате ответа сервера с ошибкой)
  • SCOREBOARD_CLOSE (в результате нажатия пользователем кнопки закрытия)
  • MESSAGE_BOX_CLOSE (в результате нажатия пользователем кнопки закрытия в окне сообщения)

Этих 5 действий достаточно для выполнения всех вышеперечисленных требований. Как видите, первые 4 действия не имеют ничего общего с «уткой». Каждое действие описывает только то, что произошло во внешнем мире (пользователь хочет это сделать, сервер сказал это) и может быть использовано любым редуктором. У нас также нет действия MESSAGE_BOX_OPEN , потому что это не «то, что произошло» (хотя это то, что должно произойти).

Единственный способ изменить дерево состояний - вызвать действие, объект, описывающий, что произошло. - README Redux

Их склеивают эти создатели действий:

function viewScoreboard () {
  return async function (dispatch) {
    dispatch({ type: 'SCOREBOARD_VIEW' })
    try {
      const result = fetchScoreboardFromServer()
      dispatch({ type: 'SCOREBOARD_FETCH_SUCCESS', result })
    } catch (e) {
      dispatch({ type: 'SCOREBOARD_FETCH_FAILURE', error: String(e) })
    }
  }
}
function closeScoreboard () {
  return { type: 'SCOREBOARD_CLOSE' }
}

Затем каждая часть магазина (управляемая редукторами) может реагировать на следующие действия:

| Часть магазина / редуктора | Поведение |
| --- | --- |
| таблоView | Обновить видимость до true для SCOREBOARD_VIEW , false на SCOREBOARD_CLOSE и SCOREBOARD_FETCH_FAILURE |
| ScoreboardLoadingIndicator | Обновить видимость до true для SCOREBOARD_VIEW , false на SCOREBOARD_FETCH_* |
| ScoreboardData | Обновить данные внутри магазина на SCOREBOARD_FETCH_SUCCESS |
| messageBox | Обновите видимость до true и сохраните сообщение на SCOREBOARD_FETCH_FAILURE и обновите видимость до false на MESSAGE_BOX_CLOSE |

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

  1. Ошибки выявлять проще.

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

Например, если серверу не удается получить табло и окно сообщения не появляется, вам не нужно выяснять, почему действие SHOW_MESSAGE_BOX не отправляется. Становится очевидным, что окно сообщения неправильно обработало действие SCOREBOARD_FETCH_FAILURE .

Исправление тривиально, его можно перезагрузить в горячем режиме и путешествовать во времени.

  1. Создатели действий и редукторы можно тестировать отдельно.

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

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

(Интеграционный тест по-прежнему будет очень полезен.)

Не стоит беспокоиться. :) Я ценю дальнейшие разъяснения. На самом деле похоже, что мы здесь согласны. Глядя на ваш пример создания действия, viewScoreboard , он очень похож на мой пример создания действия fetchAndProcessThing , прямо над ним.

Создатели действий и редукторы можно тестировать отдельно.

Хотя я согласен с этим, я думаю, что часто имеет более прагматичный смысл протестировать их вместе. Вполне вероятно, что либо ваше действие _или_, либо ваш редуктор (может быть, оба) очень просты, и поэтому окупаемость усилий при тестировании простого изолированного метода невысока. Вот почему я предложил протестировать создатель действий, редюсер и связанные селекторы вместе (как «утка»).

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

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

Ах, интересное наблюдение @winstonewert. Мы следовали шаблону использования уникальных констант типов для каждого пакета «уток», и поэтому, по расширению, редуктор реагирует только на действия, отправленные его родственными создателями действий. Честно говоря, я не совсем уверен, что я чувствую к произвольным редукторам, реагирующим на действие. Это немного похоже на плохую инкапсуляцию.

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

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

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

В Redux нет такой вещи, как объединение редукторов и создателей действий. Это чисто утка. Некоторым это нравится, но он скрывает фундаментальные сильные стороны модели Redux / Flux: мутации состояний отделены друг от друга и от кода, вызывающего их.

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

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

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

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

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

  • В императивном стиле магазину дается «что делать», например SHOW_MESSAGE_BOX или SHOW_ERROR
  • В реактивном стиле магазину выдается «факт того, что произошло», например, DATA_FETCHING_FAILED или USER_ENTERED_INVALID_THING_ID . Магазин реагирует соответствующим образом.

В предыдущем примере у меня нет SHOW_MESSAGE_BOX action или showError('Invalid thing id="'+id+'"') action creator, потому что это не факт. Это приказ.

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

// type Command = State → State
// :: Action → Command
function interpretAction (action) {
  switch (action.type) {
  case 'DATA_FETCHING_FAILED':
    return showErrorMessage('Data fetching failed')
    break
  case 'USER_ENTERED_INVALID_THING_ID':
    return showErrorMessage('User entered invalid thing ID')
    break
  case 'CLOSE_ERROR_MESSAGE':
    return hideErrorMessage()
    break
  default:
    return doNothing()
  }
}

// :: (State, Action) → State
function errorMessageReducer (state, action) {
  return interpretAction(action)(state)
}

const showErrorMessage = message => state => ({ visible: true, message })
const hideErrorMessage = () => state => ({ visible: false })
const doNothing = () => state => state

Когда действие поступает в магазин как «факт», а не как «команда», меньше шансов, что оно может пойти не так, потому что, ну, это факт.

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

Вы также можете изменить свой редуктор, чтобы при срабатывании USER_ENTERED_INVALID_THING_ID текстовое поле идентификатора объекта сбрасывалось. И это изменение также проходит во времени. Вы также можете локализовать сообщение об ошибке, не обновляя страницу. Это сужает цикл обратной связи и значительно упрощает отладку и настройку.

(Здесь я просто говорю о плюсах, конечно, есть минусы. Вам нужно гораздо больше думать о том, как представить этот факт, учитывая, что ваш магазин может реагировать на эти факты только синхронно и без побочных эффектов. См. Обсуждение альтернативные модели асинхронных / побочных эффектов и этот вопрос, который я разместил на StackOverflow . Думаю, мы еще не рассмотрели эту часть.)


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

Также очень часто несколько компонентов берут данные из одного и того же хранилища. Также довольно часто один компонент зависит от данных из нескольких частей магазина. Разве это не похоже на плохую инкапсуляцию? Чтобы стать по-настоящему модульным, не должен ли компонент React также входить в состав «утиного» пакета? (Это делает архитектура вязов .)

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

Точно так же я считаю, что Redux / Flux делает вашу модель данных реактивной , рассматривая действия как факт, поэтому вам не нужно указывать модели данных, как обновлять себя.

Спасибо, что нашли время написать и поделиться своими мыслями, @dtinth. Также спасибо @gaearon за участие в этом обсуждении. (Я знаю, что у вас много чего происходит.) Вы оба дали мне кое-какие дополнительные соображения. :)

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

Эх ... кое-что из этого субъективно, но нет. Я думаю об экспортированных создателях действий и селекторах как об API для модуля.

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

Между прочим, окно сообщения - хороший пример того, где я бы предпочел иметь отдельный создатель действий для отображения. В основном потому, что я хочу пропустить время, когда он был создан, чтобы его можно было отклонить автоматически (а создатель действия - это то место, где вы вызываете нечистое Date.now() ), потому что я хочу настроить таймер, чтобы отклонить его, я хотите заблокировать этот таймер и т. д. Поэтому я бы рассмотрел окно сообщения как случай, когда его «поток действий» достаточно важен, чтобы гарантировать его личные действия. Тем не менее, возможно, то, что я описал, можно решить более элегантно с помощью https://github.com/yelouafi/redux-saga.

Сначала я написал это в чате Discord Reactiflux, но меня попросили вставить сюда.

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

  1. Создателю действия передается минимальный объем информации, необходимый для выполнения обновления. Т.е. ничего, что можно вычислить из текущего состояния, не должно быть в нем.
  2. Состояние запрашивается для получения любой информации, необходимой для выполнения обновлений (например, когда вы хотите скопировать Todo с идентификатором X, вы получаете атрибуты Todo с идентификатором X, чтобы вы могли сделать копию). Это можно сделать в создателе действия, а затем эта информация будет включена в объект действия. Это приводит к появлению жирных объектов действия. ИЛИ это можно было бы вычислить в редукторе - объекты тонкого действия.
  3. На основе этой информации применяется чистая логика редуктора для получения следующего состояния.

Теперь проблема в том, что поместить в создатель действий, а что в редуктор, выбор между толстыми и тонкими объектами действия. Если вы поместите всю логику в создатель действий, вы получите толстые объекты действий, которые в основном объявляют обновления состояния. Редукторы становятся чистыми, тупыми: добавьте это, удалите это, обновите эти функции. Их будет легко составить. Но там будет не так много вашей бизнес-логики.

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

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

Спасибо, что поделились этими мыслями @tommikaikkonen. Я все еще не знаю, какой здесь «ответ». Я согласен с вашим резюме. Я бы добавил одну небольшую заметку в раздел «Поместите всю логику в создателя действий ...», которая позволяет вам использовать (общие) селекторы для чтения данных, что в некоторых случаях может быть приятным.

Это интересная ветка! Думаю, проблема, с которой мы все сталкиваемся, - это выяснить, где разместить код в приложении redux. Мне нравится идея CQRS о том, чтобы просто записывать то, что произошло.
Но я вижу здесь несоответствие идей, потому что в CQRS, AFAIK лучшей практикой является создание ненормированного состояния из действия / событий, которое напрямую потребляется представлениями. Но в redux лучшая практика - создать полностью нормализованное состояние и получить данные ваших представлений с помощью селекторов.
Если мы создадим денормализованное состояние, которое напрямую потребляется представлением, тогда, я думаю, проблема, заключающаяся в том, что редуктору нужны данные в другом редукторе, исчезнет (поскольку каждый редуктор может просто хранить все данные, которые ему нужны, не заботясь о нормализации). Но тогда при обновлении данных возникают другие проблемы. Может это суть обсуждения?

Исходя из многих лет объектно-ориентированной разработки ... Redux кажется серьезным шагом назад. Я нахожу себя умоляющим создать классы, которые инкапсулируют события (создателей действий) и бизнес-логику. Я все еще пытаюсь найти значимый компромисс, но пока не могу этого сделать. Кто-нибудь еще думает так же?

Объектно-ориентированное программирование поощряет объединение операций чтения с записью. Это создает множество проблем: создание снимков и откат, централизованное ведение журнала, отладка неправильных мутаций состояния, детализированные эффективные обновления. Если вы не чувствуете, что это проблемы для вас, если вы знаете, как избежать их при написании традиционного объектно-ориентированного кода MVC, и если Redux создает больше проблем, чем решает в вашем приложении, не используйте Redux: wink: .

@jayesbe Исходя из https://www.leaseweb.com/labs/2015/08/object-oriated-programming-is-exceptionally-bad/.

Отделение действий от преобразования данных упрощает тестирование применения бизнес-правил. Преобразования становятся менее зависимыми от контекста приложения.

Это не означает отказ от объектов или классов в Javascript. Например, компоненты React реализованы как объекты, а теперь как классы. Но компоненты React предназначены просто для создания проекции предоставленных данных. Рекомендуется использовать чистые компоненты, которые не хранят состояние.

Именно здесь на помощь приходит Redux: для организации состояния приложения и объединения действий и соответствующего преобразования состояния приложения.

@johnsoftek спасибо за ссылку. Однако, исходя из моего опыта за последние 10 лет ... Я не согласен с этим, но нам не нужно здесь вдаваться в дебаты между ОО и не ОО. У меня проблема с организацией кода и абстракцией.

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

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

У меня есть единственный класс Application, который самодостаточен со всей бизнес-логикой, оболочками API и т. Д., Которые мне нужны для взаимодействия с моим серверным приложением.

пример..

export default Application {
    constructor(config) {
        this.config = config;
    } 

    config() {
        return this.config;
    }

    login(data, cb) {
        const url = [
            this.config.url,
            '?client=' + this.config.client,
            '&username=' + data.username,
            ....
        ].join('');

        fetch(url).then((responseText) => {
            cb(responseText);
        })
    }

    ... more business logic 
}

Я создал единственный экземпляр этого объекта и поместил его в контекст .. путем расширения Redux Provider

import { Provider } from 'react-redux';

export default class MyProvider extends Provider {
    getChildContext() {
        return Object.assign({}, Provider.prototype.getChildContext.call(this), {
            app: this.props.app
        });
    }

    render() {
        return this.props.children;
    }
}
MyProvider.childContextTypes = {
    store: React.PropTypes.object,
    app: React.PropTypes.object
}

Тогда я использовал этого провайдера как таковой

import Application from './application';
import config from './config';

class MyApp extends Component {
  render() {
    return (
      <MyProvider store={store} app={new Application(config)}>
        <Router />
      </MyProvider>
    );
  }
}

AppRegistry.registerComponent('MyApp', () => MyApp);

наконец, в моем компоненте я использовал свое приложение ..

class Login extends React.Component {
    render() {
        const { app } = this.context;
        const { state, actions } = this.props;
        return (
              <View style={style.transparentContainer}>
                <Form ref="form" type={User} options={options} />
                <Button 
                  onPress={() => {
                    value = this.refs.form.getValue();
                    if (value) {
                      app.login(value, actions.login);
                    }
                  }}
                >
                  Login
                </Button>
              </View>
        );
    }
};
Login.contextTypes = {
  app: React.PropTypes.object,
};

function mapStateToProps(state) {
  return {
      state: state.default.auth
  };
};

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(authActions, dispatch),
    dispatch
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(Login);

Таким образом, Action Creator - это просто функция обратного вызова моей бизнес-логики.

                      app.login(value, actions.login);

На данный момент это решение работает нормально, хотя я только начал с аутентификации.

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

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

Вы не найдете здесь «функциональной толпы»: wink:. Причина, по которой мы выбираем функциональные решения в Redux, не потому, что мы догматичны, а потому, что они решают некоторые проблемы, которые люди часто создают из-за классов. Например, отделение редюсеров от создателей действий позволяет нам разделять чтение и запись, что важно для регистрации и воспроизведения ошибок. Действия, являющиеся простыми объектами, делают возможными запись и воспроизведение, поскольку они сериализуемы. Точно так же, когда состояние является простым объектом, а не экземпляром MyAppState очень легко сериализовать его на сервере и десериализовать на клиенте для рендеринга сервера или сохранить его части в localStorage . Выражение редукторов в виде функций позволяет нам реализовать путешествие во времени и горячую перезагрузку, а выражение селекторов в виде функций упрощает добавление мемоизации. Все эти преимущества не имеют ничего общего с тем, что мы являемся «функциональной группой», и все связаны с решением конкретных задач, для решения которых была создана эта библиотека.

Я создал единственный экземпляр этого объекта и поместил его в контекст .. путем расширения Redux Provider

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

Однако я бы не стал расширять Provider как это хрупко. В этом нет необходимости: React объединяет контекст компонентов, поэтому вместо этого вы можете просто обернуть его.

import { Component } from 'react';
import { Provider } from 'react-redux';

export default class MyProvider extends Component {
    getChildContext() {
        return {
            app: this.props.app
        };
    }

    render() {
        return (
            <Provider store={this.props.store}>
                {this.props.children}
            </Provider>
        );
    }
}
MyProvider.childContextTypes = {
    app: React.PropTypes.object
}
MyProvider.propTypes = {
    app: React.PropTypes.object,
    store: React.PropTypes.object
}

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

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

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

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

const appSelector = createSelector(
   (state) => state.config,
   (config) => new Application(config)
)

А затем в mapStateToProps:

function mapStateToProps(state) {
  return {
      app: appSelector(state)
  };
};

Тогда вам не понадобится выбранный вами метод провайдера, вы просто получите объект приложения из состояния. Благодаря повторному выбору объект Application будет создан только при изменении конфигурации, что, вероятно, произойдет только один раз.

Я думаю, что этот подход может иметь преимущество в том, что он легко позволяет расширить идею до наличия нескольких таких объектов, а также того, что эти объекты зависят от других частей состояния. Например, у вас может быть класс UserControl с методами входа / выхода / и т.д., который имеет доступ как к конфигурации, так и к части вашего состояния.

Итак, в целом ваш подход имеет смысл.

: +1: Спасибо, помогло. Я согласен с улучшением MyProvider. Я обновлю свой код, чтобы следовать. Одной из самых больших проблем, с которыми я столкнулся при первом изучении Redux, было семантическое понятие «Создатели действий»… оно не изменилось, пока я не приравнял их к событиям. Для меня это было своего рода осознанием того, что ... это события, которые отправляются.

@winstonewert - доступен ли createSelector на react-native? Я не верю, что это так. В то же время действительно выглядит так, как будто вы создаете новое приложение каждый раз, когда присоединяете его в mapStateToProps к какому-либо компоненту? Моя цель - создать экземпляр единого объекта, который обеспечивает всю бизнес-логику для приложения и делает этот объект доступным глобально. Я не уверен, работает ли ваше предложение. Хотя мне нравится идея иметь дополнительные объекты, доступные при необходимости ... технически я могу создать экземпляр, если необходимо, через экземпляр Application.

Одной из самых больших проблем, с которыми я столкнулся при первом изучении Redux, было семантическое понятие «Создатели действий»… оно не изменилось, пока я не приравнял их к событиям. Для меня это было своего рода осознанием того, что ... это события, которые отправляются.

Я бы сказал, что в Redux вообще нет семантического понятия Action Creators. Существует семантическое понятие действий (которые описывают то, что произошло, и примерно эквивалентны событиям, но не совсем - например, см. Обсуждение в №351). Создатели действий - это просто шаблон для организации кода. Удобно иметь фабрики для действий, потому что вы хотите убедиться, что действия одного типа имеют согласованную структуру, имеют одинаковый побочный эффект перед отправкой и т. Д. Но с точки зрения Redux создателей действий не существует - Redux видит только действия.

доступен ли createSelector в react-native?

Он доступен в Reselect, который представляет собой простой JavaScript без зависимостей и может работать в Интернете, на нативном, на сервере и т. Д.

Ах. хорошо понял. Все намного яснее. Ваше здоровье.

Не вкладывайте объекты в mapStateToProps и mapDispatchToProps

Недавно я столкнулся с проблемой, когда вложенные объекты терялись, когда Redux объединяет mapStateToProps и mapDispatchToProps (см. Responsejs / response-redux # 324). Хотя @gaearon предоставляет решение, которое позволяет мне использовать вложенные объекты, он продолжает говорить, что это

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

@bvaughn сказал

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

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

Это сбило меня с толку какое-то время ...

почему нам все еще нужно создавать файлы и функции редуктора вручную?

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

Это преимущество сохранения большей части логики обновления состояния в редукторе.

@dtinth Просто для пояснения, говоря «логика обновления состояния», вы имеете в виду «бизнес-логику»?

Я не уверен, что использование большей части вашей логики в творчестве - хорошая идея. Если все ваши редукторы - это просто тривиальные функции, принимающие ADD_X -подобные действия, тогда они вряд ли будут иметь ошибки - отлично! Но затем все ваши ошибки были переданы создателям действий, и вы теряете великолепный опыт отладки, на который ссылается @dtinth .

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

@sompylasar "бизнес-логика" и "логика обновления состояния", я думаю, это одно и то же.

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

в качестве примера .. это мой типичный редуктор

export default function reducer(state = initialState, action = {}) {
  switch (action.type) {
    case 'FOO_REQUEST':
    case 'FOO_RESPONSE':
    case 'FOO_ERROR':
    case 'FOO_RESET':
      return {
        ...state,
        ...action.data
      }; 
    default:
      return state;
  }
}

Мои типичные действия:

export function fooRequest( res ) {
  return {
    type: 'FOO_REQUEST',
    data: {
        isFooing: true,
        toFoo: res.saidToFoo
    }
  };
}

export function fooResponse( res ) {
  return {
    type: 'FOO_RESPONSE',
    data: {
        isFooing: false,
        isFooed: true,
        fooData: res.data
    }
  };
}

export function fooError( res ) {
  return {
    type: 'FOO_ERROR',
    data: {
        isFooing: false,
        fooData: null,
        isFooed: false,
        fooError: res.error
    }
  };
}

export function fooReset( res ) {
  return {
    type: 'FOO_RESET',
    data: {
        isFooing: false,
        fooData: null,
        isFooed: false,
        fooError: null,
        toFoo: true
    }
  };
}

Моя бизнес-логика определяется в объекте, хранящемся в контексте, т.е.

export default class FooBar
{
    constructor(store)
    {
        this.actions = bindActionCreators({
            ...fooActions
        }, store.dispatch);
    }

    async getFooData()
    {
        this.actions.fooRequest({
            saidToFoo: true
        });

        fetch(url)
        .then((response) => {
            this.actions.fooResponse(response);
        })
    }
}

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

Я больше нигде не использую mapDispatchToProps (). Для Redux я теперь только mapStateToProps при создании подключенного компонента. Если мне нужно инициировать какие-либо действия ... Я могу инициировать их через объект моего приложения через контекст.

class SomeComponent extends React.Component {
    componentWillReceiveProps(nextProps) {
        if (nextProps.someFoo != this.props.someFoo) {
            const { app } = this.context;
            app.actions.getFooData();
        }
    }
}
SomeComponent.contextTypes = {
    app: React.PropTypes.object
};

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

Так я организовал свою основную «бизнес-логику». Поскольку мое состояние действительно поддерживается на внутреннем сервере ... это очень хорошо работает для моего варианта использования.

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

@jayesbe Следующая часть означает, что у вас нет «бизнес-логики» в редукторах, и, более того, структура состояний переместилась в создатели действий, которые создают полезную нагрузку, которая передается в хранилище через редуктор:

    case 'FOO_REQUEST':
    case 'FOO_RESPONSE':
    case 'FOO_ERROR':
    case 'FOO_RESET':
      return {
        ...state,
        ...action.data
      }; 
export function fooRequest( res ) {
  return {
    type: 'FOO_REQUEST',
    data: {
        isFooing: true,
        toFoo: res.saidToFoo
    }
  };
}

@jayesbe Мои действия и редукторы очень похожи на ваши, некоторые действия получают простой объект сетевого ответа в качестве аргумента, и я инкапсулировал логику того, как обрабатывать данные ответа в действие и, наконец, вернуть очень простой объект в качестве возвращаемого значения, а затем перейти к редуктор через вызов dispatch (). Как и то, что вы сделали. Проблема в том, что если ваше действие написано таким образом, ваше действие сделало почти все, и ответственность вашего редуктора будет очень легкой, почему мы должны передавать данные для хранения вручную, если редуктор просто распространяет объект действия? Redux дозировать его автоматически для нас совсем не сложно.

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

В крайнем случае, посмотрите это:
«Синхронные движки RTS и история рассинхронизации» @ForrestTheWoods
https://blog.forrestthewoods.com/synchronous-rts-engines-and-a-tale-of-desyncs-9d8c3e48b2be

5 апреля 2016 г. в 17:54 "Джон Бабак" [email protected] написал:

Это преимущество сохранения большей части логики обновления состояния в
редуктор.

@dtinth https://github.com/dtinth Просто чтобы уточнить, сказав
"логика обновления состояния" вы имеете в виду "бизнес-логику"?

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую или просмотрите его на GitHub
https://github.com/reactjs/redux/issues/1171#issuecomment -205754910

@LumiaSaki Совет сохранять ваши редукторы простыми, сохраняя сложную логику в создателях действий, противоречит рекомендуемому способу использования Redux. Рекомендуемый шаблон Redux - противоположный: сохраняйте простоту создателей действий, сохраняя сложную логику в редукторах. Конечно, вы в любом случае можете использовать всю свою логику в создателях действий, но при этом вы не следуете парадигме Redux, даже если вы используете Redux.

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

Как бы то ни было, я создаю свои редукторы, используя что-то вроде:

let {reducer, actions} = defineActions({
   fooRequest: (state, res) => ({...state, isFooing: true, toFoo: res.saidToFoo}),
   fooResponse: (state, res) => ({...state, isFooing: false, isFooed: true, fooData: res.data}),
   fooError: (state, res) => ({...state, isFooing: false, fooData: null, isFooed: false, fooError: res.error})
   fooReset: (state, res) => ({...state, isFooing: false, fooData: null, isFooed: false, fooError: null, toFoo: false})
})

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

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

function reducer(state, action) {
    if (action.data) {
        return {...state, ...action.data}
   } else {
        return state;
   }
}

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

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

Рассмотрим программное обеспечение, подобное IDE, где пользователь может щелкнуть кнопку запуска, и оно скомпилирует и запустит приложение. (Здесь я использую асинхронную функцию, которая принимает хранилище, но вместо этого вы можете использовать redux-thunk .)

js export async function runApp (store) { try { store.dispatch({ type: 'startCompiling' }) const compiledApp = await compile(store) store.dispatch({ type: 'startRunning', app: compiledApp }) } catch (e) { store.dispatch({ type: 'errorCompiling', error: e }) } }

  • Детерминированная часть. Эта часть дает вполне предсказуемый результат. Учитывая одно и то же состояние и одно и то же событие, результат всегда предсказуем. На мой взгляд, с этой частью лучше всего справится редуктор. Я называю это бизнес-правилами.

`` js
импортировать u из "updeep"

экспорт const reducer = createReducer ({
// [название действия]: действие => currentState => nextState
startCompiling: () => u ({compiling: true}),
errorCompiling: ({error}) => u ({compiling: false, compileError: error}),
startRunning: ({app}) => u ({
работает: () => приложение,
компиляция: false
}),
stopRunning: () => u ({running: false}),
discardCompileError: () => u ({compileError: null}),
// ...
})
`` ''

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

@dtinth Отлично, потому что предыдущий пример в https://github.com/reactjs/redux/issues/1171#issuecomment -205782740 выглядит совершенно иначе, чем то, что вы написали в https://github.com/reactjs/redux/ issues / 1171 # issuecomment -205888533 - он предлагает создать часть состояния в создателях действий и передать их в редукторы, чтобы они просто распространяли обновления (этот подход кажется мне неправильным, и я согласен с тем же, что указано в https://github.com/reactjs/redux/issues/1171#issuecomment-205865840).

@winstonewert

Рекомендуемый шаблон Redux - противоположный: сохраняйте простоту создателей действий, сохраняя сложную логику в редукторах.

Как вы можете вложить сложную логику в рудукторы и при этом сохранить их в чистоте?

Если я, например, вызываю fetch () и загружаю данные с сервера ... а затем обрабатываю их каким-то образом. Мне еще предстоит увидеть пример редуктора со "сложной логикой".

@jayesbe : Э ... "сложный" и "чистый" ортогональны. У вас может быть _ действительно_ сложная условная логика или манипуляции внутри редуктора, и до тех пор, пока это просто функция его входов без побочных эффектов, он все еще чист.

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

@markerikson ОК, логические операторы - это одно ... но выполнение конкретных задач? Например, у меня есть одно действие, которое в одном случае запускает три других действия, а в другом случае запускает два различных и отдельных действия. Эта логика + выполнение задач не похоже на то, что они должны идти в редукторах.

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

@jayesbe : Я не думаю, что кто-либо когда-либо говорил, что запуск других действий должен происходить в редукторе. А на самом деле не должно. Работа редуктора - это просто (currentState + action) -> newState .

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

Честно говоря, я немного не понимаю, о чем идет речь.

@markerikson, похоже, тема о "бизнес-логике" и о том, куда она идет. В том-то и дело .. все зависит от приложения. Есть разные способы сделать это. Некоторые более сложные, чем другие. Если вы найдете хороший способ решить свою проблему, который упростит вам обслуживание и организацию. Это все, что действительно важно. Моя реализация оказалась очень скудной для моего варианта использования, даже если она противоречит парадигме.

Как вы заметили, все, что действительно имеет значение, - это то, что работает для вас. Но вот как я бы решил вопросы, о которых вы спрашивали.

Если я, например, вызываю fetch () и загружаю данные с сервера ... а затем обрабатываю их каким-то образом. Мне еще предстоит увидеть пример редуктора со "сложной логикой".

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

k логических операторов - это одно ... но выполнение конкретных задач? Например, у меня есть одно действие, которое в одном случае запускает три других действия, а в другом случае запускает два различных и отдельных действия. Эта логика + выполнение задач не похоже на то, что они должны идти в редукторах.

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

function createFoobar(dispatch, state, updateRegistry) {
   dispatch(createFoobarRecord());
   if (updateRegistry) {
      dispatch(updateFoobarRegistry());
   } else {
       dispatch(makeFoobarUnregistered());
   }
   if (hasFoobarTemps(state)) {
      dispatch(dismissFoobarTemps());
   }
}

Это не рекомендуемый способ использования Redux. Рекомендуемый способ Redux - иметь одно действие CREATE_FOOBAR, которое вызывает все эти желаемые изменения.

@winstonewert :

Это не рекомендуемый способ использования Redux. Рекомендуемый способ Redux - иметь одно действие CREATE_FOOBAR, которое вызывает все эти желаемые изменения.

У вас есть указатель на указанное место? Потому что, когда я проводил исследование для страницы часто задаваемых вопросов, я пришел к выводу, что «это зависит от обстоятельств», прямо от Дэна. См http://redux.js.org/docs/FAQ.html#actions -multiple-действия и этот ответ Дэном на SO .

«Бизнес-логика» - действительно довольно широкий термин. Он может охватывать такие темы, как «Что-то случилось?», «Что нам теперь делать, когда это произошло?», «Это действительно так?» И так далее. Основываясь на дизайне Redux, на эти вопросы _ можно_ ответить в разных местах в зависимости от ситуации, хотя я бы увидел, что «случилось ли это» скорее как ответственность создателя действия, а «что сейчас» почти определенно является обязанностью редуктора.

В целом, мой взгляд на этот _ весь вопрос_ "бизнес-логики" таков: _ "это зависит _". Есть причины, по которым вы можете захотеть выполнить синтаксический анализ запроса в создателе действий, и причины, по которым вы можете захотеть сделать это в редукторе. Бывают случаи, когда ваш редуктор может просто «взять этот объект и поместить его в мое состояние», а в других случаях ваш редуктор может быть очень сложной условной логикой. Бывают случаи, когда ваш создатель действий может быть очень простым, а в других случаях - сложным. Бывают случаи, когда имеет смысл отправлять несколько действий подряд для представления шагов процесса, а в других случаях, когда вам нужно отправить только общее действие «THING_HAPPENED» для представления всего этого.

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

Кроме этого? Найдите то, что вам подходит. Быть последовательным. Знайте, почему вы делаете это определенным образом. Смирись с этим.

хотя я бы рассматривал «случилось ли это» как скорее ответственность создателя действия, а «что сейчас» почти определенно является обязанностью редьюсера.

Вот почему параллельно обсуждается, как поместить побочные эффекты, т.е. нечистую часть «что сейчас», в редукторы: # 1528 и сделать их просто чистыми описаниями того, что должно произойти, например, следующие действия для отправки.

Я использовал следующий шаблон:

  • Что делать : Action / Action Creator
  • Как это сделать : ПО промежуточного слоя (например, промежуточное ПО, которое прослушивает «асинхронные» действия и выполняет вызовы моего объекта API.)
  • Что делать с результатом : редуктор

Ранее в этой беседе Дэн сказал следующее:

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

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

@winstonewert : Дэн ссылается на шаблон «композиция редуктора», то есть «это действие, которое когда-либо прослушивается только одним редуктором» против «многие редукторы могут реагировать на одно и то же действие». Дэн очень хорошо разбирается в произвольных редукторах, реагирующих на одно действие. Другие предпочитают такие вещи, как подход «уток», где редукторы и действия ОЧЕНЬ тесно связаны, и только один редуктор когда-либо обрабатывает данное действие. Итак, этот пример касается не «последовательного выполнения нескольких действий», а скорее того, «сколько частей моей структуры редуктора ожидают ответа на это».

Но прагматично делайте то, что работает.

: +1:

@sompylasar Я вижу ошибку в своих действиях, имея структуру состояний в своих действиях. Я могу легко перенести структуру состояний в свои редукторы и упростить свои действия. Ваше здоровье.

Мне кажется, это одно и то же.

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

В упомянутом вами вопросе StackOverflow он заявляет:

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

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

Я визуализирую здесь пару похожих, но несколько разных вариантов использования:

1) Действие требует обновлений для нескольких областей вашего состояния, особенно если вы используете combineReducers чтобы иметь отдельные функции редуктора, обрабатывающие каждый поддомен. Ты:

  • чтобы и Reducer A, и Reducer B реагировали на одно и то же действие и независимо обновляли свои биты состояния
  • включите в действие все соответствующие данные, чтобы любой редуктор мог получить доступ к другим битам состояния за пределами своего собственного фрагмента
  • добавить еще один редуктор верхнего уровня, который захватывает биты состояния редуктора A и в особом случае передает его редуктору B?
  • отправить действие, предназначенное для редуктора A, с нужными ему битами состояния, а второе действие для редуктора B с тем, что ему нужно?

2) У вас есть набор шагов, которые выполняются в определенной последовательности, каждый шаг требует обновления состояния или действия промежуточного программного обеспечения. Ты:

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

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

@markerikson Итак, ваш совет: «это зависит от того, с какой ситуацией вы столкнулись», и как сбалансировать «бизнес-логику» на действиях или редукторах - это только ваше рассмотрение, мы также должны максимально использовать преимущества чистой функции?

Ага. Редукторы _должны_ быть чистыми, как требование Redux (за исключением 0,00001% особых случаев). Создатели действий абсолютно _не _ должны быть чистыми, и фактически именно там будет жить большая часть ваших «нечистот». Однако, поскольку чистые функции, очевидно, легче понять и протестировать, чем нечистые, _если_ вы можете сделать часть своей логики создания действий чистой, отличной! Если нет, то ничего страшного.

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

@cpsubrian

Что делать с результатом: редуктор

На самом деле, именно поэтому саги предназначены для того, чтобы иметь дело с такими эффектами, как «если это случилось, то это тоже должно произойти»


@markerikson @LumiaSaki

Создатели экшена не обязательно должны быть чистыми, ведь именно там будет жить большая часть ваших «нечистот».

На самом деле от создателей действий даже не требуется быть нечистыми или даже существовать.
См. Http://stackoverflow.com/a/34623840/82609

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

Да, но не так очевидно замечать недостатки каждого подхода без опыта :) См. Также мой комментарий здесь: https://github.com/reactjs/redux/issues/1171#issuecomment -167585575

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

Поэтому вместо определения глобального списка действий для всего приложения вы можете начать разбивать приложение на повторно используемые компоненты, и каждый компонент имеет свой собственный список действий и может только отправлять / сокращать их. Тогда проблема в том, как вы выразите «когда дата выбрана в моем средстве выбора даты, тогда мы должны сохранить напоминание об этом элементе задачи, показать тост с обратной связью, а затем переместить приложение к задачам с напоминаниями»: вот где сага приходит в действие: оркестровка компонентов

См. Также https://github.com/slorber/scalable-frontend-with-elm-or-redux

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

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

В моем случае у меня есть 2 редуктора, заинтересованных в 1 действии. Необработанных данных action.data недостаточно. Им необходимо обрабатывать преобразованные данные. Я не хотел выполнять преобразование в двух редукторах. Поэтому я переместил функцию преобразования в преобразователь. Таким образом мои редукторы получают данные, готовые к употреблению. Это лучшее, что я мог подумать за свой короткий 1-месячный опыт сокращения.

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

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

Скажем, например, в приложении Todo я обновляю имя элемента Todo, поэтому я отправляю действие, передающее часть элемента, которую хочу обновить, то есть:

dispatch(updateItem({name: <text variable>}));

, а определение действия:

const updateItem = (updatedData) => {type: "UPDATE_ITEM", updatedData}

который, в свою очередь, обрабатывается редуктором, который может просто сделать:

Object.assign({}, item, action.updatedData)

чтобы обновить элемент.

Это отлично работает, поскольку я могу повторно использовать одно и то же действие и редуктор для обновления любого свойства элемента Todo, то есть:

updateItem({description: <text variable>})

когда вместо этого меняется описание.

Но здесь компонент должен знать, как элемент Todo определяется в магазине, и если это определение изменится, мне нужно не забыть изменить его во всех компонентах, которые от него зависят, что, очевидно, плохая идея, какие-либо предложения для этого сценария?

@dcoellarb

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

Так что у меня могло быть:

const {reducer, actions, selector} = makeRecord({
    name: TextField,
    description: TextField,
    completed: BooleanField
})

Где makeRecord - это функция для автоматического создания редукторов, создателей действий и селекторов из моего описания. Это устраняет шаблон, но если мне нужно будет сделать что-то, что не соответствует этому аккуратному шаблону, я могу добавить собственный редуктор / действия / селектор к результату makeRecord.

tks @winstonewert мне нравится подход избегать шаблонов, я вижу, что это экономит много времени в приложениях с большим количеством моделей; но я все еще не понимаю, как это отделит компонент от структуры хранилища, я имею в виду, что даже если действие сгенерировано, компоненту все равно нужно будет передать ему обновленные поля, что означает, что компонент по-прежнему должен знать структуру магазин не так ли?

@winstonewert @dcoellarb На мой взгляд, структура полезной нагрузки действия должна принадлежать действиям, а не редукторам, и явно транслироваться в структуру состояний в редукторе. Это должно быть удачным совпадением, что эти структуры зеркально отражают друг друга для первоначальной простоты. Этим двум структурам не обязательно всегда отражать друг друга, потому что они не одно и то же.

@sompylasar правильно, я делаю это, я перевожу данные api / rest в свою структуру магазина, но все же единственный, кто должен знать структуру магазина, - это редукторы и селекторы, верно? причина, по которой я их размещаю, моя проблема связана с компонентами / представлениями, я бы предпочел, чтобы им не нужно было знать структуру магазина на случай, если я решу изменить ее позже, но, как объясняется в моем примере, им нужно знать структуру, чтобы они могли отправьте правильные данные для обновления, я не нашел лучшего способа сделать это :(.

@dcoellarb Вы можете думать о своих представлениях как о

@sompylasar имеет смысл, я попробую, большое спасибо !!!

Я, вероятно, также должен добавить, что вы можете сделать действия более чистыми, используя redux-saga . Однако redux-saga изо всех сил пытается обрабатывать асинхронные события, поэтому вы можете продвинуть эту идею дальше, используя RxJS (или любую библиотеку FRP) вместо redux-saga. Вот пример использования KefirJS: https://github.com/awesome-editor/awesome-editor/blob/saga-demo/src/stores/app/AppSagaHandlers.js

Привет @frankandrobot!

redux-saga изо всех сил пытается обрабатывать асинхронные события

Что ты хочешь этим сказать? Разве redux-saga создан для элегантной обработки асинхронных событий и побочных эффектов? Взгляните на https://github.com/reactjs/redux/issues/1171#issuecomment -167715393

Нет @IceOnFire . В прошлый раз, когда я читал документы redux-saga , обрабатывать сложные асинхронные рабочие процессы сложно. См., Например: http://yelouafi.github.io/redux-saga/docs/advanced/NonBlockingCalls.html.
Он сказал (все еще говорит?) Что-то в этом роде

оставим остальные детали читателю, потому что это начинает усложняться

Сравните это со способом FRP: https://github.com/frankandrobot/rflux/blob/master/doc/06-sideeffects.md#a -more-complex-workflow
Весь этот рабочий процесс выполняется полностью. (Я мог бы добавить лишь несколько строк.) Вдобавок к этому вы по-прежнему получаете большую часть достоинств redux-saga (все - чистая функция, в основном простые модульные тесты).

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

Привет @frankandrobot!

Спасибо за ваше объяснение. Я не вижу упомянутой вами цитаты, возможно, библиотека эволюционировала (например, теперь я вижу эффект cancel никогда раньше не видел).

Если два примера (сага и FRP) ведут себя точно так же, то я не вижу большой разницы: один - это последовательность инструкций внутри блоков try / catch, а другой - это цепочка методов в потоках. Из-за отсутствия у меня опыта работы с потоками я даже считаю более читаемым пример саги и более тестируемым, поскольку вы можете тестировать каждый yield один за другим. Но я совершенно уверен, что это связано больше с моим мышлением, чем с технологиями.

В любом случае мне бы хотелось узнать мнение @yelouafi по этому

@bvaughn, можете ли вы указать на какой-нибудь достойный пример тестирования действия, редуктора, селектора в том же тесте, который вы описываете здесь?

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

Привет @ morgs32 😄

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

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

import configureMockStore from 'redux-mock-store'
import { actions, selectors, reducer } from 'your-redux-module';

it('should support adding new todo items', () => {
  const mockStore = configureMockStore()
  const store = mockStore({
    todos: []
  })

  // Start with a known state
  expect(selectors.todos(store.getState())).toEqual([])

  // Dispatch an action to modify the state
  store.dispatch(actions.addTodo('foo'))

  // Verify the action & reducer worked successfully using your selector
  // This has the added benefit of testing the selector also
  expect(selectors.todos(store.getState())).toEqual(['foo'])
})

Это было моим собственным наблюдением после нескольких месяцев использования Redux над проектом. Это не официальная рекомендация. YMMV. 👍

"Делайте больше в создателях действий и меньше в редукторах"

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

Во-первых, извините за мой английский. Но у меня разные мнения.

Мой выбор - fat reducer, thin action creators.

Мои создатели действий просто __dispatch__ действия (async, sync, serial-async, parallel-async, parallel-async in for-loop) на основе некоторого промежуточного программного обеспечения __promise__.

Мои редукторы разбиты на множество небольших сегментов состояния для обработки бизнес-логики. используйте combineReduers объедините их. reducer - это __pure function__, поэтому ее легко использовать повторно. Возможно, когда-нибудь я буду использовать angularJS, думаю, я смогу повторно использовать свои reducer в своем сервисе для той же бизнес-логики. Если ваш reducer имеет много кодов строк, он может быть разделен на меньший редуктор или абстрактные функции.

Да, есть несколько случаев с перекрестными состояниями, значения которых A зависят от B, C .. и B, C являются асинхронными данными. Мы должны использовать B,C для заполнения или инициализации A. Вот почему я использую crossSliceReducer .

О __Делайте больше в создателях действий и меньше - в редукторах__.

  1. Если вы используете redux-thunk или т. Д. Да. Вы можете получить доступ к полному состоянию в создателях действий с помощью getState() . Это выбор. Или вы можете создать некоторый __crossSliceReducer__, чтобы вы также могли получить доступ к полному состоянию, вы можете использовать какой-либо срез состояния для вычисления вашего другого состояния.

О тестировании __Unit__

reducer - это __pure function__. Так что проводить тестирование легко. А пока я просто тестирую свои редукторы, потому что это важнее всего остального.

Чтобы протестировать action creators ? Я думаю, что если они «толстые», то, возможно, будет нелегко пройти тестирование. Особенно __async action creators__.

Я согласен с тобой, @mrdulin , я тоже так пошел.

@mrdulin Ага. Похоже, промежуточное ПО - это подходящее место для размещения вашей нечистой логики.
Но для бизнес-логики редюсер не кажется подходящим местом. В итоге вы получите множество «синтетических» действий, которые не отражают то, что пользователь просил, а то, что требует ваша бизнес-логика.

Гораздо более простой выбор - просто вызвать некоторые чистые функции / методы класса из промежуточного программного обеспечения:

middleware = (...) => {
  // if(action.type == 'HIGH_LEVEL') 
  handlers[action.name]({ dispatch, params: action.payload })
}
const handlers = {
  async highLevelAction({ dispatch, params }) {
    dispatch({ loading: true });
    const data = await api.getData(params.someId);
    const processed = myPureLogic(data);
    dispatch({ loading: false, data: processed });
  }
}

@bvaughn

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

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

  • Разве модульные тесты для редукторов не должны обнаруживать опечатки в свойствах состояния?
  • По моему опыту, state.stuff.item1 на state.stuff.item2 вы выполните поиск по коду и измените его повсюду - точно так же, как на самом деле изменение имени чего-либо еще. Это обычная задача, и это несложная задача для людей, особенно использующих IDE.
  • Использование селекторов везде - это своего рода пустая абстракция. простым частям состояния. Конечно, вы получаете согласованность, имея этот API для доступа к состоянию, но вы отказываетесь от простоты.

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

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

С философской точки зрения, я всегда рассматриваю селекторы как «методы получения» функционального мира. И по той же причине, по которой я никогда не получу доступ к публичным атрибутам объекта Java, я никогда не получу доступ к подсостояниям непосредственно в приложении Redux.

@IceOnFire Нет ничего, что можно было бы использовать, если вычисления не

Методы получения могут быть обычной практикой в ​​Java, но также и доступ к POJO непосредственно в JS.

@timotgl

Почему между магазином и другим кодом redux есть API?

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

Селекторы и действия используются на уровне пользовательского интерфейса и на уровне саги (если вы используете redux-saga), а не в самом редукторе.

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

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

@timotgl Да, селекторы не обязательны. Но они делают хорошую практику для подготовки к будущим изменениям в коде приложения, позволяя рефакторинг их по отдельности:

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

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

@sompylasar Спасибо за ваш вклад.

потенциально это может быть нетривиальная задача

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

@timotgl Простой пример, которым я могу поделиться:

export const PROMISE_REDUCER_STATE_IDLE = 'idle';
export const PROMISE_REDUCER_STATE_PENDING = 'pending';
export const PROMISE_REDUCER_STATE_SUCCESS = 'success';
export const PROMISE_REDUCER_STATE_ERROR = 'error';

export const PROMISE_REDUCER_STATES = [
  PROMISE_REDUCER_STATE_IDLE,
  PROMISE_REDUCER_STATE_PENDING,
  PROMISE_REDUCER_STATE_SUCCESS,
  PROMISE_REDUCER_STATE_ERROR,
];

export const PROMISE_REDUCER_ACTION_START = 'start';
export const PROMISE_REDUCER_ACTION_RESOLVE = 'resolve';
export const PROMISE_REDUCER_ACTION_REJECT = 'reject';
export const PROMISE_REDUCER_ACTION_RESET = 'reset';

const promiseInitialState = { state: PROMISE_REDUCER_STATE_IDLE, valueOrError: null };
export function promiseReducer(state = promiseInitialState, actionType, valueOrError) {
  switch (actionType) {
    case PROMISE_REDUCER_ACTION_START:
      return { state: PROMISE_REDUCER_STATE_PENDING, valueOrError: null };
    case PROMISE_REDUCER_ACTION_RESOLVE:
      return { state: PROMISE_REDUCER_STATE_SUCCESS, valueOrError: valueOrError };
    case PROMISE_REDUCER_ACTION_REJECT:
      return { state: PROMISE_REDUCER_STATE_ERROR, valueOrError: valueOrError };
    case PROMISE_REDUCER_ACTION_RESET:
      return { ...promiseInitialState };
    default:
      return state;
  }
}

export function extractPromiseStateEnum(promiseState = promiseInitialState) {
  return promiseState.state;
}
export function extractPromiseStarted(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_PENDING);
}
export function extractPromiseSuccess(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_SUCCESS);
}
export function extractPromiseSuccessValue(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_SUCCESS ? promiseState.valueOrError || null : null);
}
export function extractPromiseError(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_ERROR ? promiseState.valueOrError || true : null);
}

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

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

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

function extractTransactionLogPromiseById(globalState, transactionId) {
  return extractState(globalState).transactionLogPromisesById[transactionId] || undefined;
}

export function extractTransactionLogPromiseStateEnumByTransactionId(globalState, transactionId) {
  return extractPromiseStateEnum(extractTransactionLogPromiseById(globalState, transactionId));
}

export function extractTransactionLogPromiseErrorTransactionId(globalState, transactionId) {
  return extractPromiseError(extractTransactionLogPromiseById(globalState, transactionId));
}

export function extractTransactionLogByTransactionId(globalState, transactionId) {
  return extractPromiseSuccessValue(extractTransactionLogPromiseById(globalState, transactionId));
}

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

@timotgl : многие из наших рекомендуемых передовых методов работы с Redux направлены на то, чтобы попытаться инкапсулировать логику и поведение, связанные с Redux.

Например, вы можете отправлять действия непосредственно из подключенного компонента, выполнив this.props.dispatch({type : "INCREMENT"}) . Однако мы не одобряем этого, потому что это заставляет компонент «знать», что он общается с Redux. Более React-идиоматический способ сделать что-то - передать создателей связанных действий, чтобы компонент мог просто вызвать this.props.increment() , и не имеет значения, является ли эта функция создателем связанного действия Redux, переданный обратный вызов вниз родительским, или имитация функции в тесте.

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

Точно так же ничто не мешает вам получить доступ к state.some.deeply.nested.field в ваших mapState функциях или преобразователях. Но, как уже было описано в этом потоке, это увеличивает вероятность опечаток, затрудняет отслеживание мест, где используется конкретная часть состояния, затрудняет рефакторинг и означает, что любая дорогостоящая логика преобразования, вероятно, повторно - запускается каждый раз, даже если в этом нет необходимости.

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

Возможно, вы захотите прочитать мой пост Idiomatic Redux: Using Reselect Selectors for Encapsulation and Performance .

@markerikson Я не

Я хотел сказать, что я не согласен с этим убеждением:

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

Что касается вашего примера с state.some.deeply.nested.field , я вижу значение наличия селектора, чтобы сократить это. selectSomeDeeplyNestedField() - это одно имя функции против 5 свойств, которые я могу ошибиться.

С другой стороны, если вы будете следовать этому руководству до буквы, вы также будете делать const selectSomeField = state => state.some.field; или даже const selectSomething = state => state.something; , и в какой-то момент накладные расходы (импорт, экспорт, тестирование) на выполнение этого последовательно на мой взгляд, больше не оправдывает (спорную) безопасность и чистоту. Это сделано из лучших побуждений, но я не могу избавиться от догматического духа руководства. Я бы доверил разработчикам моего проекта использовать селекторы с умом и, когда это возможно, потому что мой опыт показывает, что они это делают.

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

Конечно. FWIW, есть и другие библиотеки селекторов, а также оболочки для Reselect . Например, https://github.com/planttheidea/selectorator позволяет вам определять пути ключей с точечной нотацией и выполняет за вас внутренние селекторы.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги