React 18 带来了很多变化,它不会破坏你已经编写过的代码,并且有很多改进和一些新概念。
它也让很多开发人员,包括我,意识到我们错误地使用了useEffect
hook。但话说回来,我们被其名称所误导了,因为实际上useEffect
并不应该被用于副作用。
在 React 18 中,虽然仍然可以使用useEffect
来完成一些事情,如使用 API 接口读取的数据填充状态,但实际上不应该将其用于此类目的。如果你在应用程序中启用StrictMode
,在开发模式下,你将发现使用useEffect
会被调用两次,因为现在React会mount 组件、卸载它,然后再次 mount 它,以检查代码是否运行正常。
Suspense
来了
我们应该用来取而代之的,是新的Suspense
组件(虽然它已经存在于 React 17 中,但现在是推荐的方法),此组件将会按照以下方式工作:
<Suspense fallback={<p>Loading...</p>}> <MyComponent /> </Suspense>
上面的代码将会包裹一个组件,这个组件从某些数据源中加载数据,并在完成数据获取之前显示fallback。
Suspense 是什么
简而言之,可能和你想的不同,Suspense 并不是一个新的用于获取数据的接口,因为该工作仍然由诸如“fetch”或“axios”等库委派执行,而它实际上允许你将这些库与 React 集成,并且它的真正工作只是“在加载时显示这段代码,而在完成后显示那段代码”,仅此而已。
Suspense 如何工作
首先,你需要了解 Promise 的工作原理以及它的状态。无论使用传统方式new Promise()
还是新的async/await
语法来使用promise,在任何情况下,promise始终具有以下这三种状态:
pending
-> 它仍在处理请求resolved
-> 请求已返回某些数据,我们获得了200 OK状态rejected
-> 出现了错误,获得了一个错误
Suspense
使用的逻辑与ErrorBoundary
完全相反,因此如果代码引发异常(因为它仍处于加载状态或者由于加载失败),则显示fallback;如果成功解析,则显示子组件。
举个例子
来看一个简单的例子,我们只需创建一个组件来获取API中的某些数据,并且希望在准备好后渲染该组件。
注意
为了简化,这里不会提到如何使用“startTransition”,添加错误边界,甚至不会涉及各种策略之间的区别,例如“fetch-on-render”、“fetch-then-render”等等...
包装 fetch 逻辑
如上所述,当我们的组件正在加载数据或失败时,需要抛出异常,但是一旦成功解决了Promise,就可以简单地返回响应。
为此,我们需要使用以下函数包装我们的请求:
// wrapPromise.js /** * 将promise包装,以便可以与React Suspense一起使用 * @param {Promise} 要处理的promise * @returns {Object} 与Suspense兼容的响应对象 */ function wrapPromise(promise) { let status = 'pending'; let response; const suspender = promise.then( res => { status = 'success'; response = res; }, err => { status = 'error'; response = err; }, ); const handler = { pending: () => { throw suspender; }, error: () => { throw response; }, default: () => response, }; const read = () => { const result = handler[status] ? handler[status]() : handler.default(); return result; }; return { read }; } export default wrapPromise;
因此,上面的代码将检查我们Promise的状态,然后返回一个名为“read”的函数,稍后我们将在组件中调用它。
现在,我们需要使用它包装接口请求库(例子中是axios
),创建一个非常简单的函数:
//fetchData.js import axios from 'axios'; import wrapPromise from './wrapPromise'; /** * 用wrapPromise函数包装Axios请求 * @param {string} 要获取的URL * @returns {Promise} 包装的promise */ function fetchData(url) { const promise = axios.get(url).then(({data}) => data); return wrapPromise(promise); } export default fetchData;
这只是以接口请求库表现的一种抽象,我想强调这只是一种非常简单的实现,您可以将上面的所有代码扩展到任何需要做的工作中。在这里我使用了axios
,但你可以根据自己的需要使用任何东西。
在组件中读取数据
当获取方面的所有内容都准备好后,我们来在组件中使用它。假设有一个简单的组件,只需从某个接口读取名称列表并打印。不同于习惯中在组件中通过useEffect
钩子调用 fetch 的做法,这一次我们要直接在组件开始时(放在任何 hooks 之外),使用我们在包装器中导出的read
方法来调用请求,因此我们的Names
组件大概是这个样子的:
// names.jsx import React from 'react'; import fetchData from '../../api/fetchData.js'; const resource = fetchData('/sample.json'); const Names = () => { const namesList = resource.read(); // rest of the code }
这里所做的是,当调用组件时,read()
函数将开始抛出异常,直到完全解析完成;其后,会继续执行其余代码,在此例中也就是继续 render。所以该组件的全部代码如下:
// names.jsx import React from 'react'; import fetchData from '../../api/fetchData.js'; const resource = fetchData('/sample.json'); const Names = () => { const namesList = resource.read(); return ( <div> <h2>List of names</h2> <ul> {namesList.map(item => ( <li key={item.id}> {item.name} </li>))} </ul> </div> ); }; export default Names;
父组件
现在 Suspense
将要发挥作用了,首先需要在父组件中导入它:
// parent.jsx import React, { Suspense } from 'react'; import Names from './names'; const Home = () => ( <div> <Suspense fallback={<p>Loading</p>}> <Names /> </Suspense> </div> ); export default Home;
到底肿么了?
我们将Suspense
作为React组件导入,然后使用它来包装获取数据的组件,在这些数据被 resolve 之前,它将只会渲染“fallback”组件,因此只是<p>Loading...</p>
或其他什么你需要的自定义组件。
结论
长时间使用useEffect
以实现相同的结果后,当我第一次看到 Suspanse 这种用法时,我对这种新方法有些怀疑。包装获取库的整个过程有点让人生疑。但是现在,我可以看到它的好处,它非常容易处理加载状态,它抽象掉了一些代码,使其易于重用,并通过消除(好吧,至少在大多数情况下)组件本身的“useEffect”钩子简化了组件的代码,这在以前可是个让人头疼的事情。