2019 年如何处理异步副作用
作者:Peter Ekene Eze ✏️
处理异步操作一直是 React 生态系统中开发者面临的主要问题。
处理异步操作的方法有很多种,包括 Redux-Saga,但本文将重点介绍我认为目前最好的方法:使用react-async.
我们还将把该react-async库与 React 中处理异步副作用的其他现有方法进行比较。
什么是 React Async?
React Async 是一个基于 Promise 的工具,它允许你以声明式的方式处理 Promise 并获取数据。
它可以轻松处理异步 UI 状态,而无需对数据的形状或请求类型做出假设。
React Async 由一个 React 组件和几个 Hook 组成。你可以将它与 fetch、Axios、GraphQL 和其他数据获取库一起使用。
React Async 依赖于声明式语法、JSX 和原生 Promise 来解析代码中更接近你需要的位置(例如,在组件级别)的数据,这与其他系统(如 Redux)不同,Redux 的数据解析是在应用程序的更高级别进行的,使用诸如 actions 和 reducers 之类的东西。
React异步用法
要像下面的示例一样使用 React Async,我们将从……useAsync导入react-async
然后我们可以创建异步函数,该函数接收一个信号作为参数。这个信号是我们的 AbortController API,它允许我们在必要时取消已发出的获取请求。
在我们的组件中,我们调用useAsync并传递我们的异步函数。
调用useAsync返回一个对象,我们可以将其解构为三个重要值:data、error 和 isPending。
这些值告诉我们异步函数的状态——它是仍在等待、出错还是成功。
我们可以使用这些值来为用户呈现合适的界面:
import { useAsync } from "react-async"
// You can use async/await or any function that returns a Promise
const asyncFn = async ({ signal }) => {
const res = await fetch(`/api/users`, { signal })
if (!res.ok) throw new Error(res.statusText)
return res.json()
}
const MyComponent = () => {
const { data, error, isPending } = useAsync({ promiseFn: asyncFn })
if (isPending) return "Loading..."
if (error) return `Something went wrong: ${error.message}`
if (data)
<ul>
{data.users.map(user => <li>{user.name}</li>)}
</ul>
)
return null
React-Async 的使用方法有几种,并且已有相关文档记录:
- 作为钩子
- 和
useFetch - 作为一个组件
- 作为一家工厂
- 带有辅助组件
- 作为静态属性
我将简要介绍前三种方法,以便您了解这些实现方式,但您可以参考官方使用指南,深入了解每种方法。
React Async 作为 Hook
React-Async 提供了一个名为 `.` 的 Hook useAsync。在你的组件中,你可以像这样调用这个 Hook:
import { useAsync } from "react-async";
const MyComponent = () => {
const { data, error, isPending } = useAsync({ promiseFn: loadPlayer, playerId: 1 })
//...
};
React AsyncuseFetch
通过这种方式useFetch,您可以创建一个异步获取函数,该函数可以在组件中的稍后时间运行:
import { useFetch } from "react-async"
const MyComponent = () => {
const headers = { Accept: "application/json" }
const { data, error, isPending, run } = useFetch("/api/example", { headers }, options)
// You can call "handleClick" later
function handleClick() {
run()
}
<button onClick={handleClick}>Load</button>
}
React Async 作为组件
这就是 React Async 与 JSX 结合使用时真正展现其优势的地方:
import Async from "react-async"
const MyComponent = () => (
<Async promiseFn={load}>
{
({ data, error, isPending }) => {
if (isPending) return "Loading..."
if (error) return `Something went wrong: ${error.message}`
if (data)
return (<div> { JSON.stringify(data, null, 2) }</div>)
return null
}
}
</Async>
)
你必须将一个函数Async作为子组件传递给该组件。
如您所见,此函数将根据我们作为 props 提供的异步函数的状态来评估不同的节点值Async。
React Async 与useEffect
useEffect与 Async/Await 结合使用不如 React Async 方便,尤其是在考虑竞态条件、处理清理工作和取消待处理的异步操作时。
React Async 可以非常高效地为你处理所有这些事情。
让我们来看一个使用useEffectAsync/Await 处理竞态条件的典型例子:
const [usersList, updateUsersList] = useState();
useEffect(() => {
const runEffect = async () => {
const data = await fetchUsersList(filter);
updateUsersList(data);
};
runEffect();
}, [updateUsersList, filter]);
在上述例子中,如果由于任何原因我们需要调用useEffect两次,并且第二次调用fetchUsersList在第一次调用之前完成,那么我们将得到一个过时的“已更新”列表。
你可以通过添加一种方法来阻止在必要时进行调用,但是,对于多个表达式updateUsersList,这种方法可能扩展性不佳。await
另一方面,使用 React Async 时,您无需担心取消未解决的请求或处理正确的竞态条件,因为 React 已经为您处理好了:
import { useAsync } from "react-async"
// You can use async/await or any function that returns a Promise
const fetchUsersList = async ({ signal }) => {
const res = await fetch(`/api/users`, { signal })
if (!res.ok) throw new Error(res.statusText)
return res.json()
}
const filteredUsers = (users) => {
// Filter users ...
}
const MyComponent = () => {
const { data, error, isPending } = useAsync({ promiseFn: fetchUsersList})
if (isPending) return "Loading..."
if (error) return `Something went wrong: ${error.message}`
if (data)
<ul>
{ filteredUsers(data.users).map(user => <li>{user.name}</li>) }
</ul>
)
return null
在上面的代码片段中,每次我们调用时fetchUsersList,都会重新渲染MyComponent组件,这意味着我们始终会得到我们期望的状态。
此外,React Async 会在内部为我们进行清理,并使用AbortControllerAPI(即signal传递给fetchUsersList函数的变量)取消未解决的 Promise,因此我们不必担心竞态条件和取消我们不再需要的未解决的 Promise。
如果你的应用程序非常基础,添加一个 14kb 的库来处理异步操作没有意义,那么你可以选择稍微高级一点的实现useEffect。
在我看来,React Async 本身已经非常轻量级,并且经过充分测试,还有许多优点。
因此,除非节省 14kb 带来的收益至关重要,否则您可能需要使用 React Async。
React Async 与 Redux-Saga 的对比
Redux-Sagaredux-saga.js.org是一个旨在使应用程序副作用(例如异步操作,如数据获取和不纯操作,如访问浏览器缓存)更容易管理、更高效地执行、更容易测试,并且更好地处理故障的库。
Redux-Saga 的入门步骤比 React Async 多得多。
这是因为它是一个 Redux 中间件,这意味着你必须为其设置 Redux。
Redux 的理念是为应用程序的所有或主要部分创建一个集中式状态。这样,你就可以通过 dispatch 来更新状态actions。例如:
const Counter = ({ value }) =>
<div>
<button onClick={() => store.dispatch({type: 'INCREMENT_ASYNC'})}>
Increment after 1 second
</button>
<hr />
<div>
Clicked: {value} times
</div>
</div>
Redux-Saga 依靠“ES6 生成器”来帮助你进行网络调用或执行其他异步副作用:
function* incrementAsync() {
yield delay(1000)
yield put({ type: 'INCREMENT' })
}
如你所见,副作用的处理位置与组件本身相距甚远。你通过在组件内部分发 action 来触发更新。然后,更新后的状态通过 props 传入。
虽然功能很标准,但与 React Async 提供的服务截然不同,而且不太直观。
结论
- 使用 React Async,你不需要像使用 Redux 那样预先假设数据的结构。它的使用方式与你通常使用 Promise 的方式一样。
- 使用 React Async,您可以更接近需要数据的地方来解析数据,从而更清楚地了解正在发生的事情。
你无需理解像 reducer 和 actions 这样的复杂结构——你可以利用你已经知道并在组件中使用的知识——JSX、Promises 和 Hooks。
编者按:发现本文有误?您可以在这里找到正确版本。
插件:LogRocket,一款用于 Web 应用的 DVR

LogRocket是一款前端日志工具,可让您重现问题,如同在您自己的浏览器中发生一样。无需猜测错误原因,也无需用户提供屏幕截图和日志转储,LogRocket 即可让您重现会话,快速了解问题所在。它与任何框架的应用程序完美兼容,并提供插件来记录来自 Redux、Vuex 和 @ngrx/store 的额外上下文信息。
除了记录 Redux 操作和状态之外,LogRocket 还会记录控制台日志、JavaScript 错误、堆栈跟踪、包含标头和正文的网络请求/响应、浏览器元数据以及自定义日志。它还会对 DOM 进行插桩,记录页面上的 HTML 和 CSS,即使是最复杂的单页应用程序,也能生成像素级精确的视频。
免费试用。
文章《2019 年如何处理异步副作用》最初发表于LogRocket 博客。
文章来源:https://dev.to/bnevilleoneill/how-to-handle-async-side-effects-in-2019-1d5a
