Co-locate Data Dependencies
Co-locating data dependencies means we only use data-binding hooks like useResource() in components where we display/use their data directly.
- Single
- List
import { useResource } from 'rest-hooks';
// local directory for API definitions
import { todoDetail } from 'endpoints/todo';
export default function TodoDetail({ id }: { id: number }) {
const todo = useResource(todoDetail, { id });
return <div>{todo.title}</div>;
}
import { useResource } from 'rest-hooks';
// local directory for API definitions
import { todoList } from 'endpoints/todo';
export default function TodoList() {
const todos = useResource(todoList, {});
return (
<section>
{todos.map(todo => (
<div key={todo.id}>{todo.title}</div>
))}
</section>
);
}
useResource() 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.)
Use null
as the second argument on any rest hooks to indicate "do nothing."
// todo could be undefined if id is undefined
const todo = useResource(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.
- TypeScript
- JavaScript
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>
);
}
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>
);
}
function AsyncBoundary({ children, fallback = 'loading' }: Props) {
return (
<Suspense fallback={fallback}>
<NetworkErrorBoundary>{children}</NetworkErrorBoundary>
</Suspense>
);
}
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, the @rest-hooks/legacy
package includes
a hook that uses stateful loading and errors.
import { useStatefulResource } from '@rest-hooks/legacy';
// local directory for API definitions
import { todoDetail } from 'endpoints/todo';
export default function TodoDetail({ id }: { id: number }) {
const {
loading,
error,
data: todo,
} = useStatefulResource(todoDetail, { id });
if (loading) return 'loading';
if (error) return error.status;
return <div>{todo.title}</div>;
}
Read more about useStatefulResource
Subscriptions
When data is likely to change due to external factor; useSubscription() ensures continual updates while a component is mounted.
- Single
- List
import { useResource } from 'rest-hooks';
// local directory for API definitions
import { todoDetail } from 'endpoints/todo';
export default function TodoDetail({ id }: { id: number }) {
const todo = useResource(todoDetail, { id });
useSubscription(todoDetail, { id });
return <div>{todo.title}</div>;
}
import { useResource } from 'rest-hooks';
// local directory for API definitions
import { todoList } from 'endpoints/todo';
export default function TodoList() {
const todos = useResource(todoList, {});
useSubscription(todoList, {});
return (
<section>
{todos.map(todo => (
<div key={todo.id}>{todo.title}</div>
))}
</section>
);
}
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 },
);