data-client
Data Client is a TypeScript data-fetching and normalized caching library for React (and Vue, React Native, Next.js, Expo) that uses declarative, schema-driven resource definitions to bind dynamic data—REST, GraphQL, WebSockets/SSE—to components with one-line hooks and zero manual cache updater functions.
Apache-2.0Permissive — free to use in commercial and proprietary software, with attribution.View license →
Production readiness
4/5- Actively maintainedCommits in the last 6 months
- No known vulnerabilitiesNot yet scanned
- Clear, usable licenseApache-2.0 (permissive)
- Proven adoptionWidely used
- Has documentationDocumentation indexed
npm install data-clientOur analysis
A schema-driven data client for React-family apps that normalizes server data into a global cache and exposes it through suspense-based hooks. You define Entities and Resources once, and mutations automatically keep all derived views consistent without writing updater functions.
When to use data-client
Choose it for data-heavy apps with highly relational/normalized API responses where the same entities appear across many views and must stay consistent after mutations. Good when you want strong TypeScript inference, automatic referential equality, optimistic updates, and built-in support for REST, GraphQL, and streaming (WebSockets/SSE) under one model.
When not to
Overkill for simple apps with a few independent fetches—TanStack Query or SWR have a gentler learning curve and less ceremony. If your data isn't relational, the normalization/schema modeling overhead may not pay off, and the Entity/Resource class-based approach can feel heavy compared to plain key-based query libraries.
Strengths
- Automatic normalized cache with global consistency and referential equality, eliminating manual cache invalidation/updater logic
- Strong TypeScript inference end-to-end from schema to hooks
- Unified model spanning REST, GraphQL, WebSockets and SSE plus optimistic updates and subscriptions
- First-class Suspense, concurrent mode and partial-hydration SSR support; small (~9kb gzip) and tree-shakable
- Built-in data mocking/fixtures for Storybook and tests, plus a browser devtools extension
Trade-offs
- Requires upfront schema/Entity/Resource modeling—a steeper learning curve and more boilerplate than key-based libraries
- Opinionated, class-based API that differs significantly from mainstream React data tools, increasing migration cost
- Primarily React-centric despite Vue mentions; the broader ecosystem is much smaller than Apollo or TanStack Query
- Normalization adds conceptual complexity and can be a poor fit for non-relational or ad-hoc data
Maturity
Mature and actively maintained project (formerly Rest Hooks) with ~2k stars, extensive documentation, multiple example apps, devtools, and production usage. Smaller community than the dominant alternatives but stable APIs and a clear feature set.
The scalable way to build applications with dynamic data.
Declarative resouce definitons 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
Installation
npm install --save @data-client/react @data-client/rest @data-client/test
For more details, see the Getting Started docs page.
Skills
npx skills add reactive/data-client
Then run skill "data-client-setup"
Usage
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>
);
Reactive Mutations
const ctrl = useController();
return (
<>
<CreateArticleForm
onSubmit={article =>
ctrl.fetch(ArticleResource.getList.push, { id }, article)
}
/>
<ProfileForm
onSubmit={user =>
ctrl.fetch(UserResource.update, { id: article.author.id }, user)
}
/>
<button onClick={() => ctrl.fetch(ArticleResource.delete, { id })}>
Delete
</button>
</>
);
Subscriptions
const price = useLive(PriceResource.get, { symbol });
return price.value;
Type-safe Imperative Actions
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);
Programmatic queries
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);
Powerful Middlewares
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();
}
}
Integrated data mocking
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>
);
...all typed ...fast ...and consistent
For the small price of 9kb gziped. 🏁Get started now
Features
Strong Typescript inference
🛌 React Suspense support
🧵 React 18 Concurrent mode compatible
📝 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)
♻️ Optional redux integration
📱 React Native support
📱 Expo support
⚛️ NextJS support
💽 Global data consistency guarantees
🏇 Automatic race condition elimination
👯 Global referential equality guarantees
Examples
API
Reactive Applications
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, NetworkManager, SubscriptionManager, PollingSubscription, DevToolsManager