Skip to main content
Version: 5.0

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 describe an asynchronous API.

These define both runtime behaviors, as well as (optionally) typing.

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

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

const fetchTodoDetail = ({ id }: Params): Promise<Todo> =>
fetch(`https://jsonplaceholder.typicode.com/todos/${id}`).then(res =>
res.json(),
);

const todoDetail = new Endpoint(fetchTodoDetail);

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

Add one-line data hookup in the components that need it with useResource()

import { useResource } from 'rest-hooks';

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

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

Async Fallbacks with Boundaries

Unify and reuse loading and error fallbacks with Suspense and NetworkErrorBoundary

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

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

Non-Suspense fallback handling

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.

useFetcher enhances our function, integrating the Rest Hooks store.

import { useFetcher } from 'rest-hooks';

const update = useFetcher(todoUpdate);
return <ArticleForm onSubmit={data => update({ 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 [update, loading, error] = useLoading(useFetcher(todoUpdate));

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.

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 { useResource } from 'rest-hooks';

export default function TodoListComponent() {
const todos = useResource(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, optimisticUpdate 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 optimisticUpdate = (params: Params, body: FormData) => ({
id: params.id,
...body,
});
todoUpdate = todoUpdate.extend({
optimisticUpdate,
});

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 } 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,
optimisticUpdate,
});

const optimisticUpdate = (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.

Instead of defining an Entity, we define a Resource. Resource extends from Entity, so we still need the pk() definiton.

In addition, providing static urlRoot enable 6 Endpoints with easy logic sharing and overrides.

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

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

static urlRoot = 'https://jsonplaceholder.typicode.com/todos';

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

Introduction to Resource

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

// GET https://jsonplaceholder.typicode.com/todos
const todos = useResource(TodoResource.list(), {});

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

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

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

// DELETE https://jsonplaceholder.typicode.com/todos/5
const del = useFetcher(TodoResource.delete());
del({ 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 } = useResource(userDetail, { name: 'Fong' });

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

See this all in action in examples/todo-app

Or a github api demo