Skip to main content

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.

export class Todo extends Entity {
id = 0;
userId = 0;
title = '';
completed = false;
pk() {
return `${this.id}`;
}
}
export const TodoResource = createResource({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
schema: Todo,
});
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} />);
Live Preview
Loading...
Store
Endpoints used in many contextsEndpoints used in many contexts

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.

Conditional Dependencies

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.

export class Todo extends Entity {
id = 0;
userId = 0;
title = '';
completed = false;
pk() {
return `${this.id}`;
}
}
export const TodoResource = createResource({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
schema: Todo,
});
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} />);
Live Preview
Loading...
Store

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.

export class Todo extends Entity {
id = 0;
userId = 0;
title = '';
completed = false;
pk() {
return `${this.id}`;
}
}
export const TodoResource = createResource({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
schema: Todo,
pollFrequency: 10000,
});
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} />);
Live Preview
Loading...
Store

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

api/ExchangeRates
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,
});
AssetPrice
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" />);
Live Preview
Loading...
Store