The scalable way to build applications with dynamic data.
Declarative resouce definitons for REST, GraphQL, Websockets+SSE and more
Performant rendering in Vue 3
Schema driven. Zero updater functions.
Installation
npm install --save @data-client/vue @data-client/rest
For more details, see the Getting Started docs page.
Skills
npx skills add reactive/data-client
Then run skill "data-client-setup"
Usage
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',
});
<template>
<article>
<h2>
{{ article.title }} by {{ article.author.username }}
</h2>
<p>{{ article.body }}</p>
</article>
</template>
<script setup lang="ts">
const props = defineProps<{ id: string }>();
const article = await useSuspense(ArticleResource.get, { id: props.id });
</script>
<template>
<div>
<CreateArticleForm @submit="handleCreateArticle" />
<ProfileForm @submit="handleUpdateProfile" />
<button @click="handleDeleteArticle">Delete</button>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{ id: string; article: Article }>();
const ctrl = useController();
const handleCreateArticle = (article: Partial<Article>) =>
ctrl.fetch(ArticleResource.getList.push, { id: props.id }, article);
const handleUpdateProfile = (user: Partial<User>) =>
ctrl.fetch(UserResource.update, { id: props.article.author.id }, user);
const handleDeleteArticle = () =>
ctrl.fetch(ArticleResource.delete, { id: props.id });
</script>
<template>
<div>{{ price.value }}</div>
</template>
<script setup lang="ts">
const props = defineProps<{ symbol: string }>();
const price = useLive(PriceResource.get, { symbol: props.symbol });
</script>
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();
}
}
import { createApp } from 'vue';
import { DataClientPlugin } from '@data-client/vue';
import { MockPlugin } from '@data-client/vue/test';
const app = createApp(App);
app.use(DataClientPlugin);
if (process.env.NODE_ENV !== 'production') {
app.use(MockPlugin, {
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,
}),
},
],
});
}
app.mount('#app');
Note: MockPlugin must be installed after DataClientPlugin and before mounting the app.
...all typed ...fast ...and consistent
For the small price of 9kb gziped. 🏁Get started now
Features
API
- Rendering:
useSuspense(), useLive(), useCache(), useDLE(), useQuery(), useLoading(), useDebounce(), useCancelling()
- Event handling:
useController() returns Controller
ctrl.fetch
ctrl.fetchIfStale
ctrl.expireAll
ctrl.invalidate
ctrl.invalidateAll
ctrl.resetEntireStore
ctrl.set
ctrl.setResponse
ctrl.setError
ctrl.resolve
ctrl.subscribe
ctrl.unsubscribe
- Components:
<AsyncBoundary/>, <ErrorBoundary/>
- Middleware:
LogoutManager, NetworkManager, SubscriptionManager, PollingSubscription, DevToolsManager