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
Translation pending
Developer toolsTypeScript · JavaScriptGitHub @reactiveSelf-installOwner unconfirmed
Paste into Claude or ChatGPT for installation instructions suited to your environment.
Have you tried it? One tap is enough. No sign-in needed.
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full 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, , , ,