Skip to main content
Version: 6.3

Co-locate Data Dependencies

Co-locating data dependencies means we only use data-binding hooks like useSuspense() in components where we display/use their data directly.

import { useSuspense } from 'rest-hooks';
// local directory for API definitions
import { todoDetail } from 'endpoints/todo';

export default function TodoDetail({ id }: { id: number }) {
const todo = useSuspense(todoDetail, { id });
return <div>{todo.title}</div>;
}

useSuspense() guarantees access to data with sufficient freshness. This means it may issue network calls, and it may suspend until the the fetch completes. Param changes will result in accessing the appropriate data, which also sometimes results in new network calls and/or suspends.

  • Fetches are centrally controlled, and thus automatically deduplicated
  • Data is centralized and normalized guaranteeing consistency across uses, even with different endpoints.
    • (For example: navigating to a detail page with a single entry from a list view will instantly show the same data as the list without requiring a refetch.)
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(todoDetail, id ? { id } : null);

Async Fallbacks (loading/error)

This works great if the client already has the data. But while it's waiting on a response from the server, we need some kind of loading indication. Similarly if there is an error in the fetch, we should indicate such. These are called 'fallbacks'.

Boundaries (Suspense/NetworkErrorBoundary)

In React 18, the best way to achieve this is with boundaries. (React 16.3+ supported, but less powerful.) <Suspense /> and <NetworkErrorBoundary /\> are wrapper components which show fallback elements when any component rendered as a descendent is loading or errored while loading their data dependency.

import React, { Suspense } from 'react';
import { NetworkErrorBoundary } from 'rest-hooks';

export default function TodoPage({ id }: { id: number }) {
return (
<AsyncBoundary>
<section>
<TodoDetail id={1} />
<TodoDetail id={5} />
<TodoDetail id={10} />
</section>
</AsyncBoundary>
);
}

interface Props {
fallback: React.ReactElement;
children: React.ReactNode;
}

function AsyncBoundary({ children, fallback = 'loading' }: Props) {
return (
<Suspense fallback={fallback}>
<NetworkErrorBoundary>{children}</NetworkErrorBoundary>
</Suspense>
);
}
note

This greatly simplifies complex orchestrations of data dependencies by decoupling where to show fallbacks from the components using the data.

For instance, here we have three different components requesting different todo data. These will all loading in parallel and only show one loading indicator instead of filling the screen with them. Although this case is obviously contrived; in practice this comes up quite often, especially when data dependencies end up deeply nesting.

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() is provided.

import { useDLE } from 'rest-hooks';
// local directory for API definitions
import { todoDetail } from 'endpoints/todo';

export default function TodoDetail({ id }: { id: number }) {
const {
loading,
error,
data: todo,
} = useDLE(todoDetail, { id });
if (loading) return 'loading';
if (error) return error.status;
return <div>{todo.title}</div>;
}

Subscriptions

When data is likely to change due to external factor; useSubscription() ensures continual updates while a component is mounted.

import { useSuspense } from 'rest-hooks';
// local directory for API definitions
import { todoDetail } from 'endpoints/todo';

export default function TodoDetail({ id }: { id: number }) {
const todo = useSuspense(todoDetail, { id });
useSubscription(todoDetail, { id });
return <div>{todo.title}</div>;
}

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.

const fetchTodoDetail = ({ id }) =>
fetch(`https://jsonplaceholder.typicode.com/todos/${id}`).then(res =>
res.json(),
);
const todoDetail = new Endpoint(
fetchTodoDetail,
{ pollFrequency: 1000 },
);

Live Crypto Price Example

class ExchangeRatesResource extends Resource {
readonly currency: string = 'USD';
readonly rates: Record<string, string> = {};
pk(): string {
return this.currency;
}
static urlRoot = 'https://www.coinbase.com/api/v2/exchange-rates';
static getEndpointExtra() {
return { pollFrequency: 5000 };
}
static list<T extends typeof Resource>(
this: T,
): RestEndpoint<RestFetch<[{ currency: string }]>, { data: T }, undefined> {
return super.list().extend({
schema: { data: this },
});
}
}
function AssetPrice({ symbol }: { symbol: string }) {
const { data: price } = useSuspense(ExchangeRatesResource.list(), {
currency: 'USD',
});
useSubscription(ExchangeRatesResource.list(), {
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