Rendering Asynchronous Data
Make your components reusable by binding the data where you need it with the one-line useSuspense(), which guarantees data like await.
- Single
- List
import { useSuspense } from '@rest-hooks/react';import { TodoResource } from './api/Todo';function TodoDetail({ id }: { id: number }) {const todo = useSuspense(TodoResource.get, { id });return <div>{todo.title}</div>;}render(<TodoDetail id={1} />);
import { useSuspense } from '@rest-hooks/react';import { TodoResource } from './api/Todo';function TodoList() {const todos = useSuspense(TodoResource.getList);return (<section style={{ maxHeight: '300px', overflow: 'scroll' }}>{todos.map(todo => (<div key={todo.id}>{todo.title}</div>))}</section>);}render(<TodoList />);
No more prop drilling, or cumbersome external state management. Rest Hooks guarantees global referential equality, data safety and performance.
Co-location also allows Server Side Rendering to incrementally stream HTML, greatly reducing TTFB. Rest Hooks SSR automatically hydrates its store, allowing immediate interactive mutations with zero client-side fetches on first load.
Use null
as the second argument on any rest hooks to indicate "do nothing."
// todo could be undefined if id is undefined
const todo = useSuspense(TodoResource.get, id ? { id } : null);
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 /> at or above navigational boundaries like pages, routes or modals.
import React, { Suspense } from 'react';
import { AsyncBoundary } from '@rest-hooks/react';
export default function TodoPage({ id }: { id: number }) {
return (
<AsyncBoundary>
<section>
<TodoDetail id={1} />
<TodoDetail id={5} />
<TodoDetail id={10} />
</section>
</AsyncBoundary>
);
}
useTransition powered routers or navigation means React never has to show a loading fallback. Of course, these are only possible in React 18 or above, so for 16 and 17 this will merely centralize the fallback, eliminating 100s of loading spinners.
In either case, a signficiant amount of component complexity is removed by centralizing fallback conditionals.
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 '@rest-hooks/react';import { TodoResource } from './api/Todo';function TodoDetail({ id }: { id: number }) {const { loading, error, data: todo } = useDLE(TodoResource.get, { id });if (loading || !todo) return <div>loading</div>;if (error) return <div>{error.message}</div>;return <div>{todo.title}</div>;}render(<TodoDetail id={1} />);
This downside of useDLE vs useSuspense is more loading and error handling code and potentially a much worse user experience.
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.
- Single
- List
import { useLive } from '@rest-hooks/react';import { TodoResource } from './api/Todo';function TodoDetail({ id }: { id: number }) {const todo = useLive(TodoResource.get, { id });return <div>{todo.title}</div>;}render(<TodoDetail id={1} />);
import { useLive } from '@rest-hooks/react';import { TodoResource } from './api/Todo';function TodoList() {const todos = useLive(TodoResource.getList);return (<section style={{ maxHeight: '300px', overflowY: 'scroll' }}>{todos.map(todo => (<div key={todo.id}>{todo.title}</div>))}</section>);}render(<TodoList />);
Subscriptions are orchestrated by Managers. Out of the box, polling based subscriptions can be used by adding pollFrequency to an endpoint. For pushed based networking protocols like websockets, see the example websocket stream manager.
export const TodoResource = createResource({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
schema: Todo,
pollFrequency: 10000,
});
Live Crypto Price Example
export class ExchangeRates extends Entity {readonly currency: string = 'USD';readonly rates: Record<string, string> = {};pk(): string {return this.currency;}}export const getExchangeRates = new RestEndpoint({urlPrefix: 'https://www.coinbase.com/api/v2',path: '/exchange-rates',searchParams: {} as { currency: string },schema: { data: ExchangeRates },pollFrequency: 15000,});
import { useLive } from '@rest-hooks/react';import { getExchangeRates } from './api/ExchangeRates';function AssetPrice({ symbol }: { symbol: string }) {const { data: price } = useLive(getExchangeRates, { currency: 'USD' });const displayPrice = new Intl.NumberFormat('en-US', {style: 'currency',currency: 'USD',}).format(1 / Number.parseFloat(price.rates[symbol]));return (<span>{symbol} {displayPrice}</span>);}render(<AssetPrice symbol="BTC" />);