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.