Vaxee: A Simpler, Fetch-Aware State Manager for Vue and Nuxt
When you think of state management in Vue.js or Nuxt.js, Pinia is the go-to library. It’s solid — but when working in Nuxt, managing data fetching and SSR often feels clunky. To keep server/client state in sync, I usually end up mixing callOnce(), useFetch(), or useAsyncData() in awkward ways.
Recently, I discovered Vaxee — a lightweight state manager that treats fetching as a first-class concept. It simplifies store logic, server-side fetching, and hydration — all while staying small and intuitive.
What Makes Vaxee Special
Vaxee gives you a compact createStore() factory and three simple primitives:
state()— reactive state (like Vue’sref()) with extra options like persistence.getter()— computed-style derivations (likecomputed()).request()— an async helper with the feel of Nuxt’suseAsyncData()but defined directly inside the store.
The magic lies in request(): it keeps fetching logic inside the store, exposing helpers like data, refresh, and status for easy use in pages.
Defining a Store
Vaxee’s API centers around createStore(name, setup), where all your state, getters, and requests live together.
import { createStore } from 'vaxee';
export const useUserStore = createStore('user', ({ state, getter, request }) => {
const tokens = state({ access: '', refresh: '' }, { persist: 'user.tokens' });
const user = request(({signal}) => $fetch('/api/user', { signal }));
const fullName = getter(() => user.data.value ? `${user.data.value?.first_name} ${user.data.value?.last_name}` : undefined);
return { tokens, fullName, user };
});
This single file holds everything: state, derived data, and fetch logic.
The request() Advantage
Unlike Pinia, where i often need to take some data fetching logics outside the store, request() keeps it internal. Each request exposes:
data,error,execute,status,refresh()etc for easy use in components and pages.
It even supports parameters:
const user = request<User, { id: number }>(({ signal, params }) =>
$fetch(`/api/user/${params.id}`, { signal }));
<script setup lang="ts">
const route = useRoute();
const { user: { execute } } = useUserStore();
await execute({ id: route.params.id });
</script>
Full Example — Store + Page
import { createStore } from 'vaxee';
export const useUserStore = createStore('user', ({ state, getter, request }) => {
const tokens = state({ access: '', refresh: '' }, { persist: 'user.tokens' });
const user = request(({signal}) => $fetch('/api/user', { signal }));
const fullName = getter(() => user.data.value ? `${user.data.value?.first_name} ${user.data.value?.last_name}` : undefined);
return { tokens, fullName, user };
});
<script setup lang="ts">
const { user: { data, execute }, fullName } = useUserStore();
await execute();
</script>
<template>
<div v-if="data">
<h1>{{ fullName }}</h1>
<p>{{ data.email }}</p>
</div>
</template>
This setup keeps the fetch logic in the store while the page simply consumes the data.
TL;DR
- Pinia: Reliable, flexible, but fetch coordination in Nuxt can be verbose.
- Vaxee: Minimal and fetch-aware by design.
- If you want cleaner, colocated state + fetch logic — give Vaxee a try.