Data client
Async State Management without the Management. REST, GraphQL, SSE, Websockets
The scalable way to build applications with [dynamic data](https://dataclient.io/docs/getting-started/mutations). The project is written primarily in TypeScript, distributed under the Apache License 2.0 license, first published in 2019. It has gained significant community traction with 2,031 stars and 98 forks on GitHub. Key topics include: expo, fetch, hooks, normalized, normalizr.
The scalable way to build applications with dynamic data.
Declarative resouce definitons for REST, GraphQL, Websockets+SSE and more
<br/>Performant rendering in React, NextJS, React Native, Expo, Vue
Schema driven. Zero updater functions.
📖Read The Docs | 🏁Getting Started | 🤖Agent Skills<br/>🎮 Demos:
Todo |
Github Social |
NextJS SSR |
Websockets+SSR
Installation
bashnpm install --save @data-client/react @data-client/rest @data-client/test
For more details, see the Getting Started docs page.
Skills
bashnpx skills add reactive/data-client
Then run skill "data-client-setup"
Usage
Simple TypeScript definition
typescriptclass 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
typescriptconst 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
tsxconst article = useSuspense(ArticleResource.get, { id }); return ( <article> <h2> {article.title} by {article.author.username} </h2> <p>{article.body}</p> </article> );
Reactive Mutations
tsxconst 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
tsxconst price = useLive(PriceResource.get, { symbol }); return price.value;
Type-safe Imperative Actions
tsxconst 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
typescriptconst queryTotalVotes = new Query( new Collection([BlogPost]), posts => posts.reduce((total, post) => total + post.votes, 0), ); const totalVotes = useQuery(queryTotalVotes); const totalVotesForUser = useQuery(queryTotalVotes, { userId });
typescriptconst groupTodoByUser = new Query( TodoResource.getList.schema, todos => Object.groupBy(todos, todo => todo.userId), ); const todosByUser = useQuery(groupTodoByUser);
Powerful Middlewares
tsclass 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() {} }
tsclass 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
tsxconst 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
- <img src="https://github.com/reactive/data-client/raw/master/packages/react/typescript.svg?sanitize=true" alt="TS" style="max-width: 100%;"> 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
Examples
API
Reactive Applications
-
Rendering: useSuspense(), useLive(), useCache(), useDLE(), useQuery(), useLoading(), useDebounce(), useCancelling()
-
Event handling: useController() returns Controller
<table> <thead> <tr> <th>Method</th> <th>Subject</th> </tr> </thead> <tbody> <tr> <th colSpan="2" align="center">Fetch</th> </tr> <tr> <td><a href="https://dataclient.io/docs/api/Controller#fetch">ctrl.fetch</a></td> <td>Endpoint + Args</td> </tr> <tr> <td><a href="https://dataclient.io/docs/api/Controller#fetchIfStale">ctrl.fetchIfStale</a></td> <td>Endpoint + Args</td> </tr> <tr> <th colSpan="2" align="center">Expiry</th> </tr> <tr> <td><a href="https://dataclient.io/docs/api/Controller#expireAll">ctrl.expireAll</a></td> <td>Endpoint</td> </tr> <tr> <td><a href="https://dataclient.io/docs/api/Controller#invalidate">ctrl.invalidate</a></td> <td>Endpoint + Args</td> </tr> <tr> <td><a href="https://dataclient.io/docs/api/Controller#invalidateAll">ctrl.invalidateAll</a></td> <td>Endpoint</td> </tr> <tr> <td><a href="https://dataclient.io/docs/api/Controller#resetEntireStore">ctrl.resetEntireStore</a></td> <td>Everything</td> </tr> <tr> <th colSpan="2" align="center">Set</th> </tr> <tr> <td><a href="https://dataclient.io/docs/api/Controller#set">ctrl.set</a></td> <td>Schema + Args</td> </tr> <tr> <td><a href="https://dataclient.io/docs/api/Controller#setResponse">ctrl.setResponse</a></td> <td>Endpoint + Args</td> </tr> <tr> <td><a href="https://dataclient.io/docs/api/Controller#setError">ctrl.setError</a></td> <td>Endpoint + Args</td> </tr> <tr> <td><a href="https://dataclient.io/docs/api/Controller#resolve">ctrl.resolve</a></td> <td>Endpoint + Args</td> </tr> <tr> <th colSpan="2" align="center">Subscription</th> </tr> <tr> <td><a href="https://dataclient.io/docs/api/Controller#subscribe">ctrl.subscribe</a></td> <td>Endpoint + Args</td> </tr> <tr> <td><a href="https://dataclient.io/docs/api/Controller#unsubscribe">ctrl.unsubscribe</a></td> <td>Endpoint + Args</td> </tr> </tbody> </table> -
Components: <DataProvider/>, <AsyncBoundary/>, <ErrorBoundary/>, <MockResolver/>
-
Data Mocking: Fixture, Interceptor, renderDataHook()
-
Middleware: LogoutManager, NetworkManager, SubscriptionManager, PollingSubscription, DevToolsManager
Define Data
Networking definition
<table> <caption> <a href="https://dataclient.io/docs/concepts/normalization">Data model</a> </caption> <thead> <tr> <th>Data Type</th> <th>Mutable</th> <th>Schema</th> <th>Description</th> <th><a href="https://dataclient.io/rest/api/schema#queryable">Queryable</a></th> </tr> </thead> <tbody><tr> <td rowSpan="4"><a href="https://en.wikipedia.org/wiki/Object_(computer_science)">Object</a></td> <td align="center">✅</td> <td><a href="https://dataclient.io/rest/api/Entity">Entity</a>, <a href="https://dataclient.io/rest/api/EntityMixin">EntityMixin</a></td> <td>single <em>unique</em> object</td> <td align="center">✅</td> </tr> <tr> <td align="center">✅</td> <td><a href="https://dataclient.io/rest/api/Union">Union(Entity)</a></td> <td>polymorphic objects (<code>A | B</code>)</td> <td align="center">✅</td> </tr> <tr> <td align="center">🛑</td> <td><a href="https://dataclient.io/rest/api/Object">Object</a></td> <td>statically known keys</td> <td align="center">🛑</td> </tr> <tr> <td align="center"></td> <td><a href="https://dataclient.io/rest/api/Invalidate">Invalidate(Entity)</a></td> <td><a href="https://dataclient.io/docs/concepts/expiry-policy#invalidate-entity">delete an entity</a></td> <td align="center">🛑</td> </tr> <tr> <td rowSpan="3"><a href="https://en.wikipedia.org/wiki/List_(abstract_data_type)">List</a></td> <td align="center">✅</td> <td><a href="https://dataclient.io/rest/api/Collection">Collection(Array)</a></td> <td>growable lists</td> <td align="center">✅</td> </tr> <tr> <td align="center">🛑</td> <td><a href="https://dataclient.io/rest/api/Array">Array</a></td> <td>immutable lists</td> <td align="center">🛑</td> </tr> <tr> <td align="center"> </td> <td><a href="https://dataclient.io/rest/api/All">All</a></td> <td>list of all entities of a kind</td> <td align="center">✅</td> </tr> <tr> <td rowSpan="2"><a href="https://en.wikipedia.org/wiki/Associative_array">Map</a></td> <td align="center">✅</td> <td><a href="https://dataclient.io/rest/api/Collection">Collection(Values)</a></td> <td>growable maps</td> <td align="center">✅</td> </tr> <tr> <td align="center">🛑</td> <td><a href="https://dataclient.io/rest/api/Values">Values</a></td> <td>immutable maps</td> <td align="center">🛑</td> </tr> <tr> <td rowSpan="1"><a href="https://en.wikipedia.org/wiki/Scalar_(mathematics)">Scalar</a></td> <td align="center">✅</td> <td><a href="https://dataclient.io/rest/api/Scalar">Scalar</a></td> <td>lens-dependent entity fields</td> <td align="center">✅</td> </tr> <tr> <td rowSpan="2">any</td> <td align="center"></td> <td><a href="https://dataclient.io/rest/api/Query">Query(Queryable)</a></td> <td>memoized custom transforms</td> <td align="center">✅</td> </tr> <tr> <td align="center"></td> <td><a href="https://dataclient.io/rest/api/Lazy">Lazy(Schema)</a></td> <td>deferred denormalization</td> <td align="center">✅</td> </tr> </tbody></table>Contributors
Showing top 12 contributors by commit count.
