优化React项目中的异步数据处理、提升性能与用户体验
引言
最近和@Bolt ᶘ ᵒᴥᵒᶅ 聊到一些React项目中异步数据的处理优化,结合着搜了一些资料,有些收获,予以记录。
问题
- 对于接口数据,即使使用 类似 swr 的工具,我们也有很多的状态要写在逻辑层进行处理,这种工作非常无趣
import useSWR from "swr";
function App() {
const fetcher = (...args) => fetch(...args).then((res) => res.json());
// fetch data
const { data, error } = useSWR(
"https://jsonplaceholder.typicode.com/todos/1",
fetcher
);
if (error) return <div>failed to load</div>;
if (!data) return <div>loading...</div>;
// render data
return (
<div className="App">
<h2>{data.title}</h2>
<p>UserId: {data.userId}</p>
<p>Id: {data.id}</p>
{data.completed? <p>Completed</p> : <p>Not Completed</p>}
</div>
);
}
export default App;
- 有些接口数据之间存在依赖关系,有些并没有,但业务中我们常常是集中处理接口情况,会造成不必要的等待,无法实现完美的按需 loading 提示。 「懒得写例子了」
useEffect(() => {
Promise.all([a, b, c])
...
})
只消费 a
消费 b、c
方案
新React hook: use
依赖注意
"dependencies": {
"react": "experimental",
"react-dom": "experimental"
},
import { Suspense, useMemo, use } from "react"
function Comp({ data: prosData }) {
const data = use(prosData)
return <div>{JSON.stringify(data)}</div>
}
const mockApi = () =>
new Promise((resolve) => {
setTimeout(() => {
resolve({
name: "John",
})
}, 3000)
})
function App() {
const promiseData = useMemo(() => mockApi(), [])
return (
<Suspense fallback={<div>loading ++</div>}>
<Comp data={promiseData} />
</Suspense>
)
}
export default App
行为描述: use(prosData) 会告知 最近的 Suspense「A」 数据情况,若处于 pending 状态,则走 A 的 fallback 行为 无视常规 hook 要求的规则,如不能 condition
- 缺点
- 处于 experimental 阶段
- 优点
- React 官方支持,用起来非常爽
Router Await
限定下问题:穿入数据为 Promise,自行会 loading 我们不难写出如下通用组件来完成
export function Await(props: AwaitProps) {
const { children, resolve } = props
const [resolved, setResolved] = useState(false)
const [res, setRes] = useState(null)
useEffect(() => {
if (resolve instanceof Promise) {
resolve.then((res) => {
setResolved(true)
setRes(res)
})
} else {
setResolved(true)
}
}, [resolve])
if (!resolved) {
return <div>loading</div>
}
return children(res)
}
// 用法
<Await resolve={comp1Data}>
{(res) => {
return <Comp data={res} />
}}
</Await>
大致是这样的思路,React Router 已经干的很好了
import { Await, useLoaderData } from "react-router-dom";
function Book() {
const { book, reviews } = useLoaderData();
return (
<div>
<h1>{book.title}</h1>
<p>{book.description}</p>
<React.Suspense fallback={<ReviewsSkeleton />}>
<Await
resolve={reviews}
errorElement={
<div>Could not load reviews 😬</div>
}
children={(resolvedReviews) => (
<Reviews items={resolvedReviews} />
)}
/>
</React.Suspense>
</div>
);
}
declare function Await(
props: AwaitProps
): React.ReactElement;
interface AwaitProps {
children: React.ReactNode | AwaitResolveRenderFunction;
errorElement?: React.ReactNode;
resolve: TrackedPromise | any;
}
interface AwaitResolveRenderFunction {
(data: Awaited<any>): React.ReactElement;
}
Note: <Await>
expects to be rendered inside of a <React.Suspense>
or <React.SuspenseList>
parent to enable the fallback UI.
和自己实现的弱鸡版本相比,React Router 的 Await 可以利用 React 的 Suspense
你可能会好奇,咋做到通知 Suspense 的呢?
对于这个问题,推荐看看这篇文章 我摘要部分
- Suspense: 可以看做是 react 提供用了加载数据的一个标准,当加载到某个组件时,如果该组件本身或者组件需要的数据是未知的,需要动态加载,此时就可以使用 Suspense。Suspense 提供了
加载 -> 过渡 -> 完成后切换
这样一个标准的业务流程。 - lazy: lazy 是在 Suspense 的标准下,实现的一个动态加载的组件的工具方法。
与 lazy 同理 React Router 中的 Await 实现遵循了 Suspense 加载数据的标准,利用在 pending 状态下,向外 throw promise 完成对 Suspense 的告知
// 模拟请求 promise
function mockApi(){
return delay(5000).then(() => "data fetched")
}
// 处理请求状态变更
function fetchData(){
let status = "uninit"
let data = null
let promise = null
return () => {
switch(status){
// 初始状态,发出请求并抛出 promise
case "uninit": {
const p = mockApi()
.then(x => {
status = "resolved"
data = x
})
status = "loading"
promise = p
throw promise
};
// 加载状态,直接抛出 promise
case "loading": throw promise;
// 如果加载完成直接返回数据
case "resolved": return data;
default: break;
}
}
}
const reader = fetchData()
function TestDataLoad(){
const data = reader()
return (
<p>{data}</p>
)
}
function App() {
const [count, setCount] = useState(1)
useEffect(() => {
setInterval(() => setCount(c => c > 100 ? c: c + 1), 1000)
}, [])
return (
<>
<Suspense fallback={"loading"}>
<TestDataLoad/>
</Suspense>
<p>count: {count}</p>
</>
)
}
- 优点
- 现在就能用上
- 缺点
- 还是没有 use 用起来优雅
总结
业务使用中,可以把业务数据请求结果抽到合理的上层,向下传递 Promise 合理放置 Suspense,减少不必要的 rerender
其他
2021年12月9日 React 18 Keynote 就提过本文讲的内容 因为 use 这个在官方文档还找不到,推荐去 rfcs 里看下出处
转载自:https://juejin.cn/post/7247151913438986277