Rendering Asynchronous Data
Make your components reusable by binding the data where you use it with the one-line useSuspense(), which guarantees data like 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> ); }
Do not prop drill. Instead, useSuspense() in the components that render the data from it. This is known as data co-location.
Instead of writing complex update functions or invalidations cascades, Reactive Data Client automatically updates bound components immediately upon data change. This is known as reactive programming.
Loading and Error
You might have noticed the return type shows the value is always there. useSuspense() operates very much like await. This enables us to make error/loading disjoint from data usage.
Async Boundaries
Instead we place <AsyncBoundary /> to handling loading and error conditions at or above navigational boundaries like pages, routes, or modals.
- 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';
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>
);
}
import { AsyncBoundary } from '@data-client/react';
import { Button, Modal } from 'antd';
export default function ModalOpen() {
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's useTransition and Server Side Rendering powered routers or navigation means never seeing a loading fallback again. In React 16 and 17 fallbacks can be centralized to eliminate redundant loading indicators while keeping components reusable.
<AsyncBoundary /> also allows Server Side Rendering to incrementally stream HTML, greatly reducing TTFB. Reactive Data Client SSR's automatic store hydration means immediate user interactivity with zero client-side fetches on first load.
AsyncBoundary's error fallback and loading fallback can both be customized.
Stateful
You may find cases where it's still useful to use a stateful approach to fallbacks when using React 16 and 17. For these cases, or compatibility with some component libraries, useDLE() - [D]ata [L]oading [E]rror - is provided.
import { useDLE } from '@data-client/react'; import { ProfileResource } from './ProfileResource'; function ProfileList(): 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 />);
Since useDLE does not useSuspense, you won't be able to easily centrally orchestrate loading and error code. Additionally, React 18 features like useTransition, and incrementally streaming SSR won't work with components that use it.
Conditional
Use null
as the second argument to any Data Client hook means "do nothing."
// todo could be undefined if id is undefined
const todo = useSuspense(TodoResource.get, id ? { id } : null);
Subscriptions
When data is likely to change due to external factor; useSubscription() ensures continual updates while a component is mounted. useLive() calls both useSubscription() and useSuspense(), making it quite easy to use fresh data.
import { useLive } from '@data-client/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" />);
Subscriptions are orchestrated by Managers. Out of the box, polling based subscriptions can be used by adding pollFrequency to an Endpoint or Resource. For pushed based networking protocols like SSE and websockets, see the example stream manager.
export const getTicker = new RestEndpoint({
urlPrefix: 'https://api.exchange.coinbase.com',
path: '/products/:productId/ticker',
schema: Ticker,
pollFrequency: 2000,
});