- NPM
- Yarn
- pnpm
- esm.sh
yarn add @data-client/rest
npm install --save @data-client/rest
pnpm add @data-client/rest
<script type="module">
import * from 'https://esm.sh/@data-client/rest';
</script>
Define the Resources
Resources are a collection of methods
for a given data model
. Entities and Schemas are the declarative data model.
RestEndpoint are the methods on
that data.
- Class
- Mixin
import { Entity, resource } from '@data-client/rest'; import { User } from './User'; export class Article extends Entity { slug = ''; title = ''; content = ''; author = User.fromJS(); tags: string[] = []; createdAt = Temporal.Instant.fromEpochSeconds(0); pk() { return this.slug; } static key = 'Article'; static schema = { author: User, createdAt: Temporal.Instant.from, }; } export const ArticleResource = resource({ urlPrefix: 'http://test.com', path: '/article/:slug', searchParams: {} as { userId?: string } | undefined, schema: Article, paginationField: 'page', });
import { EntityMixin, resource } from '@data-client/rest'; import { UserEntity } from './User'; export class Article { slug = ''; title = ''; content = ''; author = UserEntity.fromJS(); tags: string[] = []; createdAt = Temporal.Instant.fromEpochSeconds(0); } export class ArticleEntity extends EntityMixin(Article, { schema: { author: UserEntity, createdAt: Temporal.Instant.from, }, key: 'Article', pk: 'slug', }) {} export const ArticleResource = resource({ urlPrefix: 'http://test.com', path: '/article/:slug', searchParams: {} as { userId?: string } | undefined, schema: ArticleEntity, paginationField: 'page', });
Entity is a kind of schema that has a primary key (pk). This is what allows us to avoid state duplication, which is one of the core design choices that enable such high safety and performance characteristics.
static schema lets us specify declarative transformations like auto field deserialization with createdAt
and nesting the author field.
Urls are constructed by combining the urlPrefix with path templating.
TypeScript enforces the arguments specified with a prefixed colon like :slug
in this example.
// GET http://test.com/article/use-reactive-data-client
ArticleResource.get({ slug: 'use-reactive-data-client' });
Render the data
- Single
- List
- Server Component
import { useSuspense } from '@data-client/react';
import { ArticleResource } from '@/resources/Article';
export default function ArticleDetail({ slug }: { slug: string }) {
const article = useSuspense(ArticleResource.get, { slug });
return (
<article>
<h2>{article.title}</h2>
<div>{article.content}</div>
</article>
);
}
useSuspense() acts like await, ensuring the data is available before returning. Learn how to be declare your data dependencies
import { useSuspense } from '@data-client/react';
import { ArticleResource } from '@/resources/Article';
import ArticleSummary from './ArticleSummary';
export default function ArticleList({ userId }: { userId?: number }) {
const articles = useSuspense(ArticleResource.getList, { userId });
return (
<section>
{articles.map(article => (
<ArticleSummary key={article.pk()} article={article} />
))}
</section>
);
}
useSuspense() acts like await, ensuring the data is available before returning. Learn how to be declare your data dependencies
import { useSuspense } from '@data-client/react';
import { ArticleResource } from '@/resources/Article';
import ArticleSummary from './ArticleSummary';
export default async function ArticleList({ params }: { params: { userId: number } }) {
const articles = await ArticleResource.getList(params);
return (
<section>
{articles.map(article => (
<ArticleSummary key={article.pk()} article={article} />
))}
</section>
);
}
Server Components makes the data static and un-mutable.
Mutate the data
- Create
- Update
- Delete
import { useController } from '@data-client/react';
import { ArticleResource } from '@/resources/Article';
export default function NewArticleForm() {
const ctrl = useController();
return (
<Form
onSubmit={e =>
ctrl.fetch(ArticleResource.getList.push, new FormData(e.target))
}
>
<FormField name="title" />
<FormField name="content" type="textarea" />
<FormField name="tags" type="tag" />
</Form>
);
}
getList.push then takes any keyable
body to send as the payload and then returns a promise that
resolves to the new Resource created by the API. It will automatically be added in the cache for any consumers to display.
import { useController } from '@data-client/react';
import { ArticleResource } from '@/resources/Article';
export default function UpdateArticleForm({ slug }: { slug: string }) {
const article = useSuspense(ArticleResource.get, { slug });
const ctrl = useController();
return (
<Form
onSubmit={e =>
ctrl.fetch(ArticleResource.update, { slug }, new FormData(e.target))
}
initialValues={article}
>
<FormField name="title" />
<FormField name="content" type="textarea" />
<FormField name="tags" type="tag" />
</Form>
);
}
update then takes any keyable
body to send as the payload and then returns a promise that
then takes any keyable
body to send as the payload and then returns a promise that
resolves to the new Resource created by the API. It will automatically be added in the cache for any consumers to display.
import { useController } from '@data-client/react';
import { Article, ArticleResource } from '@/resources/Article';
export default function ArticleWithDelete({
article,
}: {
article: Article;
}) {
const ctrl = useController();
return (
<article>
<h2>{article.title}</h2>
<div>{article.content}</div>
<button
onClick={() =>
ctrl.fetch(ArticleResource.delete, { slug: article.slug })
}
>
Delete
</button>
</article>
);
}
We use FormData in the example since it doesn't require any opinionated form state management solution. Feel free to use whichever one you prefer.
Mutations automatically updates all usages without the need for additional requests.
When using TypeScript (optional), version 4.0 or above is required.