Redux: запрос на успех или поток ошибок из Компонента (с использованием redux-saga)

Это единственное, для чего я еще не нашел стандартного решения.

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

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

До redux этот вид потока был прямолинейным, он выглядел бы примерно так:

try {
  this.setState({loading: true});
  const result = await doSomeRequest();
  this.setState({item: result, loading: false});
} catch (e) {
  this.setState({loading: false, error: e});
}

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

У меня могло быть 3 действия: «запрошено», «успешно», «не удалось». В моем компоненте я бы отправил запрошенное действие. Ответственная сага затем отправит действие «успешно» или «не удалось» после обработки «запрошенного».

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

Я действительно пытался найти такой (казалось бы, распространенный) сценарий в документах redux, redux-saga docs или Stackoverflow / Google, но безуспешно.

Примечание: также с redux-thunk я думаю, что этого поведения легко достичь, так как я могу просто. Затем при отправке асинхронного действия и получить действие успеха или действие ошибки в catch (исправьте меня, если я ошибаюсь, никогда реально использовал thunk). Но я еще не видел такого же поведения, достигнутого с помощью redux-saga.

Я придумал 3 конкретных решения:

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

  2. Запуск одноразовой саги прямо перед отправкой действия запроса, который сопоставляет действия "успех" / "сбой" друг с другом и позволяет реагировать на первое выполняющееся действие. Для этого я написал помощник, который абстрагирует ход саги: https://github.com/milanju/redux-post-handling-example/blob/master/src/watchNext.js Этот пример мне нравится намного больше, чем 1. поскольку он простой и декларативный. Хотя я не знаю, имеет ли создание саги во время выполнения, подобное этой, какие-либо негативные последствия, или, может быть, есть другой «правильный» способ добиться того, что я делаю с помощью redux-saga?

  3. Помещение всего, что связано с действием (загрузка, успешный флаг, ошибка), в хранилище и реакция в componentWillReceiveProps на изменения действия ((! This.props.success && nextProps.success)) означает, что действие выполнено успешно). Это похоже на второй пример, но работает с любым выбранным вами решением для обработки побочных эффектов. Может быть, я наблюдаю за чем-то вроде обнаружения неработающего действия, если реквизиты появляются очень быстро, а реквизиты, входящие в componentWillReceiveProps, будут `` накапливаться '', а компонент вообще пропускает переход от неуспеха к успеху?

Не стесняйтесь взглянуть на пример проекта, который я создал для этого вопроса, в котором реализованы полные примеры решений: https://github.com/milanju/redux-post-handling-example

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

  1. Я что-то не понимаю? «Решения», которые я придумал, были для меня непростыми. Может, я смотрю на проблему не с той стороны.
  2. Есть ли проблемы с приведенными выше примерами?
  3. Есть ли лучшие практики или стандартные решения этой проблемы?
  4. Как вы справляетесь с описанным потоком?

Спасибо за прочтение.


person Milant    schedule 27.06.2017    source источник
comment
Тот же вопрос здесь. Я удивлен, что это не то, чем библиотека уже занимается, так как это кажется действительно простым.   -  person Augustin Riedinger    schedule 24.04.2018


Ответы (3)


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

Затем вы сравниваете состояние (запрошено / выполнено / не удалось) со старым состоянием и соответствующим образом обрабатываете различные переходы.

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

person Richard Szalay    schedule 27.06.2017
comment
Это очень непонятно. запрошено / выполнено / не удалось - это действия. Что ставишь в магазин? Также componentWillReceiveProps устарел. - person Augustin Riedinger; 24.04.2018
comment
@AugustinRiedinger, если это не риторический вопрос, я добавил подробный ответ относительно решения Ричарда. - person Nirit Levi; 25.04.2018

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

class CallbackableComponent extends Component {
  constructor() {
    super()
    this.state = {
      asyncActionId: null,
    }
  }

  onTriggerAction = event => {
    if (this.state.asyncActionId) return; // Only once at a time
    const asyncActionId = randomHash();
    this.setState({
      asyncActionId
    })
    this.props.asyncActionWithId({
      actionId: asyncActionId,
      ...whateverParams
    })
  }

  static getDerivedStateFromProps(newProps, prevState) {
    if (prevState.asyncActionId) {
      const returnedQuery = newProps.queries.find(q => q.id === prevState.asyncActionId)
      return {
        asyncActionId: get(returnedQuery, 'status', '') === 'PENDING' ? returnedQuery.id : null
      }
    }
    return null;
  }
}

С редуктором queries следующим образом:

import get from 'lodash.get'

const PENDING = 'PENDING'
const SUCCESS = 'SUCCESS'
const FAIL = 'FAIL'

export default (state = [], action) => {
  const id = get(action, 'config.actionId')
  if (/REQUEST_DATA_(POST|PUT|DELETE|PATCH)_(.*)/.test(action.type)) {
    return state.concat({
      id,
      status: PENDING,
    })
  } else if (
    /(SUCCESS|FAIL)_DATA_(GET|POST|PUT|PATCH)_(.*)/.test(action.type)
  ) {
    return state
      .filter(s => s.status !== PENDING) // Delete the finished ones (SUCCESS/FAIL) in the next cycle
      .map(
        s =>
          s.id === id
            ? {
                id,
                status: action.type.indexOf(SUCCESS) === 0 ? SUCCESS : FAIL,
              }
            : s
      )
  }
  return state
}

В конце концов, мой CallbackableComponent знает, завершился ли запрос, проверяя, присутствует ли this.state.asyncActionId.

Но это происходит за счет:

  1. Добавление записи в магазин (хотя это неизбежно)
  2. Добавление большого количества сложности в компонент.

Я ожидал:

  1. логика asyncActionId, которая должна храниться на стороне саги (например, когда асинхронное действие connected с использованием mapActionsToProps, оно возвращает id при вызове: const asyncActionId = this.props.asyncAction(params), как setTimeout)
  2. часть магазина, которая будет абстрагирована redux-saga, точно так же, как react-router отвечает за добавление текущего маршрута к магазину.

На данный момент я не вижу более чистого способа добиться этого. Но я хотел бы узнать об этом!

person Augustin Riedinger    schedule 25.04.2018

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

mapStateToProps = state => {
 return {
   loading: state.loading,
   error  : state.error,
   data   : state.data 
 };
}

и компонент будет отображаться как:

return(
   {this.props.loading  && <span>loading</span>}
   {!this.props.loading && <span>{this.props.data}</span>}
   {this.props.error    && <span>error!</span>}
)

и когда отправлено действие requested, его редуктор обновит состояние хранилища до {loading: true, error: null}.

и когда отправлено действие succeeded, его редуктор обновит состояние хранилища до {loading: false, error: null, data: results}.

и когда отправлено действие failed, его редуктор обновит состояние хранилища до {loading: false, error: theError}.

таким образом вам не придется использовать componentWillReceiveProps. Надеюсь, это было понятно.

person Nirit Levi    schedule 25.04.2018
comment
Это работает, но на самом деле это не зависит от того, какой компонент инициировал запрос (в случае, если их несколько). Я сообщу о своем решении, хотя я не большой его поклонник. - person Augustin Riedinger; 25.04.2018
comment
Посмотрите на мой ответ и дайте мне знать, имеет ли он для вас смысл. - person Augustin Riedinger; 25.04.2018