Skip to main content

Suspense

Suspense

React Suspense 是一个内置的功能,简化了在 React 应用程序中管理异步操作。与 Axios 这样的数据获取库或 Redux 这样的状态管理工具不同,Suspense 专注于管理在组件等待异步任务完成时显示的内容。

工作原理

当 React 遇到一个 Suspense 组件时,它会检查是否有任何子组件在等待一个 Promise 解决。如果是,React 会挂起这些组件的渲染,并显示一个后备 UI,例如加载旋转器或消息,直到 Promise 被解决。

<Suspense fallback={<div>Loading books...</div>}>
<Books />
</Suspense>

React Suspense 还通过允许逐步渲染应用程序的部分内容来增强服务器端渲染(SSR)。通过 SSR,可以使用 renderToPipeableStream 首先加载页面的基本部分,并在其余部分可用时逐步加载。Suspense 在此过程中管理后备选项,提高性能、用户体验和 SEO。

数据获取模式

当一个 React 组件需要从 API 获取数据时,有三种常见的数据获取模式:

  • 渲染时获取。
  • 获取后渲染。
  • 边获取边渲染(React Suspense 所支持的)。
渲染时获取

此模式中,网络请求是在组件挂载后触发的。这种模式虽然简单,但是可能会导致性能问题,特别是当嵌套组件发出类似请求时。

const UserProfile = () => {
const [user, setUser] = useState(null);

useEffect(() => {
fetch('/api/user')
.then((response) => response.json())
.then((data) => setUser(data));
}, []);

if (!user) return <p>Loading user profile...</p>;

return (
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
};

这种方法可能导致网络瀑布效应,其中每个后续组件都在等待前一个组件获取数据,从而导致延迟。

useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => setUser(data));
}, []);

useEffect(() => {
if (user) {
fetch(`/api/posts?userId=${user.id}`)
.then(res => res.json())
.then(data => setPosts(data));
}
}, [user]);

如上:

  • 第一个 useEffect 加载用户数据,设置 user
  • 第二个 useEffect 依赖 user,因此必须等 user 设置完之后才触发。

两个请求变成了 先 user,再 posts,串行执行,总耗时较长。

获取后渲染

此模式在组件挂载之前发起网络请求,确保数据在组件渲染时可用。这种模式有助于避免在渲染时方法中出现的网络瀑布问题。

const fetchUserData = () => {
return fetch('/api/user').then((response) => response.json());
};

const App = () => {
const [user, setUser] = useState(null);

useEffect(() => {
fetchUserData().then((data) => setUser(data));
}, []);

if (!user) return <p>Loading user data...</p>;

return (
<div>
<UserProfile user={user} />
</div>
);
};

const UserProfile = ({ user }) => (
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);

如上,fetchUserData 函数在 UserProfile 挂载之前被调用,数据在 useEffect 钩子中设置。加载状态通过检查 user 数据是否可用来进行类似的管理。

这种模式提前开始获取数据,但仍然在渲染有用数据之前等待所有承诺被解决,如果某个请求较慢,可能会导致延迟。

边获取边渲染

React Suspense 引入了边获取边渲染的模式,允许组件在发起网络请求后立即渲染。通过在数据可用时立即渲染 UI 元素来改善用户体验,而无需等待所有数据被获取。

const fetchUserData = () => {
let data;
let promise = fetch('/api/user')
.then((response) => response.json())
.then((json) => {
data = json;
});

return {
read() {
if (!data) {
throw promise;
}
return data;
},
};
};

const resource = fetchUserData();

const App = () => (
<Suspense fallback={<p>Loading user profile...</p>}>
<UserProfile />
</Suspense>
);

const UserProfile = () => {
const user = resource.read();
return (
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
};

这种模式允许每个组件独立管理其加载状态,从而减少等待时间并提高应用程序的整体响应能力。

应用场景

数据获取

Suspense 的主要用例之一是管理应用程序中的数据获取。使用 Suspense,可以在从 API 获取数据时显示加载状态,从而提供更流畅的用户体验。

React.lazy

Suspense 与 React 的 lazy() 函数无缝协作,仅在需要时加载组件,从而减少应用程序的初始加载时间。这对于大型应用程序尤其有用,因为并非所有组件都需要立即加载。

const LazyComponent = React.lazy(() => import('./LazyComponent'));

const App = () => (
<Suspense fallback={<p>Loading component...</p>}>
<LazyComponent />
</Suspense>
);
多异步操作

Suspense 可以管理多个异步操作,确保 UI 的每个部分独立显示其加载状态。这在应用程序的不同部分从不同来源获取数据的场景中非常有用。

const userResource = fetchUserData();
const postsResource = fetchPostsData();

const App = () => (
<div>
<Suspense fallback={<p>Loading user...</p>}>
<UserProfile />
</Suspense>
<Suspense fallback={<p>Loading posts...</p>}>
<Posts />
</Suspense>
</div>
);
顺序渲染

Suspense 还可以嵌套,使组件以使用 Suspense 管理渲染顺序:

const App = () => (
<div>
<Suspense fallback={<p>Loading user profile...</p>}>
<UserProfile />
<Suspense fallback={<p>Loading posts...</p>}>
<Posts />
</Suspense>
</Suspense>
</div>
);
SSR

Suspense 可以通过指定应用程序的哪些部分应该在服务器上渲染,哪些部分应该等到客户端有更多数据时再渲染,从而改善 SSR。这可以显著提升 web 应用程序的性能和 SEO。

import { renderToPipeableStream } from 'react-dom/server';

const App = () => (
<Suspense fallback={<p>Loading...</p>}>
<MainComponent />
</Suspense>
);

const { pipe } = renderToPipeableStream(<App />);

错误边界

错误边界可以确保应用程序能够处理错误。这个组件会捕捉在数据获取或渲染过程中出现的任何错误,防止整个应用程序崩溃,并提供用户友好的错误消息。

import { Component } from 'react';

class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static defaultProps = {
fallback: <h1>Something went wrong.</h1>,
};

static getDerivedStateFromError(error) {
return { hasError: true };
}

componentDidCatch(error, errorInfo) {
console.log(error, errorInfo);
}

render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}

}
export default ErrorBoundary;

使用错误边界:

import { Suspense, lazy } from 'react';

import Posts from './components/Posts';
import PostSkeleton from './components/PostSkeleton';
import ErrorBoundary from './components/ErrorBoundary';

const App = () => {
return (
<ErrorBoundary fallback={<div>Failed to fetch data!</div>}>
<Suspense fallback={<PostSkeleton />}>
<Posts />
</Suspense>
</ErrorBoundary>
);
};

export default App;

在这种情况下,即使 Posts 组件在渲染过程中出错,错误边界会捕获错误并给出备用 UI,这确保了即使在后台出现问题时,用户体验也会更好。