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

Posted

技术标签:

【中文标题】如何在 redux-saga 中等待另一个动作【英文标题】:How to wait for another action in redux-saga 【发布时间】:2022-01-12 18:24:34 【问题描述】:

我有一些 sagas 可能会完成,然后 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 或函数?这是一个正确的模式,还是我的想法是阻止一个动作直到另一个动作完成是错误的?

【问题讨论】:

【参考方案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 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);
  

【讨论】:

谢谢!对我来说缺少的部分是在传奇中调用take。我天真地假设它仅用于将生成器函数绑定到通道。但是更仔细地阅读文档,我发现我错了。好吧,我想我应该使用 RTFM。非常感谢从耦合到应用范围解决方案的旅程!

以上是关于如何在 redux-saga 中等待另一个动作的主要内容,如果未能解决你的问题,请参考以下文章

如何在 redux-saga 中等待 saga 的执行完成?

如何让 ES6 生成器等待承诺,就像在 redux-saga 中一样?

如何等待从另一家商店在一家商店发生的调度

在调用另一个动作创建者之前等待动作创建者完成 - Redux thunk

redux-saga 的单元测试抛出错误:必须在迭代器上调用 runSaga

redux-saga api yield 调用有效,但没有返回数据