Skip to main content

A simple data fetch

Add a single useSuspense() call where you need its data.

Rest Hooks automatically optimizes performance by caching the results, deduplicating fetches, efficient component render bindings and more.

Editor
import { Endpoint } from '@rest-hooks/endpoint';
interface Todo {
id: number;
userId: number;
title: string;
completed: boolean;
}
const fetchTodo = (id: string | number): Promise<Todo> =>
fetch(`https://jsonplaceholder.typicode.com/todos/${id}`).then(res =>
res.json(),
);
export const getTodo = new Endpoint(fetchTodo);
import { getTodo } from './api';
function TodoDetail({ id }: { id: number }) {
const todo = useSuspense(getTodo, id);
return <div>{todo.title}</div>;
}
render(<TodoDetail id={1} />);
Live Preview
Loading...
Store
Editor
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 { TodoResource } from './api';
function TodoDetail({ id }: { id: number }) {
const todo = useSuspense(TodoResource.get, { id });
return <div>{todo.title}</div>;
}
render(<TodoDetail id={1} />);
Live Preview
Loading...
Store
Editor
import { GQLEndpoint } from '@rest-hooks/graphql';
const gql = new GQLEndpoint('/');
export const getTodo = gql.query(`query GetTodo($id: ID!) {
todo(id: $id) {
id
title
completed
}
}
`);
import { getTodo } from './api';
function TodoDetail({ id }: { id: number }) {
const { todo } = useSuspense(getTodo, { id });
return <div>{todo.title}</div>;
}
render(<TodoDetail id={1} />);
Live Preview
Loading...
Store

Stateful mutations

Use Controller.fetch() to update the store.

This updates all usages atomically and immediately with zero additional fetches. Rest Hooks automatically ensures data consistency and integrity globally.

Editor
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,
optimistic: true,
});
import { TodoResource } from './api';
function TodoDetail({ id }: { id: number }) {
const todo = useSuspense(TodoResource.get, { id });
const controller = useController();
const updateWith = title => () =>
controller.fetch(
TodoResource.partialUpdate,
{ id },
{ title }
);
return (
<div>
<div>{todo.title}</div>
<button onClick={updateWith('🥑')}>🥑</button>
<button onClick={updateWith('💖')}>💖</button>
</div>
);
}
render(<TodoDetail id={1} />);
Live Preview
Loading...
Store
Editor
import { GQLEndpoint, GQLEntity } from '@rest-hooks/graphql';
const gql = new GQLEndpoint('/');
export class Todo extends GQLEntity {
readonly title: string = '';
readonly completed: boolean = false;
}
export const TodoResource = {
get: gql.query(`
query GetTodo($id: ID!) {
todo(id: $id) {
id
title
completed
}
}
`, { todo: Todo }),
update: gql.mutation(
`mutation UpdateTodo($todo: Todo!) {
updateTodo(todo: $todo) {
id
title
completed
}
}`,
{ updateTodo: Todo },
),
}
import { TodoResource } from './api';
function TodoDetail({ id }: { id: number }) {
const { todo } = useSuspense(TodoResource.get, { id });
const controller = useController();
const updateWith = title => () =>
controller.fetch(
TodoResource.update,
{ todo: { id, title } }
);
return (
<div>
<div>{todo.title}</div>
<button onClick={updateWith('🥑')}>🥑</button>
<button onClick={updateWith('💖')}>💖</button>
</div>
);
}
render(<TodoDetail id={1} />);
Live Preview
Loading...
Store

An application

Data can be consumed and controlled in many contexts, speeding up development.

Rest Hooks uses data normalization to maintain consistency no matter how and where the data is consumed.

Every piece of data maintains referential stability unless it changes. This ensures the most optimized render performance, as well as predictable equality checks.

Rest easy with the help of debugging, unit testing, and storybook integration.

Editor
export class Todo extends Entity {
id = 0;
userId = 0;
title = '';
completed = false;
pk() {
return `${this.id}`;
}
}
const BaseTodoResource = createResource({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
schema: Todo,
optimistic: true,
});
export const TodoResource = {
...BaseTodoResource,
getList: BaseTodoResource.getList.extend({
process(todos) {
// for demo purposes we'll only use the first seven
return todos.slice(0, 7);
},
}),
queryRemaining: new Query(
new schema.All(Todo),
(entries, { userId } = {}) => {
if (userId !== undefined)
return entries.filter(todo => todo.userId === userId && !todo.completed)
.length;
return entries.filter(todo => !todo.completed).length;
},
),
};
import { TodoResource, type Todo } from './api';
export default function TodoItem({ todo }: { todo: Todo }) {
const controller = useController();
return (
<div>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={e =>
controller.fetch(
TodoResource.partialUpdate,
{ id: todo.id },
{ completed: e.currentTarget.checked },
)
}
/>
{todo.completed ? <strike>{todo.title}</strike> : todo.title}
</label>
<CancelButton
onClick={() =>
controller.fetch(TodoResource.delete, {
id: todo.id,
})
}
/>
</div>
);
}
import { useCache } from '@rest-hooks/react';
import { TodoResource } from './api';
export default function TodoStats({ userId }: { userId?: number }) {
const remaining = useCache(TodoResource.queryRemaining, { userId });
return <div style={{ textAlign: 'center' }}>{remaining} tasks remaining</div>;
}
import { TodoResource } from './api';
import TodoItem from './TodoItem';
import TodoStats from './TodoStats';
function TodoList() {
const userId = 1;
const todos = useSuspense(TodoResource.getList, { userId });
return (
<div>
<TodoStats userId={userId} />
{todos.map(todo => (
<TodoItem key={todo.pk()} todo={todo} />
))}
</div>
);
}
render(<TodoList />);
Live Preview
Loading...
Store
Editor
import { GQLEndpoint, GQLEntity, Query, schema } from '@rest-hooks/graphql';
const gql = new GQLEndpoint('/');
export class Todo extends GQLEntity {
readonly title: string = '';
readonly completed: boolean = false;
readonly userId: number = 0;
}
export const TodoResource = {
get: gql.query(
`query GetTodo($id: ID!) {
todo(id: $id) {
id
title
completed
userId
}
}
`,
{ todo: Todo },
),
getList: gql.query(
`query GetTodos {
todo {
id
title
completed
userId
}
}
`,
{ todos: [Todo] },
),
update: gql.mutation(
`mutation UpdateTodo($todo: Todo!) {
updateTodo(todo: $todo) {
id
title
completed
}
}`,
{ updateTodo: Todo },
),
queryRemaining: new Query(
new schema.All(Todo),
(entries, { userId } = {}) => {
if (userId !== undefined)
return entries.filter(todo => todo.userId === userId && !todo.completed)
.length;
return entries.filter(todo => !todo.completed).length;
},
),
};
import { TodoResource, type Todo } from './api';
export default function TodoItem({ todo }: { todo: Todo }) {
const controller = useController();
return (
<div>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={e =>
controller.fetch(TodoResource.update, {
todo: { id: todo.id, completed: e.currentTarget.checked },
})
}
/>
{todo.completed ? <strike>{todo.title}</strike> : todo.title}
</label>
</div>
);
}
import { useCache } from '@rest-hooks/react';
import { TodoResource } from './api';
export default function TodoStats({ userId }: { userId?: number }) {
const remaining = useCache(TodoResource.queryRemaining, { userId });
return <div style={{ textAlign: 'center' }}>{remaining} tasks remaining</div>;
}
import { TodoResource } from './api';
import TodoItem from './TodoItem';
import TodoStats from './TodoStats';
function TodoList() {
const { todos } = useSuspense(TodoResource.getList, {});
return (
<div>
<TodoStats />
{todos.map(todo => (
<TodoItem key={todo.pk()} todo={todo} />
))}
</div>
);
}
render(<TodoList />);
Live Preview
Loading...
Store
Data IntegrityData Integrity

Data Integrity

Strong inferred types; single source of truth that is referentially stable ensures consistency; asynchronous invariants make it easy to avoid race conditions

Performance

Normalized cache means data is often ready before it is even needed. Automatic request deduplication means less data to send over the network.

Composition over configuration

Declare what you need where you need it. Share data definitions across platforms, components, protocols, and behaviors.

Incremental Adoption

Get started fast with one line data definition and one line data binding. Then add TypeScript, normalized cache with Schemas, optimistic updates and more.