Vue
fate supports Vue through vue-fate, with the same views, requests, cache, and mutations as the React adapter. Request and view composables expose reactive Vue resources, with <Suspense> support for loading states.
Installation
Install vue-fate in your Vue client:
npm add vue-fatepnpm add vue-fateyarn add vue-fateIf your server lives in a separate package, install @nkzw/fate there as a runtime dependency too.
Vite Plugin
Use the Vue adapter's Vite plugin in the client app:
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
import { fate } from 'vue-fate/vite';
export default defineConfig({
plugins: [
vue(),
fate({
module: '@your-org/server/fate.ts',
transport: 'native',
}),
],
});The plugin generates vue-fate/client, which contains the typed createFateClient helper for your app. The module and transport options are the same options used by the React adapter.
Providing the Client
Create a client with createFateClient and provide it with FateClient:
<script setup lang="ts">
import { computed, ref } from 'vue';
import { FateClient } from 'vue-fate';
import { createFateClient } from 'vue-fate/client';
import AppRoutes from './AppRoutes.vue';
const token = ref<string | null>(null);
const fate = computed(() =>
createFateClient({
headers: () => ({
authorization: token.value ? `Bearer ${token.value}` : '',
}),
url: '/fate',
}),
);
</script>
<template>
<FateClient :client="fate">
<Suspense>
<AppRoutes />
</Suspense>
</FateClient>
</template>The client prop accepts a plain client, a ref, a computed value, or a getter. Descendants always read the current client, so switching credentials, endpoints, or transports does not require remounting the provider.
You can also install the client as a Vue plugin:
import { createApp } from 'vue';
import { createFatePlugin } from 'vue-fate';
import { createFateClient } from 'vue-fate/client';
import App from './App.vue';
const fate = createFateClient({ url: '/fate' });
createApp(App).use(createFatePlugin(fate)).mount('#app');Defining Views
Views are plain TypeScript values and can live anywhere. In Vue apps, it is usually best to define shared views in .ts modules and import them from single-file components:
import type { Post, User } from '@your-org/server/views';
import { view } from 'vue-fate';
export const UserView = view<User>()({
id: true,
name: true,
username: true,
});
export const PostView = view<Post>()({
author: UserView,
id: true,
title: true,
});Requests
useRequest declares the data a route, page, or component tree needs. It returns a resource with data, pending, error, ready, refresh, and dispose:
<script setup lang="ts">
import { useListView, useRequest } from 'vue-fate';
import { PostCardView } from '../fateViews';
import PostCard from '../ui/PostCard.vue';
const request = useRequest({
posts: {
args: { first: 20 },
list: PostCardView,
},
});
await request.ready();
const [postItems, loadNext] = useListView(PostCardView, () => request.data.value?.posts);
</script>
<template>
<PostCard v-for="{ node } in postItems" :key="node.id" :post="node" />
<button v-if="loadNext" @click="loadNext()">Load more</button>
</template>Awaiting ready() in <script setup> participates in Vue Suspense. The getter passed to useListView keeps the list in sync when the request result changes. To render loading and error states yourself, read the resource’s pending and error refs without awaiting ready().
Refreshing Requests
Use useRefetch to refresh data from a button or event handler:
<script setup lang="ts">
import { useRefetch } from 'vue-fate';
import { UserView } from '../fateViews';
const refresh = useRefetch({ viewer: { view: UserView } });
</script>
<template>
<button @click="refresh().catch(reportError)">Refresh profile</button>
</template>The composable waits until you call refresh() to fetch from the network. Existing data stays visible, and request and view resources update when the result arrives. Handle failures in the event handler; reportError above represents your application's error handler.
Requests and options can be refs or getters. Each call uses their current values. The optional second argument accepts persist, timeout, and signal; see Timeouts and Cancellation.
Views in Components
Use useView to read a ViewRef from the normalized cache and subscribe to updates for the selected fields:
<script setup lang="ts">
import type { ViewRef } from 'vue-fate';
import { useView } from 'vue-fate';
import { PostCardView, UserView } from '../fateViews';
import UserCard from './UserCard.vue';
const props = defineProps<{
post: ViewRef<'Post'>;
}>();
const post = useView(PostCardView, () => props.post);
const author = useView(UserView, () => post.value?.author ?? null);
</script>
<template>
<article v-if="post">
<h2>{{ post.title }}</h2>
<UserCard v-if="author" :user="author" />
</article>
</template>Pass reactive props through a getter so fate tracks prop changes. In script, resources are refs and need .value. In templates, Vue unwraps them automatically.
Resolving Arrays of Refs
Use useViews when one component needs selected data from an array of refs, for example to filter or sort before rendering:
<script setup lang="ts">
import { computed } from 'vue';
import { useViews, type ViewRef } from 'vue-fate';
import { PostCardView } from '../fateViews';
const props = defineProps<{ posts: ReadonlyArray<ViewRef<'Post'>> }>();
const posts = useViews(PostCardView, () => props.posts);
const sorted = computed(() => [...posts.value].sort((a, b) => a.title.localeCompare(b.title)));
await posts.ready();
</script>
<template>
<h2 v-for="post in sorted" :key="post.id">{{ post.title }}</h2>
</template>Pass a plain array, ref, or getter, as you would with useView. The result follows the input order and updates when the refs or their selected fields change. Deferred refs are supported, and null or undefined entries keep their values in the result.
Await ready() to use Vue Suspense, or render the resource's pending and error states yourself.
Lists and Live Views
useListView subscribes to a connection returned from useRequest or from a nested view field:
const [comments, loadNextCommentPage] = useListView(CommentView, () => post.value?.comments);Items and pagination helpers are refs. A missing connection returns undefined items and null pagination helpers. Use comments.value ?? [] to treat missing comments as an empty list.
useLiveView and useLiveListView have the same resource shape as useView and useListView, but they also subscribe to server-pushed updates when the selected transport supports live views:
const post = useLiveView(PostCardView, () => props.post);
const [comments] = useLiveListView(CommentView, () => post.value?.comments);Vue cleans up resources when the component unmounts. Call dispose() to stop a resource earlier.
Mutations
Use useFateClient to access generated mutations:
<script setup lang="ts">
import { ref } from 'vue';
import { useFateClient } from 'vue-fate';
const props = defineProps<{
post: { id: string; likes: number };
}>();
const fate = useFateClient();
const pending = ref(false);
const error = ref<unknown>(null);
const like = async () => {
pending.value = true;
error.value = null;
try {
const result = await fate.mutations.post.like({
input: { id: props.post.id },
optimistic: { likes: props.post.likes + 1 },
});
error.value = result.error ?? null;
} catch (caughtError) {
error.value = caughtError;
} finally {
pending.value = false;
}
};
</script>
<template>
<button :disabled="pending" @click="like">Like</button>
<p v-if="error">Could not like this post.</p>
</template>Expected mutation errors are returned in result.error; unexpected failures reject the promise. See Action & Mutation Error Handling.
Use Vue refs or your application state library to track pending and error state. The mutation options work the same way in both frameworks, including optimistic, insert, remove, invalidate, and view. See Mutations.
API Differences from React
Views, requests, caching, and mutations work the same way in both adapters. Vue composables return reactive resources; await ready() to use Suspense, or read their pending and error state. For mutations, use fate.mutations and Vue refs for local state instead of React Actions.