Vue
fate also supports Vue through vue-fate. It exports the same core primitives as react-fate where Vue has a natural equivalent: view, useRequest, useView, useListView, useLiveView, useLiveListView, useFateClient, and the FateClient provider.
Vue components use fate through Vue resources built from refs, computed values, watchers, and <Suspense>. The view model, generated client, normalized cache, masking, request shapes, list views, live views, and mutations are shared with the React adapter.
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 { fate } from 'vue-fate/vite';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
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,
});Vue can import values across components, but single-file components have one default component export. Keeping reusable views in .ts files avoids coupling your data model to component files and makes view composition straightforward.
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,
},
});
const { posts } = await request.ready();
const [postItems, loadNext] = useListView(PostCardView, 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. If you do not await it, read request.data.value, request.pending.value, and request.error.value in script, or use the refs directly in templates.
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.
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);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);Manual cleanup works through dispose():
const post = useLiveView(PostCardView, () => props.post);
onBeforeUnmount(() => {
post.dispose();
});Vue scope disposal also cleans up resources automatically.
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 {
await fate.mutations.post.like({
input: { id: props.post.id },
optimistic: { likes: props.post.likes + 1 },
});
} catch (caughtError) {
error.value = caughtError;
} finally {
pending.value = false;
}
};
</script>
<template>
<button :disabled="pending" @click="like">Like</button>
</template>The mutation call shape is the same as react-fate: input, optimistic, insert, and view work the same way. Vue does not have React Actions or useActionState, so Vue components should model pending and error state with Vue refs or your application state library.
API Differences from React
The names intentionally mirror react-fate where Vue has an equivalent API. The main differences are Vue framework differences:
useRequest,useView, and list hooks return Vue resources instead of throwing promises from render.- Async setup and
<Suspense>replace React's async component model. - Mutations use
fate.mutationsdirectly; React-onlyfate.actionsanduseActionStatepatterns do not apply. - Shared views are best kept in
.tsmodules instead of exporting named views from component files.
The generated client, server integrations, cache behavior, masking, pagination, optimistic updates, and live transport behavior are shared across adapters.