如何使用 redux-saga 处理异步 API 调用?

Posted

技术标签:

【中文标题】如何使用 redux-saga 处理异步 API 调用?【英文标题】:How to handle async API calls using redux-saga? 【发布时间】: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,以打开成功模式。我该怎么做? 【参考方案1】:

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

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

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

https://medium.com/nossa-coletividad/redux-saga-voc%C3%AA-no-controle-das-opera%C3%A7%C3%B5es-ass%C3%ADncronas-71c9e6b3aabc https://oieduardorabelo.medium.com/redux-saga-gerenciando-efeitos-f518a31c744e

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

https://github.com/mmajdanski/cra-redux-saga-template

在 CodeSandbox 中:

https://codesandbox.io/s/6mgb2

【讨论】:

感谢@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 调用并将状态设置为商店,然后才打开例如,成功模式?在这种情况下,将在单击按钮时调用请求。

以上是关于如何使用 redux-saga 处理异步 API 调用?的主要内容,如果未能解决你的问题,请参考以下文章

如何防止 redux-saga 调用我的 api 调用两次?

redux-saga常用api概述

手写Redux-Saga源码

将异步操作分派到 redux-saga 后如何捕获 redux 存储中的更改

redux-saga基本用法

redux-saga 与 redux 一起使用会导致 render() 被调用两次