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/testThen 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 follow it.
Or use the Claude Code, Cursor or Codex plugin
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 projectThen 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-clientThen in Codex's chat:
$data-client:data-client-setup Set up `@data-client/react` and use it for all API data
Simple TypeScript definition
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,
};
}Create collection of API Endpoints
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',
});One line data binding
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
-
Strong Typescript inference
- 🛌 React Suspense support
- 🧵 React 18 Concurrent mode compatible
- 💦 Partial Hydration Server Side Rendering
- 🎣 Declarative API
- 📝 Composition over configuration
- 💰 Normalized caching
- 💥 Tiny bundle footprint
- 🛑 Automatic overfetching elimination
- ✨ Fast optimistic updates
- 🧘 Flexible to fit any API design (one size fits all)
- 🔧 Debugging and inspection via browser extension
- 🌳 Tree-shakable (only use what you need)
- 🔁 Subscriptions
- ♻️ Optional redux integration
- 📙 Storybook mocking
- 📱 React Native support
- 📱 Expo support
- ⚛️ NextJS support
- 🚯 Declarative cache lifetime policy
- 🧅 Composable middlewares
- 💽 Global data consistency guarantees
- 🏇 Automatic race condition elimination
- 👯 Global referential equality guarantees
-
Rendering: useSuspense(), useLive(), useCache(), useDLE(), useQuery(), useLoading(), useDebounce(), useCancelling()
-
Event handling: useController() returns Controller
Method Subject Fetch ctrl.fetch Endpoint + Args ctrl.fetchIfStale Endpoint + Args Expiry ctrl.expireAll Endpoint ctrl.invalidate Endpoint + Args ctrl.invalidateAll Endpoint ctrl.resetEntireStore Everything Set ctrl.set Schema + Args ctrl.setResponse Endpoint + Args ctrl.setError Endpoint + Args ctrl.resolve Endpoint + Args Subscription ctrl.subscribe Endpoint + Args ctrl.unsubscribe Endpoint + Args -
Components: <DataProvider/>, <AsyncBoundary/>, <ErrorBoundary/>, <MockResolver/>
-
Data Mocking: Fixture, Interceptor, renderDataHook()
-
Middleware: LogoutManager, NetworkManager, SubscriptionManager, PollingSubscription, DevToolsManager
| Data Type | Mutable | Schema | Description | Queryable |
|---|---|---|---|---|
| Object | ✅ | Entity, EntityMixin | single unique object | ✅ |
| ✅ | Union(Entity) | polymorphic objects (A | B) |
✅ | |
| 🛑 | Object | statically known keys | 🛑 | |
| Invalidate(Entity) | delete an entity | 🛑 | ||
| List | ✅ | Collection(Array) | growable lists | ✅ |
| 🛑 | Array | immutable lists | 🛑 | |
| All | list of all entities of a kind | ✅ | ||
| Map | ✅ | Collection(Values) | growable maps | ✅ |
| 🛑 | Values | immutable maps | 🛑 | |
| Scalar | ✅ | Scalar | lens-dependent entity fields | ✅ |
| any | Query(Queryable) | memoized custom transforms | ✅ | |
| Lazy(Schema) | deferred denormalization | ✅ |