渲染异步数据
只需一行 useSuspense(),就能在使用数据的地方绑定数据,让你的组件可复用;它能 像 await 一样保证数据的存在。
import { useSuspense } from '@data-client/react'; import PostItem from './PostItem'; import { PostResource } from './Resources'; export default function PostList({ setRoute }) { const posts = useSuspense(PostResource.getList); return ( <div> {posts.map(post => ( <PostItem key={post.pk()} post={post} setRoute={setRoute} /> ))} </div> ); }

不要进行 prop 逐层传递。而是在渲染数据的组件中直接调用 useSuspense()。这被称为 数据就近放置(data co-location)。
不要把数据绑定 hook 藏在自定义 hook 中。而应把紧密耦合的数据转换放在 Query 中——数据逻辑应当与数据模型放在一起,这样它保持可见、可复用,并且可以独立于视图自由修改。
Reactive Data Client 会在数据变化时立即自动更新绑定的组件,无需编写复杂的更新函数或级联失效逻辑。这被称为 响应式编程。
加载与错误
你可能已经注意到,返回类型表明这个值总是存在的。useSuspense() 的工作方式非常像 await。这让我们可以把错误和加载的处理与数据的使用分离开来。
异步边界
我们会在**页面、路由或模态框**等导航边界处或其上层放置 <AsyncBoundary />,用来处理加载和错误状态。
- React Router
- NextJS
- Expo
- Antd Modal
import { AsyncBoundary } from '@data-client/react';
import { Outlet } from 'react-router';
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<section>
<AsyncBoundary>
<Outlet />
</AsyncBoundary>
</section>
</div>
);
}
import { AsyncBoundary } from '@data-client/react';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<h1>Dashboard</h1>
<section>
<AsyncBoundary>{children}</AsyncBoundary>
</section>
</div>
);
}
import { AsyncBoundary } from '@data-client/react';
import { Slot } from 'expo-router';
import { Image, StyleSheet } from 'react-native';
import ParallaxScrollView from '@/components/ParallaxScrollView';
export default function DashboardLayout() {
return (
<ParallaxScrollView
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
headerImage={
<Image
source={require('@/assets/images/my-logo.png')}
style={styles.logo}
/>
}
>
<AsyncBoundary>
<Slot />
</AsyncBoundary>
</ParallaxScrollView>
);
}
const styles = StyleSheet.create({
logo: { height: 178, width: 290 },
});
import { AsyncBoundary } from '@data-client/react';
import { useState } from 'react';
import { Button, Modal } from 'antd';
import MyModalBody from './MyModalBody';
export default function ModalOpen() {
const [isModalOpen, setIsModalOpen] = useState(false);
const showModal = () => setIsModalOpen(true);
const handleOk = () => setIsModalOpen(false);
const handleCancel = () => setIsModalOpen(false);
return (
<>
<Button type="primary" onClick={showModal}>
Open Modal
</Button>
<Modal title="Basic Modal" open={isModalOpen} onOk={handleOk} onCancel={handleCancel}>
<AsyncBoundary>
<MyModalBody />
</AsyncBoundary>
</Modal>
</>
);
}
借助 React 18 的 useTransition 和基于服务端渲染的路由或导航,你将再也看不到加载 fallback。在 React 16 和 17 中,可以集中管理 fallback,在保持组件可复用的同时消除冗余的加载指示器。
<AsyncBoundary /> 还能让服务端渲染以增量方式流式传输 HTML,大幅降低 TTFB。Reactive Data Client SSR 的自动 store 注水意味着首次加载时用户可以立即交互,且客户端获取次数为零。
AsyncBoundary 的错误 fallback 和加载 fallback 都可以定制。
有状态的方式
在某些情况下,你可能会发现使用有状态的方式处理 fallback 仍然有用(在使用 React 16 和 17 时)。对于这些情况,或者为了兼容某些组件库,我们提供了 useDLE() - [D]ata [L]oading [E]rror。
import React from 'react'; import { useDLE } from '@data-client/react'; import { ProfileResource } from './ProfileResource'; function ProfileList(): React.JSX.Element { const { data, loading, error } = useDLE(ProfileResource.getList); if (error) return <div>Error {`${error.status}`}</div>; if (loading || !data) return <Loading />; return ( <div> {data.map(profile => ( <div className="listItem" key={profile.pk()}> <Avatar src={profile.avatar} /> <div> <h4>{profile.fullName}</h4> <p>{profile.bio}</p> </div> </div> ))} </div> ); } render(<ProfileList />);
由于 useDLE 不使用 useSuspense,你将无法轻松地集中编排加载和错误处理的 代码。此外,React 18 的特性,例如 useTransition 和增量流式 SSR,在使用它的组件中将无法工作。
条件获取
将 null 作为任意 Data Client hook 的第二个参数,表示“什么都不做”。
// todo could be undefined if id is undefined
const todo = useSuspense(TodoResource.get, id ? { id } : null);
订阅
当数据可能因外部因素而发生变化时,useSubscription() 可以确保组件挂载期间持续更新。useLive() 会同时调用 useSubscription() 和 useSuspense(),让你能够非常轻松地使用最新数据。
import { useLive } from '@data-client/react'; import NumberFlow from '@number-flow/react'; import { getTicker } from './Ticker'; function AssetPrice({ productId }: Props) { const ticker = useLive(getTicker, { productId }); return ( <center> {productId}{' '} <NumberFlow value={ticker.price} format={{ style: 'currency', currency: 'USD' }} /> </center> ); } interface Props { productId: string; } render(<AssetPrice productId="BTC-USD" />);
订阅由 Managers 编排。开箱即用地,只需在 Endpoint 或 Resource 上添加 pollFrequency,就可以使用基于轮询的订阅。对于 SSE 和 websockets 这类基于推送的网络协议,请参阅数据流 manager 示例。
export const getTicker = new RestEndpoint({
urlPrefix: 'https://api.exchange.coinbase.com',
path: '/products/:productId/ticker',
schema: Ticker,
pollFrequency: 2000,
});