- NPM
- Yarn
- pnpm
- esm.sh
npm install --save @data-client/rest
yarn add @data-client/rest
pnpm add @data-client/rest
<script type="importmap">
{
"imports": {
"@data-client/rest": "https://esm.sh/@data-client/rest"
}
}
</script>
定义 Resource
Resource 是针对某个 data model 的一组 methods。Entity 和 schema 是声明式的_数据模型_。
RestEndpoint 则是作用于这些数据的方法。
- 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.fromEpochMilliseconds(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.fromEpochMilliseconds(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 是一种拥有主键(pk)的 schema。正是它让我们能够避免状态重复,这是实现如此高的安全性和性能的核心设计决策之一。
static schema 让我们可以声明式地指定转换,例如对 createdAt 进行自动字段反序列化,以及嵌套 author 字段。
Url 的构建方式是将 urlPrefix 与路径模板组合起来。
TypeScript 会强制要求以冒号前缀指定的参数,例如本例中的 :slug。
// GET http://test.com/article/use-reactive-data-client
ArticleResource.get({ slug: 'use-reactive-data-client' });
渲染数据
- 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() 的作用类似 await,确保在返回之前数据已经可用。了解如何声明你的数据依赖
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() 的作用类似 await,确保在返回之前数据已经可用。了解如何声明你的数据依赖
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 会让数据变成静态且不可变更的。
变更数据
- 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 接收任意 keyable 的 body 作为请求载荷发送,然后返回一个 promise,它会 resolve 为 API 创建的新 Resource。该 Resource 会自动加入缓存,供所有使用方展示。
import { useController, useSuspense } 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 接收任意 keyable 的 body 作为请求载荷发送,然后返回一个 promise,它接收任意 keyable 的 body 作为请求载荷发送,然后返回一个 promise,它会 resolve 为 API 创建的新 Resource。该 Resource 会自动加入缓存,供所有使用方展示。
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>
);
}
示例中我们使用了 FormData,因为它不需要任何特定的表单状态管理方案。你可以随意选用自己喜欢的方案。
变更会自动更新_所有_用到该数据的地方,无需额外的请求。
使用 TypeScript(可选)时,需要 4.0 或更高版本。
REST Agent Skills
- Skills
- OpenSkills
- Claude Code
npx skills add reactive/data-client \
--skill data-client-schema \
--skill data-client-rest-setup \
--skill data-client-rest
npx openskills install reactive/data-client/.agents/skills/data-client-schema
npx openskills install reactive/data-client/.agents/skills/data-client-rest-setup
npx openskills install reactive/data-client/.agents/skills/data-client-rest
claude plugin marketplace add reactive/data-client
claude plugin install core@data-client
然后调用 /data-client-rest-setup 进行迁移
从 Axios 迁移
data-client-rest-setup skill 会自动检测 axios 的使用情况并执行 axios 迁移——包括 codemod、interceptor 转换以及错误处理的迁移。
请参阅完整的 Axios 迁移指南,其中包含分步示例、速查表以及独立的 codemod。