如何等待 redux-saga 中的另一个动作

How to wait for another action in redux-saga

我有一些故事可能会完成,然后 put 进入商店的另一个动作。

一些 sagas 应该只在其他 sagas 执行之后执行:它们必须阻塞,或者等到另一个 sagas 完成。

总结如下:

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);
}

我试图在 sagas 之间保持尽可能少的耦合,因此向我展示一种在函数之外实现此目的的方法可加分。

请注意,这是一个简化版本。实际上有几个这样的 fetchMessages 可能会被触发,所有这些都应该等到 AUTHORIZE_SUCCEEDED 进来。

我可以在 fetchMessage() 函数中添加一个循环,但这感觉很糟糕。我对 Javascript、Redux、Saga 或生成器函数不是很熟悉,所以也许这种感觉是完全错误的。我也不确定如何 运行 带有 sagas 超时的循环 yield/select 等

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

另一个有效但笨拙的技巧是在 401 上重试 fetchMessages api 调用。

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;
  }
}

saga 中是否有针对此的 API 或函数?这是一个正确的模式,还是我的想法是阻止一个动作直到另一个动作完成错误开始?

从更耦合但更简单的解决方案开始 - 代替使用延迟在循环中等待,您可以使用 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 saga 本身,并且也就是说 buffer the other actions using 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);
  }
}