Skip to main content
Version: 6.6

Introduction

Rest Hooks is an asynchronous data framework for TypeScript and JavaScript. While it is completely protocol and platform agnostic, it is not a networking stack for things like minecraft game servers.

A good way to tell if this could be useful is if you use something similar to any of the following to build data-driven applications:

Rest Hooks focuses on solving the following challenges in a declarative composable manner

  • Asynchronous behavior and race conditions
  • Global consistency and integrity of dynamic data
  • High performance at scale

Endpoint

Endpoints make it easy to share and reuse strongly typed APIs

Protocol implementations make definitions a breeze by building off the basic Endpoint. For protocols not shipped here, feel free to extend Endpoint directly.

import { RestEndpoint } from '@rest-hooks/rest';

const getTodo = new RestEndpoint({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
});

By decoupling endpoint definitions from their usage, we are able to reuse them in many contexts.

  • Easy reuse in different components eases co-locating data dependencies
  • Reuse with different hooks allows different behaviors with the same endpoint
  • Reuse across different platforms like React Native, React web, or even beyond React in Angular, Svelte, Vue, or Node
  • Published as packages independent of their consumption

Co-locate data dependencies

Bind the data where you need it with the one-line useSuspense()

import { useSuspense } from 'rest-hooks';

export default function TodoDetail({ id }: { id: number }) {
const todo = useSuspense(getTodo, { id });

return <div>{todo.title}</div>;
}
render(<TodoDetail id={1} />);
  • Avoid prop drilling
  • Data updates only re-render components that need to

Async Fallbacks with Boundaries

Unify and reuse loading and error fallbacks with AsyncBoundary

import { Suspense } from 'react';
import { AsyncBoundary } from 'rest-hooks';

function App() {
return (
<AsyncBoundary fallback="loading">
<AnotherRoute />
<TodoDetail id={5} />
</AsyncBoundary>
);
}

Non-Suspense fallback handling can also be used for certain cases in React 16 and 17

Mutations

todoUpdate
import { Endpoint } from '@rest-hooks/endpoint';

interface Todo {
userId: number;
id: number;
title: string;
completed: boolean;
}
interface Params {
id: number;
}

const fetchTodoUpdate = ({ id }: Params, body: FormData): Promise<Todo> =>
fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'PATCH',
body,
}).then(res => res.json());

const todoUpdate = new Endpoint(fetchTodoUpdate, { sideEffect: true });

Instead of just calling the todoUpdate endpoint with our data, we want to ensure all co-located usages of the todo being edited are updated. This avoid both the complexity and performance problems of attempting to cascade endpoint refreshes.

useController gives us access to the Rest Hooks Controller, which is used to trigger imperative actions like fetch.

import { useController } from 'rest-hooks';

const { fetch } = useController();
return <ArticleForm onSubmit={data => fetch(todoUpdate, { id }, data)} />;
Tracking imperative loading/error state

useLoading() enhances async functions by tracking their loading and error states.

import { useLoading } from '@rest-hooks/hooks';

const { fetch } = useController();
const [update, loading, error] = useLoading(
data => fetch(todoUpdate, { id }, data),
[fetch],
);
return <ArticleForm onSubmit={update} />;

However, there is still one issue. Our todoUpdate and todoDetail endpoint are not aware of each other so how can Rest Hooks know to update todoDetail with this data?

Entities

Adding Entities to our endpoint definition tells Rest Hooks how to extract and find a given piece of data no matter where it is used. The pk() (primary key) method is used as a key in a lookup table.

This enables a DRY storage pattern, which prevents 'data tearing' jank and improves performance.

import { Entity } from '@rest-hooks/endpoint';

export class Todo extends Entity {
readonly userId: number = 0;
readonly id: number = 0;
readonly title: string = '';
readonly completed: boolean = false;

pk() {
return `${this.id}`;
}
}

Schema

What if our entity is not the top level item? Here we define the todoList endpoint with [Todo] as its schema. Schemas tell Rest Hooks where to find the Entities. By placing inside a list, Rest Hooks knows to expect a response where each item of the list is the entity specified.

import { Endpoint } from '@rest-hooks/endpoint';

const fetchTodoList = (params: any) =>
fetch(`https://jsonplaceholder.typicode.com/todos/`).then(res => res.json());

const todoList = new Endpoint(fetchTodoList, {
schema: [Todo],
sideEffect: true,
});

Schemas also automatically infer and enforce the response type, ensuring the variable todos will be typed precisely. If the API responds in another manner the hook with throw instead, triggering the error fallback specified in <NetworkErrorBoundary />

import { useSuspense } from 'rest-hooks';

export default function TodoListComponent() {
const todos = useSuspense(todoList, {});

return (
<div>
{todos.map(todo => (
<TodoListItem key={todo.pk()} todo={todo} />
))}
</div>
);
}

This also guarantees data consistency (as well as referential equality) between todoList and todoDetail endpoints, as well as any mutations that occur.

Optimistic Updates

By using the response of the mutation call to update the Rest Hooks store, we were able to keep our components updated automatically and only after one request.

However, after toggling todo.completed, this is just too slow! No worries, getOptimisticResponse tells Rest Hooks what response it expects to receive from the mutation call, Rest Hooks can immediately update all components using the relevant entity.

const getOptimisticResponse = (
snap: SnapshotInterface,
params: Params,
body: FormData,
) => ({
id: params.id,
...body,
});
todoUpdate = todoUpdate.extend({
getOptimisticResponse,
});

Rest Hooks ensures data integrity against any possible networking failure or race condition, so don't worry about network failures, multiple mutation calls editing the same data, or other common problems in asynchronous programming.

todoUpdate
import { Endpoint, SnapshotInterface } from '@rest-hooks/endpoint';

interface Params {
id: number;
}

const fetchTodoUpdate = ({ id }: Params, body: FormData) =>
fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'PATCH',
body,
}).then(res => res.json());

const todoUpdate = new Endpoint(fetchTodoUpdate, {
sideEffect: true,
schema: Todo,
getOptimisticResponse,
});

const getOptimisticResponse = (
snap: SnapshotInterface,
params: Params,
body: FormData,
) => ({
id: params.id,
...body,
});

Protocol specific patterns

At this point we've defined todoDetail, todoList and todoUpdate. You might have noticed that these endpoint definitions share some logic and information. For this reason Rest Hooks encourages extracting shared logic among endpoints.

@rest-hooks/rest

One common pattern is having endpoints Create Read Update Delete (CRUD) for a given resource. Using @rest-hooks/rest (docs) simplifies these patterns.

RestEndpoint extends Endpoint simplifying HTTP patterns.

createResource takes this one step further by creating 6 Endpoints with easy logic sharing and overrides.

import { Entity, createResource } from '@rest-hooks/rest';

class Todo extends Entity {
readonly id: number = 0;
readonly userId: number = 0;
readonly title: string = '';
readonly completed: boolean = false;

pk() {
return `${this.id}`;
}
}

const TodoResource = createResource({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
});

Introduction to Resource

Resource Endpoints
// read
// GET https://jsonplaceholder.typicode.com/todos/5
const todo = useSuspense(TodoResource.get, { id: 5 });

// GET https://jsonplaceholder.typicode.com/todos
const todos = useSuspense(TodoResource.getList);

// mutate
// POST https://jsonplaceholder.typicode.com/todos
const controller = useController();
controller.fetch(TodoResource.create, { title: 'my todo' });

// PUT https://jsonplaceholder.typicode.com/todos/5
const controller = useController();
controller.fetch(TodoResource.update, { id: 5 }, { title: 'my todo' });

// PATCH https://jsonplaceholder.typicode.com/todos/5
const controller = useController();
controller.fetch(TodoResource.partialUpdate, { id: 5 }, { title: 'my todo' });

// DELETE https://jsonplaceholder.typicode.com/todos/5
const controller = useController();
controller.fetch(TodoResource.delete, { id: 5 });

@rest-hooks/graphql

GraphQL support ships in the @rest-hooks/graphql (docs) package.

import { GQLEntity, GQLEndpoint } from '@rest-hooks/graphql';

class User extends GQLEntity {
readonly name: string = '';
readonly email: string = '';
}

const gql = new GQLEndpoint('https://nosy-baritone.glitch.me');

const userDetail = gql.query(
`query UserDetail($name: String!) {
user(name: $name) {
id
name
email
}
}`,
{ user: User },
);
const { user } = useSuspense(userDetail, { name: 'Fong' });

@rest-hooks/img

A simple ArrayBuffer can be easily achieved using @rest-hooks/endpoint directly

import { Endpoint } from '@rest-hooks/endpoint';

export const getPhoto = new Endpoint(async ({ userId }: { userId: string }) => {
const response = await fetch(`/users/${userId}/photo`);
const photoArrayBuffer = await response.arrayBuffer();

return photoArrayBuffer;
});

@rest-hooks/img integrates images with Suspense as well as the render as you fetch pattern for improved user experience.

Debugging

redux-devtools

Add the Redux DevTools for chrome extension or firefox extension

Click the icon to open the inspector, which allows you to observe dispatched actions, their effect on the cache state as well as current cache state.

Mock data

Writing FixtureEndpoints is a standard format that can be used across all @rest-hooks/test helpers as well as your own uses.

import type { FixtureEndpoint } from '@rest-hooks/test';
import { todoDetail } from './todo';

const todoDetailFixture: FixtureEndpoint = {
endpoint: todoDetail,
args: [{ id: 5 }] as const,
response: {
id: 5,
title: 'Star Rest Hooks on Github',
userId: 11,
completed: false,
},
};

Demo

Todo Demo

Open demo in own tab

Explore on github

Github Demo

Open demo in own tab

Explore on github