允许在子组件完成加载前展示备选方案(fallback),子组件有两种准备状态
- 组件代码没有 ready:Suspense 支持组件懒加载中演示了 Suspense 和 React.lazy 配合支持组件按需加载
- 组件数据没有 ready: 如果子组件中依赖了异步数据,Suspense 可以在异步数据 ready 前展示 fallback,数据 ready 后重新渲染子组件
这次通过一个子组件需要异步获取数据的 demo,学习一下 Suspense 是怎么支持子组件数据 ready 前后的渲染切换的
一个简单的 demo
不使用 Suspense 我们一般自己设置 loading 状态,让组件在等待数据期间展示 fallback UI
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Data fetched!'); // 模拟网络请求
}, 2000);
});
}
// 组件
function MyComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData()
.then(response => {
setData(response);
setLoading(false);
})
}, []);
if (loading) {
return <div>Loading...</div>; // 加载中状态
}
return <div>{data}</div>;
}
function App() {
return (
<div>
<h1>Data Fetching Example</h1>
<MyComponent />
</div>
);
}
使用 Suspense 在等待数据期间展示 fallback UI
Suspense 在等待子组件数据加载期间展示 fallback UI 的原理和动态导入组件的原理类似
- 在第一次执行时候子组件主动 throw 加载数据的 Promise 对象
- Suspense catch 该 Promise 对象,如果状态是 pending 则暂停子组件渲染,并渲染 fallback UI
- 一旦 Promise 对象状态变为 fulfilled,React 重新渲染子组件
let data; // 使用全局变量
function fetchData() {
if (data) return data;
const promise = new Promise(resolve => {
setTimeout(() => {
data = 'data fetched!'
resolve()
}, 2000)
})
throw promise;
}
function Content() {
fetchData();
// 使用全局定义的 data
return <p>{data}</p>
}
function App() {
return (
<>
<h1>Suspense async data</h1>
<Suspense fallback={'loading data...'}>
<Content />
</Suspense>
</>
)
}
fetchData在首次调用时候主动 throw 了 Promise 对象- 该 Promise 对象被 Suspense catch,对该 Promise 对象追加
.then等待其 resolve - React 暂停子组件渲染,显示 fallback UI,等待 Promise 对象 resolve
- 当数据获取到后更新全局 data 变量,然后 resolve Promise 对象
- Promise 对象追加的
.then回调执行,销毁 fallback UI,触发子组件重新渲染
代码使用了全局的 data 对象,非常的不优雅,可以简单调整一下代码
function createResouce(promise) {
let status = 'pending';
let result;
promise
.then((res) => {
result = res;
status = 'fulfilled';
})
.catch((err) => {
result = err;
status = 'rejected';
});
return () => {
if (status === 'pending') {
throw promise;
} else if (status === 'fulfilled') {
return result;
} else if (status === 'rejected') {
throw result;
}
};
}
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('data fetched!');
}, 1000);
});
}
// 需要在 Content 组件外部定义
const getData = createResouce(fetchData());
function Content() {
const data = getData();
return <div>{data}</div>;
}
function App() {
return (
<>
<h1>Suspense async data</h1>
<Suspense fallback={'loading data...'}>
<Content />
</Suspense>
</>
);
}
这样可以解决全局变量的问题,为了防止在重新渲染过程中再次创建 Promise 对象,导致程序死循环,需要在组件外部定义数据获取函数
Jotai 实现
Jotai 是一个支持 Suspense 特性的小巧的状态管理 library,使用 Jotai 管理异步状态特别方便
import { Suspense } from 'react';
import { atom, useAtom } from 'jotai';
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('data fetched!');
}, 1000);
});
}
const dataAtom = atom(async () => {
const data = await fetchData();
return data;
});
function Content() {
const data = useAtom(dataAtom);
return <div>{data}</div>;
}
function App() {
return (
<>
<h1>Jotai with Suspense</h1>
<Suspense fallback={'loading data...'}>
<Content />
</Suspense>
</>
);
}
有了上面的知识会不会已经猜出来 Jotai 怎么实现的了
const use =
ReactExports.use ||
(<T>(
promise: PromiseLike<T> & {
status?: 'pending' | 'fulfilled' | 'rejected'
value?: T
reason?: unknown
},
): T => {
if (promise.status === 'pending') {
throw promise
} else if (promise.status === 'fulfilled') {
return promise.value as T
} else if (promise.status === 'rejected') {
throw promise.reason
} else {
promise.status = 'pending'
promise.then(
(v) => {
promise.status = 'fulfilled'
promise.value = v
},
(e) => {
promise.status = 'rejected'
promise.reason = e
},
)
throw promise
}
})
use
Jotai 的实现最开始有一句判断
const use = ReactExports.use || ...
ReactExports.use是在面向未来兼容 React canary 版本(预发布版本)中原生提供的 use 功能
useis a React API that lets you read the value of a resource like a Promise or context.
来看一下未来的使用方式,首先需要安装 React canary 版本
npm i react@canary react-dom@canary
这时候可以在组件内部创建 promise 了,代码逻辑更加内聚
import { Suspense, use } from 'react';
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('data fetched!');
}, 1000);
});
}
function Content() {
const data = use(fetchData());
return <div>{data}</div>;
}
function App() {
return (
<>
<h1>Suspense async data</h1>
<Suspense fallback={'loading data...'}>
<Content />
</Suspense>
</>
);
}
使用之后可以看到 Console 中会有警告,所以现在还不能在线上环境使用,未来可期
ErrorBoundary 支持
除了 Suspense 外 React 中 ErroBoundary 也是使用同样的原理实现的,只不过 catch 的不是 Promise 而是 Error
创建一个 ErrorBoundary 组件
import React, { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新状态,以便下一个渲染可以展示降级UI
return { hasError: true };
}
componentDidCatch(error, info) {
// 可以将错误报告到错误报告服务
console.error("Error captured by ErrorBoundary: ", error, info);
}
render() {
if (this.state.hasError) {
// 可以渲染任何自定义的降级UI
return <h2>Something went wrong.</h2>;
}
return this.props.children;
}
}
组件获取数据失败,触发 ErrorBoundary 渲染 fallback UI
// ...
function App() {
return (
<>
<h1>Suspense async data</h1>
<ErrorBoundary>
<Suspense fallback={'loading data...'}>
<Content />
</Suspense>
</ErrorBoundary>
</>
);
}