Installation
npm install --save @data-client/react @data-client/rest @data-client/test
For more details, see the Installation 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
- ๐ฆ 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
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
Define Data
Networking definition
Data model| 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 | ๐ | |
| any | Query(Queryable) | memoized custom transforms | โ |