Как дождаться другого действия в redux-saga

#javascript #redux-saga

#javascript #redux-saga

Вопрос:

У меня есть несколько саг, которые могут завершиться, а затем put еще одно действие в хранилище.

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

Резюмируется следующим образом:

 export function* authorize(action) {
  const { clientId } = action.data;

  const response = yield call(apiAuthorize, clientId);
  // Redux reducer picks this up and sets a token in storage. 
  yield put({ type: AUTHORIZE_SUCCEEDED, data: response.data.data });
}

export function* fetchMessages(action) {
  console.log(action);
  const { timelineId } = action.data;

  // how can we block this until either `token` is set (getToken returns non-null)
  //  or until AUTHORIZE_SUCCEEDED is sent?

  // The token set by AUTHORIZED_SUCCEEDED is read from the storage.
  // This will be null untill the AUTHORIZE_SUCCEEDED is handled by redux.
  // When null, the api-call will return a 401 so we want to block  untill we
  // have the token.
  const token = yield select(getToken);
  const response = yield call(apiFetchMessages, token);
  yield put({ type: MESSAGES_REQUEST_SUCCEEDED, data: response.data.data });
}

export default function* appSaga() {
  yield takeEvery(AUTHORIZE_REQUESTED, authorize);
  yield takeEvery(MESSAGES_REQUESTED, fetchMessages);
}
 

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

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

Я могу добавить цикл в fetchMessage() функцию, но это кажется неприятным. Я не очень хорошо знаком с функциями Javascript, Redux, Saga или генератора, поэтому, возможно, это чувство совершенно неверно. Я также не уверен, как запустить цикл с таймаутом с выходом / выбором sagas и т. Д.

 while (true) {
  const token = yield setTimeout(() => select(getToken), 1000);
  if (!!token) { 
    break;
  }
});
 

Другой трюк, который работает, но является неуклюжим, заключается в повторной попытке вызова API fetchMessages для 401.

 try {
  const response = yield call(apiFetchMessages, token);
  yield put({ type: MESSAGES_REQUEST_SUCCEEDED, data: response.data.data });
} catch (error) {
  if (error.request.status === 401) {
    yield put({ type: MESSAGES_REQUESTED, data: { blockId } });
  } else {
    throw error;
  }
}
 

Есть ли API или функция для этого в saga? Является ли это правильным шаблоном, или моя идея блокировать одно действие до завершения другого изначально неверна?

Ответ №1:

Начиная с более связанного, но более простого решения — вместо использования задержки для ожидания в цикле, вы можете использовать take эффект для ожидания AUTHORIZE_SUCCEEDED действия:

 export function* fetchMessages(action) {
  const { timelineId } = action.data;

  // the cycle might not be needed if you are sure the 
  // AUTHORIZE_SUCCEEDED action is always dispatched with a valid token
  let token;
  while (true) {
     token = yield select(getToken);
     if (token) break;
     yield take(AUTHORIZE_SUCCEEDED);
  }

  const response = yield call(apiFetchMessages, token);
  yield put({ type: MESSAGES_REQUEST_SUCCEEDED, data: response.data.data });
}
 

Чтобы сделать это менее неуклюжим, вы можете абстрагировать это в свою собственную сагу:

 export function* getTokenSaga() {
  let token;
  while (true) {
     token = yield select(getToken);
     if (token) break;
     yield take(AUTHORIZE_SUCCEEDED);
  }
  return token;
}

export function* fetchMessages(action) {
  const { timelineId } = action.data;

  const token = yield call(getTokenSaga);
  const response = yield call(apiFetchMessages, token);
  yield put({ type: MESSAGES_REQUEST_SUCCEEDED, data: response.data.data });
}
 

Другой способ приблизиться к этому — обернуть метод выборки:

 export function* fetchWithToken(fetchFn, ...params) {
  let token;
  while (true) {
     token = yield select(getToken);
     if (token) break;
     yield take(AUTHORIZE_SUCCEEDED);
  }
  return yield call(fetchFn, token, ...params);
}

export function* fetchMessages(action) {
  const { timelineId } = action.data;

  const response = yield call(fetchWithToken, apiFetchMessages);
  yield put({ type: MESSAGES_REQUEST_SUCCEEDED, data: response.data.data });
}
 

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

В таком случае вы могли бы затем изменить сам fetch метод, чтобы получить токен, поскольку он всегда будет доступен:

 const loadData = (endpoint, payload) => {
  const token = getTokenSelector(store.getState())
  return fetch(endpoint, payload).then(...);
}

const apiFetchMessages = () => {
  return loadData('/messages');
}

export function* fetchMessages(action) {
  const { timelineId } = action.data;

  const response = yield call(apiFetchMessages);
  yield put({ type: MESSAGES_REQUEST_SUCCEEDED, data: response.data.data });
}
 

Если такое изменение невозможно в том месте, где вы отправляете действия, есть еще один способ, которым я могу придумать, как убедиться, что токен всегда доступен без изменения самой fetchMessages саги, и вместо этого буферизировать другие действия с помощью actionChannel effect, пока у вас не будет токена — это может привести к сбою.немного сложнее, так как вам нужно подумать о том, что буферизировать, когда:

 export default function* appSaga() {
  // we buffer all fetching actions
  const channel = yield actionChannel([MESSAGES_REQUESTED, FOO_REQUESTED]);

  // then we block the saga until AUTHORIZE_REQUESTED is dispatched and processed
  const action = yield take(AUTHORIZE_REQUESTED);
  yield call(authorize, action);

  // There is multiple ways to process the buffer, for example
  // we can simply redispatch the actions once we started
  // listening for them using the `takeEvery` effect
  yield takeEvery(MESSAGES_REQUESTED, fetchMessages);
  yield takeEvery(FOO_REQUESTED, fetchFoo);
  while (const action = yield take(channel)) {
    yield put(action);
  }
}
 

Комментарии:

1. Спасибо! Недостающей частью для меня было вызвать take внутри saga. Я наивно предположил, что он использовался только для привязки функций генератора к каналам. Но, читая документы более внимательно, я вижу, что был неправ. Ну, думаю, у меня должен быть RTFM. Отдельное спасибо за переход от совместного решения к решению для всего приложения!