Using our Create, Update, and Delete endpoints with
Controller.fetch() reactively updates all appropriate components atomically (at the same time).
useController() gives components access to this global setState()
on steriods.
import { Entity, createResource } from '@data-client/rest';
export class Todo extends Entity {
id = 0;
userId = 0;
title = '';
completed = false;
pk() {
return `${this.id}`;
}
static key = 'Todo';
}
export const TodoResource = createResource({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
searchParams: {} as { userId?: string | number } | undefined,
schema: Todo,
optimistic: true,
});
import { useController } from '@data-client/react';
import { TodoResource, type Todo } from './TodoResource';
export default function TodoItem({ todo }: { todo: Todo }) {
const ctrl = useController();
const handleChange = e =>
ctrl.fetch(
TodoResource.partialUpdate,
{ id: todo.id },
{ completed: e.currentTarget.checked },
);
const handleDelete = () =>
ctrl.fetch(TodoResource.delete, {
id: todo.id,
});
return (
<div className="listItem nogap">
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={handleChange}
/>
{todo.completed ? <strike>{todo.title}</strike> : todo.title}
</label>
<CancelButton onClick={handleDelete} />
</div>
);
}
import { v4 as uuid } from 'uuid';
import { useController } from '@data-client/react';
import { TodoResource } from './TodoResource';
export default function CreateTodo({ userId }: { userId: number }) {
const ctrl = useController();
const handleKeyDown = async e => {
if (e.key === 'Enter') {
ctrl.fetch(TodoResource.getList.push, {
userId,
title: e.currentTarget.value,
});
e.currentTarget.value = '';
}
};
return (
<div className="listItem nogap">
<label>
<input type="checkbox" name="new" checked={false} disabled />
<input type="text" onKeyDown={handleKeyDown} />
</label>
<CancelButton />
</div>
);
}
import { useSuspense } from '@data-client/react';
import { TodoResource } from './TodoResource';
import TodoItem from './TodoItem';
import CreateTodo from './CreateTodo';
function TodoList() {
const userId = 1;
const todos = useSuspense(TodoResource.getList, { userId });
return (
<div>
{todos.map(todo => (
<TodoItem key={todo.pk()} todo={todo} />
))}
<CreateTodo userId={userId} />
</div>
);
}
render(<TodoList />);
Rather than triggering invalidation cascades or using manually written update functions,
RDC reactively updates appropriate components using the fetch response.
import { Entity } from '@data-client/rest';
export class Post extends Entity {
id = 0;
userId = 0;
title = '';
body = '';
votes = 0;
pk() {
return this.id?.toString();
}
static key = 'Post';
get img() {
return `//placekitten.com/96/72?image=${this.id % 16}`;
}
}
import { RestEndpoint, createResource } from '@data-client/rest';
import { AbortOptimistic } from '@data-client/rest';
import { Post } from './Post';
export { Post };
export const PostResource = createResource({
path: '/posts/:id',
schema: Post,
}).extend(Base => ({
vote: new RestEndpoint({
path: '/posts/:id/vote',
method: 'POST',
body: undefined,
schema: Post,
getOptimisticResponse(snapshot, { id }) {
const { data } = snapshot.getResponse(Base.get, { id });
if (!data) throw new AbortOptimistic();
return {
id,
votes: data.votes + 1,
};
},
}),
}));
import { useController } from '@data-client/react';
import { PostResource, type Post } from './PostResource';
export default function PostItem({ post }: { post: Post }) {
const ctrl = useController();
const handleVote = () => {
ctrl.fetch(PostResource.vote, { id: post.id });
};
return (
<div>
<div className="voteBlock">
<small className="vote">
<button className="up" onClick={handleVote}>
</button>
{post.votes}
</small>
<img src={post.img} width="70" height="52" />
</div>
<div>
<h4>{post.title}</h4>
<p>{post.body}</p>
</div>
</div>
);
}
import { Query, schema } from '@data-client/rest';
import { Post } from './PostResource';
const queryTotalVotes = new Query(
new schema.All(Post),
(posts, { userId } = {}) => {
if (userId !== undefined)
posts = posts.filter(post => post.userId === userId);
return posts.reduce((total, post) => total + post.votes, 0);
},
);
export default function TotalVotes({ userId }: { userId: number }) {
const totalVotes = useCache(queryTotalVotes, { userId });
return (
<center>
<small>{totalVotes} votes total</small>
</center>
);
}
import { useSuspense } from '@data-client/react';
import { PostResource } from './PostResource';
import PostItem from './PostItem';
import TotalVotes from './TotalVotes';
function PostList() {
const userId = 2;
const posts = useSuspense(PostResource.getList, { userId });
return (
<div>
{posts.map(post => (
<PostItem key={post.pk()} post={post} />
))}
<TotalVotes userId={userId} />
</div>
);
}
render(<PostList />);
getOptimisticResponse is just like setState with an updater function. Snapshot provides typesafe access to the previous store value,
which we use to return the expected fetch response.
Reactive Data Client 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.
useLoading() enhances async functions by tracking their loading and error states.
import { Entity, createResource } from '@data-client/rest';
export class Post extends Entity {
id = 0;
userId = 0;
title = '';
body = '';
votes = 0;
pk() {
return this.id?.toString();
}
static key = 'Post';
get img() {
return `//placekitten.com/96/72?image=${this.id % 16}`;
}
}
export const PostResource = createResource({
path: '/posts/:id',
schema: Post,
});
import { useSuspense } from '@data-client/react';
import { PostResource } from './PostResource';
export default function PostDetail({ id }) {
const post = useSuspense(PostResource.get, { id });
return (
<div>
<div className="voteBlock">
<img src={post.img} width="70" height="52" />
</div>
<div>
<h4>{post.title}</h4>
<p>{post.body}</p>
</div>
</div>
);
}
export default function PostForm({ onSubmit, loading, error }) {
const handleSubmit = e => {
e.preventDefault();
const data = new FormData(e.target);
onSubmit(data);
};
return (
<form onSubmit={handleSubmit}>
<label>
Title:
<br />
<input type="text" name="title" defaultValue="My New Post"
required />
</label>
<br />
<label>
Body:
<br />
<textarea name="body" rows={12} required>
After clicking 'save', the button will be disabled until
the POST is completed. Upon completion the newly created
post is displayed immediately as Reactive Data Client is
able to use the fetch response to populate the store.
</textarea>
</label>
{error ? (
<div className="alert alert--danger">{error.message}</div>
) : null}
<div>
<button type="submit" disabled={loading}>
{loading ? 'saving...' : 'Save'}
</button>
</div>
</form>
);
}
import { useController } from '@data-client/react';
import { useLoading } from '@data-client/hooks';
import { PostResource } from './PostResource';
import PostForm from './PostForm';
export default function PostCreate({ navigateToPost }) {
const ctrl = useController();
const [handleSubmit, loading, error] = useLoading(
async data => {
const post = await ctrl.fetch(PostResource.getList.push, data);
navigateToPost(post.id);
},
[ctrl],
);
return <PostForm onSubmit={handleSubmit} loading={loading}
error={error} />;
}
import PostCreate from './PostCreate';
import PostDetail from './PostDetail';
function Navigation() {
const [id, setId] = React.useState<undefined | number>(undefined);
if (id) {
return (
<div>
<PostDetail id={id} />
<center>
<button onClick={() => setId(undefined)}>New Post</button>
</center>
</div>
);
}
return <PostCreate navigateToPost={setId} />;
}
render(<Navigation />);