Skip to main content

DataClientPlugin

Vue plugin that creates the store and Controller, and provides them to every component in the app. Install it once, before app.mount(); composables only work in components of an app it is installed on.

main.ts
import { createApp } from 'vue';
import { DataClientPlugin } from '@data-client/vue';
import App from './App.vue';

const app = createApp(App);
app.use(DataClientPlugin);
app.mount('#app');

Managers start when the plugin is installed, and stop when the app is unmounted.

Options​

app.use(DataClientPlugin, options);
interface ProvideOptions {
managers?: Manager[];
initialState?: State<unknown>;
Controller?: typeof Controller;
gcPolicy?: GCInterface;
}

managers?: Manager[]​

List of Managers to use. This is the main extensibility point of the store.

Defaults to getDefaultManagers(), which can also be used to extend the defaults.

main.ts
import { DataClientPlugin, getDefaultManagers } from '@data-client/vue';

app.use(DataClientPlugin, {
managers: [...getDefaultManagers(), new MyManager()],
});

Default Production:

[new NetworkManager(), new SubscriptionManager(PollingSubscription)];

Default Development:

[
new DevToolsManager(),
new NetworkManager(),
new SubscriptionManager(PollingSubscription),
];

initialState?: State<unknown>​

Instead of starting with an empty cache, you can provide your own initial state. This can be useful for testing, or rehydrating the cache state when using server side rendering. mockInitialState() builds one from fixtures.

main.ts
app.use(DataClientPlugin, { initialState: window.__INITIAL_STATE__ });
export interface State<T> {
readonly entities: {
readonly [entityKey: string]: { readonly [pk: string]: T } | undefined;
};
readonly endpoints: {
readonly [key: string]: unknown | PK[] | PK | undefined;
};
readonly indexes: NormalizedIndex;
readonly meta: {
readonly [key: string]: {
readonly date: number;
readonly fetchedAt: number;
readonly expiresAt: number;
readonly prevExpiresAt?: number;
readonly error?: ErrorTypes;
readonly invalidated?: boolean;
readonly errorPolicy?: 'hard' | 'soft' | undefined;
};
};
readonly entitiesMeta: {
readonly [entityKey: string]: {
readonly [pk: string]: {
readonly date: number;
readonly expiresAt: number;
readonly fetchedAt: number;
};
};
};
readonly optimistic: (SetResponseAction | OptimisticAction)[];
readonly lastReset: number;
}

Controller?: typeof Controller​

This allows you to extend Controller to provide additional functionality. This might be useful if you have additional actions you want to dispatch to custom Managers.

main.ts
import { Controller, DataClientPlugin } from '@data-client/vue';

class MyController extends Controller {
doSomething = () => {
console.log('hi');
};
}

app.use(DataClientPlugin, { Controller: MyController });

useController() and $dataClient then return a MyController instance, but they are still typed as Controller. Cast to reach the added members:

import { useController } from '@data-client/vue';

const ctrl = useController() as MyController;
ctrl.doSomething();

gcPolicy?: GCInterface​

Removes data from the store once no component uses it and it has gone stale. Defaults to new GCPolicy(); pass one to change how often it sweeps or how long unused data is kept.

main.ts
import { DataClientPlugin, GCPolicy } from '@data-client/vue';

app.use(DataClientPlugin, {
// sweep every 10 minutes
gcPolicy: new GCPolicy({ intervalMS: 60 * 1000 * 10 }),
});
GCPolicy options
new GCPolicy({
// how often to sweep (default 5 minutes)
intervalMS: 60 * 1000 * 5,
// how many stale lifetimes before data is removed (default 2)
expiryMultiplier: 2,
// or choose when unused data is removed (replaces expiryMultiplier)
// here: one minute after it goes stale
expiresAt: ({ expiresAt }) => expiresAt + 60 * 1000,
});

$dataClient​

The plugin also adds the Controller as the $dataClient global property, so templates and Options API components (as this.$dataClient) can use it without useController(). It is typed as Controller with no extra setup.

DeleteTodo.vue
<script setup lang="ts">
import { TodoResource } from '@/resources/Todo';

defineProps<{ id: number }>();
</script>

<template>
<button @click="$dataClient.fetch(TodoResource.delete, { id })">
Delete
</button>
</template>

Using composables​

Composables like useSuspense() must run during a component's setup, so Vue knows which app's store to use. Awaiting them requires <script setup>: in a hand-written async setup(), composables called after the first await lose the component instance and throw.

TodoDetail.vue
<script setup lang="ts">
import { useSuspense } from '@data-client/vue';
import { TodoResource } from '@/resources/Todo';
import { UserResource } from '@/resources/User';

const todo = await useSuspense(TodoResource.get, { id: 1 });
// still works after the await
const user = await useSuspense(UserResource.get, {
id: todo.value.userId,
});
</script>

Components that await must render inside a <Suspense> boundary.