Подтвердить что ты не робот

Как обрабатывать сложные побочные эффекты в Redux?

Я много часов пытался найти решение этой проблемы...

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

Табло показывает рейтинг игроков и таблицу лидеров.

Снимок экрана

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

Здесь логика становится очень сложной:

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

  • В противном случае табло будет загружено немедленно. Игроку будет предоставлена ​​возможность входа в систему или регистрации. После этого счет будет отправлен, а затем табло снова будет обновлено.

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

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

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

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

  • Для просмотра операций результаты должны быть кэшированы в памяти, поэтому, если пользователь повторно подписывается на тот же табло, выбор не будет. Однако, если есть оценка, кэш не должен использоваться.

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

  • Эти операции должны быть атомарными. Все состояния должны обновляться за один раз (без промежуточных состояний).

В настоящее время я могу решить эту проблему с помощью Bacon.js(функциональной библиотеки реактивного программирования), поскольку она поставляется с поддержкой Atom-обновления. Код довольно краткий, но сейчас это грязный непредсказуемый код спагетти.

Я начал смотреть на Redux. Поэтому я попытался структурировать хранилище и придумал что-то вроде этого (в синтаксисе YAMLish):

user: (user information)
record:
  level1:
    status: (loading / completed / error)
    data:   (record data)
    error:  (error / null)
scoreboard:
  level1:
    status: (loading / completed / error)
    data:
      - (record data)
      - (record data)
      - (record data)
    error:  (error / null)

Проблема становится: где я помещаю побочные эффекты.

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

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

Но поскольку у меня есть оценка для отправки, это действие SET_USER также должно вызывать запрос AJAX, и в то же время установить record.levelN.status на loading.

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

В архитектуре Elm обновитель также может испускать побочные эффекты при использовании формы Action -> Model -> (Model, Effects Action), но в Redux - просто (State, Action) -> State.

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

function login (options) {
  return (dispatch) => {
    service.login(options).then(user => dispatch(setUser(user)))
  }
}

function setUser (user) {
  return (dispatch, getState) => {
    dispatch({ type: 'SET_USER', user })
    let scoreboards = getObservedScoreboards(getState())
    for (let scoreboard of scoreboards) {
      service.loadUserRanking(scoreboard.level)
    }
  }
}

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

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

Также кажется, что я должен вручную отслеживать всех активных наблюдателей. Если в версии Bacon.js я сделал что-то вроде этого:

Bacon.once() // When first observing the scoreboard
.merge(resubmit口) // When resubmitting because of network error
.merge(user川.changes().filter(user => !!user).first()) // When user logs in (but only once)
.flatMapLatest(submitOrGetRanking(data))

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

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

4b9b3361

Ответ 1

Если вам нужны сложные асинхронные зависимости, просто используйте Bacon, Rx, каналы, саги или другую асинхронную абстракцию. Вы можете использовать их с Redux или без него. Пример с Redux:

observeSomething()
  .flatMap(someTransformation)
  .filter(someFilter)
  .map(createActionSomehow)
  .subscribe(store.dispatch);

Вы можете скомпоновать свои асинхронные действия любым способом - единственная важная часть состоит в том, что в итоге они превращаются в вызовы store.dispatch(action).

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


Обновление: прошло некоторое время, и появилось несколько новых решений. Я предлагаю вам проверить Redux Saga, который стал довольно популярным решением для асинхронного потока управления в Redux.

Ответ 2

Изменить: теперь redux-saga проект, вдохновленный этими идеями

Вот несколько приятных ресурсов


Flux/Redux вдохновлен обработкой потока событий (независимо от имени: eventourcing, CQRS, CEP, лямбда-архитектуры...).

Мы можем сравнить ActionCreators/Actions of Flux с командами/событиями (терминология обычно используется в бэкэнд-системах).

В этих базовых архитектурах мы используем шаблон, который часто называют Saga или Process Manager. В основном это часть в системе, которая принимает события, может управлять своим собственным состоянием, а затем может выдавать новые команды. Чтобы сделать его простым, это немного похоже на реализацию IFTTT (If-This-Then-That).

Вы можете реализовать это с помощью FRP и BaconJS, но вы также можете реализовать это поверх редукторов Redux.

function submitScoreAfterLoginSaga(action, state = {}) {  
  switch (action.type) {

    case SCORE_RECORDED:
      // We record the score for later use if USER_LOGGED_IN is fired
      state = Object.assign({}, state, {score: action.score}

    case USER_LOGGED_IN: 
      if ( state.score ) {
        // Trigger ActionCreator here to submit that score
        dispatch(sendScore(state.score))
      } 
    break;

  }
  return state;
}

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

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

Я сделал это в нашей структуре запуска, и этот шаблон отлично работает. Это позволяет нам обрабатывать IFTTT такие вещи, как:

  • Когда пользователь включен, пользователь закрывает Popup1, затем открывает Popup2 и отображает подсказку подсказки.

  • Когда пользователь использует мобильный сайт и открывает Menu2, закройте Menu1

ВАЖНО: если вы используете функции отмены/повтора/повтора некоторых фреймворков, таких как Redux, важно, чтобы во время воспроизведения журнала событий все эти саги были обнулены, t хочу, чтобы во время повторного воспроизведения были запущены новые события!