AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
翻譯準備中
Async State Management without the Management. REST, GraphQL, SSE, Websockets
翻譯準備中
開發工具TypeScript · JavaScriptGitHub @reactive自行安裝營運者未確認
試用過了嗎? 點一下就好,不需要登入。
製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗The scalable way to build applications with dynamic data.
Declarative resource definitions for REST, GraphQL, Websockets+SSE and more Performant rendering in React, NextJS, React Native, Expo, Vue
Schema driven. Zero updater functions.
📖Read The Docs | 🏁Getting Started | 🤖Agent Skills🎮 Demos: Todo | Github Social | NextJS SSR | Websockets+SSR
npm install --save @data-client/react @data-client/rest @data-client/test
Then add the provider at your app's root.
Or skip the steps above and paste this into your coding agent's chat. It installs Data Client's setup skill, which adds the packages, wires up the provider and links the skills they ship:
Set up `@data-client/react` and use it for all API data. Install its setup skill first: `npx skills add reactive/data-client --skill data-client-setup -y`, then read `.agents/skills/data-client-setup/SKILL.md` and follow it.
More on each in Installation.
Claude Code: in a terminal at the project root:
claude plugin marketplace add reactive/data-client --scope project \
--sparse .claude-plugin .agents/skills
claude plugin install data-client@data-client --scope project
Then in Claude Code's chat:
/data-client:data-client-setup Set up `@data-client/react` and use it for all API data
Cursor: in Cursor's agent chat:
/add-plugin data-client@https://github.com/reactive/data-client
Then, in a new chat:
/data-client-setup Set up `@data-client/react` and use it for all API data
Codex: in a terminal:
codex plugin marketplace add reactive/data-client \
--sparse .claude-plugin --sparse .agents/skills
codex plugin add data-client@data-client
Then in Codex's chat:
$data-client:data-client-setup Set up `@data-client/react` and use it for all API data
class User extends Entity {
id = '';
username = '';
}
class Article extends Entity {
id = '';
title = '';
body = '';
author = User.fromJS();
createdAt = Temporal.Instant.fromEpochMilliseconds(0);
static schema = {
author: User,
createdAt: Temporal.Instant.from,
};
}
const UserResource = resource({
path: '/users/:id',
schema: User,
optimistic: true,
});
const ArticleResource = resource({
path: '/articles/:id',
schema: Article,
searchParams: {} as { author?: string },
optimistic: true,
paginationField: 'cursor',
});
const article = useSuspense(ArticleResource.get, { id });
return (
<article>
<h2>
{article.title} by {article.author.username}
</h2>
<p>{article.body}</p>
</article>
);
const ctrl = useController();
return (
<>
<CreateArticleForm
onSubmit={article =>
ctrl.fetch(ArticleResource.getList.push, article)
}
/>
<ProfileForm
onSubmit={user =>
ctrl.fetch(UserResource.update, { id: article.author.id }, user)
}
/>
<button onClick={() => ctrl.fetch(ArticleResource.delete, { id })}>
Delete
</button>
</>
);
const price = useLive(PriceResource.get, { symbol });
return price.value;
const ctrl = useController();
await ctrl.fetch(ArticleResource.update, { id }, articleData);
await ctrl.fetchIfStale(ArticleResource.get, { id });
ctrl.expireAll(ArticleResource.getList);
ctrl.invalidate(ArticleResource.get, { id });
ctrl.invalidateAll(ArticleResource.getList);
ctrl.setResponse(ArticleResource.get, { id }, articleData);
ctrl.set(Article, { id }, articleData);
const queryTotalVotes = new Query(
new Collection([BlogPost]),
posts => posts.reduce((total, post) => total + post.votes, 0),
);
const totalVotes = useQuery(queryTotalVotes);
const totalVotesForUser = useQuery(queryTotalVotes, { userId });
const groupTodoByUser = new Query(
TodoResource.getList.schema,
todos => Object.groupBy(todos, todo => todo.userId),
);
const todosByUser = useQuery(groupTodoByUser);
class LoggingManager implements Manager {
middleware: Middleware = controller => next => async action => {
console.log('before', action, controller.getState());
await next(action);
console.log('after', action, controller.getState());
};
cleanup() {}
}
class TickerStream implements Manager {
middleware: Middleware = controller => {
this.handleMsg = msg => {
controller.set(Ticker, { id: msg.id }, msg);
};
return next => action => next(action);
};
init() {
this.websocket = new WebSocket('wss://ws-feed.myexchange.com');
this.websocket.onmessage = event => {
const msg = JSON.parse(event.data);
this.handleMsg(msg);
};
}
cleanup() {
this.websocket.close();
}
}
const fixtures = [
{
endpoint: ArticleResource.getList,
args: [{ maxResults: 10 }] as const,
response: [
{
id: '5',
title: 'first post',
body: 'have a merry christmas',
author: { id: '10', username: 'bob' },
createdAt: new Date(0).toISOString(),
},
{
id: '532',
title: 'second post',
body: 'never again',
author: { id: '10', username: 'bob' },
createdAt: new Date(0).toISOString(),
},
],
},
{
endpoint: ArticleResource.update,
response: ({ id }, body) => ({
...body,
id,
}),
},
];
const Story = () => (
<MockResolver fixtures={options[result]}>
<ArticleList maxResults={10} />
</MockResolver>
);
For the small price of 9kb gzipped. 🏁Get started now
Rendering: useSuspense(), useLive(), useCache(), useDLE(), useQuery(), useLoading(), useDebounce(), useCancelling()
Event handling: useController() returns Controller
Components: <DataProvider/>, <AsyncBoundary/>, <ErrorBoundary/>, <MockResolver/>
Data Mocking: Fixture, Interceptor, renderDataHook()
Middleware: LogoutManager, , , ,