Async State Management without the Management. REST, GraphQL, SSE, Websockets
See the codeThe 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
For more details, see the Getting Started docs page.
npx skills add reactive/data-client
Then run skill "data-client-setup"
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
| 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 | โ |
718 followers ยท starred Oct 2019
17,405 followers ยท starred Jul 2019
6,144 followers ยท starred Aug 2019
414 followers ยท starred Oct 2022
Async State Management without the Management. REST, GraphQL, SSE, Websockets
See the codeThe 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
For more details, see the Getting Started docs page.
npx skills add reactive/data-client
Then run skill "data-client-setup"
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
| 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 | โ |
718 followers ยท starred Oct 2019
17,405 followers ยท starred Jul 2019
6,144 followers ยท starred Aug 2019
414 followers ยท starred Oct 2022