【问题标题】:How to handle async API calls using redux-saga?如何使用 redux-saga 处理异步 API 调用?
【发布时间】:2021-06-18 14:16:46
【问题描述】:

最近,我们开始使用 redux-saga 中间件来管理来自 store 的异步 API 调用。

以前,我们使用基于承诺的通用操作,一旦我们可以在操作本身上使用 async/await.then().catch(),就可以轻松管理组件流。

承诺:

const fetchCars = async () => {
  const {data} = await fetchCarsRequest();
  
  setCars(data);
}

cars.map((car) => <li>{car.name}</li>);

与传奇:

function* fetchCarsSaga() {
  try {
    const {data} = yield call(fetchCarsService);

    yield put(fetchCarsSuccess);
  } catch(error) {
    yield put(fetchCarsFailure);
  } 
}

const fetchCars = async () => {
 fetchCarsRequest(); //action that calls the above saga

 setCars(cars); //state from store 
}

cars.map((car) => <li>{car.name}</li>);

在第二个例子中,即使我们使用生成器来处理异步API调用,也不可能使用await语句,所以它在调用fetchCarsRequest之后立即调用setCars函数,尝试将尚不存在的东西设置为组件状态。

所以我的问题是:有没有办法将 await 语句替换为某些东西并等待请求函数(不是异步函数)完成 API 调用?

【问题讨论】:

  • 不清楚你在问什么 - 你使用的名称与代码中的函数/生成器名称不完全匹配这一事实无济于事。但是 redux-saga 的 call 会自动“等待”一个 Promise 来解决,如果被调用的函数返回一个 Promise - 就像所有的 async 函数一样。如果您将await 放在对fetchCarsRequest 的调用之前(因此该函数隐式返回的承诺直到fetchCarsRequest 才解决)那么我认为一切都应该正常吗?
  • 我想我表达得不够好。我的主要疑问是,一旦 saga 处理异步调用,如何等待 API 调用完成调用另一个函数而不使用 await。例如,我想等待 saga 完成 API 调用并将状态设置为 store,以打开成功模式。我该怎么做?

标签: javascript reactjs asynchronous redux redux-saga


【解决方案1】:

使用 redux-saga,流程会有些不同。您不直接在组件中使用生成器。您需要从 saga 更新 redux 状态并在组件中获取它。

react component -> redux action -> saga -> redux state -> react component

您可以看看 PT-BR 中的两篇文章:

一个好的create-react-app模板是:

在 CodeSandbox 中:

【讨论】:

  • 感谢@oieduardorabelo 的回复。文章真的很有用。但我的问题是:例如,我如何等待 saga 完成 API 调用并将状态设置为商店,然后才打开成功模式?在这种情况下,调用 saga 的操作将在单击按钮时被调用。
  • @GabrielSantana,您需要在 redux 状态下控制它。假设你的 saga 用 yield put({ type: "CAR_MODAL_OPEN" }) 更新了 redux 状态,这个动作触发了一个返回的 reducer 和 { modals: { car: "open" } } 的新状态,然后在你的 react 组件中,你可以将此状态映射到 props,并在你检查的渲染函数中props.carModal === 'open' 渲染你需要的任何东西。循环react component -&gt; redux action -&gt; saga -&gt; redux state -&gt; react component 是状态流动/更新的方式。
  • 是的。事实上,它确实解决了问题。但它仍然在减速器内部处理,具有全局状态,而不是在调用减速器的组件内部。也许没有办法像await 那样等待。我想对于 sagas,你总是必须跟踪 edux 状态的变化,才能根据状态值在 useEffect 内做一些事情。
  • 你是绝对正确的。您需要先同步 redux-saga 和 redux,然后将 redux 与 react 组件同步,然后再将 props 与 useEffect 同步...不是很好,也不是很糟糕!哈哈哈祝@GabrielSantana 好运!
【解决方案2】:

杀死组件状态

您想要调度一个动作fetchCarsRequest(),它会触发一个 API 调用并将结果保存到您的 redux 存储中。你想等待这个完成,然后调用setCars(cars) 将你的组件状态设置为来自 redux 状态的值。为什么?

这是一种反模式。数据已经存在于 redux 中,应该直接从组件中的 redux 访问。您应该使用useSelectorconnect HOC 从redux 中选择cars 的值。一旦您的 fetch 发送 fetchCarsSuccess 并更新了存储状态,它将自动使用新数据重新渲染您的组件。

它应该看起来像这样。

传奇

function* fetchCarsSaga() {
  try {
    const {data} = yield call(fetchCarsService);

    // this action should store the data to the store
    // it needs the data as an argument!
    yield put(fetchCarsSuccess(data));
  } catch(error) {
    yield put(fetchCarsFailure(error));
  } 
}

组件

const Cars = () => {
   const dispatch = useDispatch();
   const cars = useSelector( state => state.cars.data ) || [];
   const isLoading = useSelector( state => state.cars.isLoading );
   const error = useSelector( state => state.cars.error );

   useEffect( () => {
      dispatch(fetchCarsRequest()); // action that calls the saga
   }, [] );


   return (
      {!! error && (<div>Error: {error}</div>)}
      {isLoading ? <Spinner/> : (
        <ul>
          {cars.map((car) => <li key={car.id}>{car.name}</li>)}
        </ul>
      )}
   )
}

【讨论】:

  • @Gabriel 在我看来,useEffect 是“正确”的方式,所以我不确定你为什么要这样做。 redux-thunk 中间件使您可以await dispatch。我不认为 redux-saga 会这样做,但我从未尝试过。
  • 你们完全正确。它确实解决了useEffect 的问题,但仅限于这种情况。事实上,我几乎在每个必须在一开始就发出 API 请求的组件中都这样做,我的问题是:我如何等待 saga 完成 API 调用并将状态设置为商店,然后才打开例如,成功模式?在这种情况下,将在单击按钮时调用请求。