@amirafa/vue3-global-store
vue3-global-store
A tiny, dependency-free global state manager for Vue 3 and Nuxt 3, built on top of Vue's reactivity system.
Define stores once with defineGlobalStore, install the plugin with createGlobalStorePlugin, and access reactive state anywhere inside your Vue app — without Pinia, Vuex, or boilerplate.
📦 Overview
vue3-global-store provides application-wide reactive state through a small, typed API:
defineGlobalStore(key, initialState)— recommended way to define reusable stores.useGlobalStore(key, initialState)— direct accessor for creating or retrieving a store.createGlobalStorePlugin()— installs an app-scoped store registry.
The registry is provided through Vue's provide / inject system, so each Vue application gets its own store container. This makes the architecture suitable for SSR applications such as Nuxt 3.
The recommended pattern
Define a store once:
// stores/auth.ts
import { defineGlobalStore } from "vue3-global-store";
export const useAuthStore = defineGlobalStore("auth", {
user: null as null | {
id: number;
name: string;
},
token: "",
loggedIn: false,
});
Then consume it anywhere inside the application:
const auth = useAuthStore();
auth.state.loggedIn;
auth.patch({
loggedIn: true,
});
The initial state lives in one place and is not repeated throughout your components.
✨ Features
- 🪶 Zero dependencies — built entirely on Vue 3's reactivity system.
- 🧩 Plugin-based — install once with
app.use(createGlobalStorePlugin()). - 🏭
defineGlobalStore— define a reusable, typed store once. - 🎯
useGlobalStore— directly create or retrieve a named store. - 🔄 Simple store API —
state,set,patch,update, andreset. - 🧊 TypeScript-friendly — strongly typed state and store operations.
- 🖥️ SSR-safe registry — stores are kept inside the Vue app's provided registry rather than a module-level singleton.
- 📦 Primitive, object, and array states — supports different state shapes while preserving Vue reactivity.
- 🪶 Small API — intentionally avoids the complexity of a full state-management framework.
⚙️ Installation
npm install vue3-global-store
Or:
yarn add vue3-global-store
⚙️ Vue 3 Setup
Install the plugin when creating your application:
// main.ts
import { createApp } from "vue";
import App from "./App.vue";
import { createGlobalStorePlugin } from "vue3-global-store";
const app = createApp(App);
app.use(createGlobalStorePlugin());
app.mount("#app");
The plugin creates a store registry for that Vue application.
⚙️ Nuxt 3 Setup
Create a Nuxt plugin:
// plugins/global-store.ts
import { createGlobalStorePlugin } from "vue3-global-store";
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(createGlobalStorePlugin());
});
Keep the plugin universal:
plugins/
└── global-store.ts
Do not use .client.ts if you need the store during SSR.
📖 API Reference
createGlobalStorePlugin()
Creates the Vue plugin responsible for providing the app-scoped store registry.
const plugin = createGlobalStorePlugin();
app.use(plugin);
The plugin does not require any options.
Internally, each Vue app receives its own registry:
Vue App
│
└── Global Store Registry
├── auth
├── cart
└── preferences
This avoids keeping the registry in module-level state.
defineGlobalStore<S>(key, initialState)
Defines a named store and returns a reusable composable.
import { defineGlobalStore } from "vue3-global-store";
interface CounterState {
count: number;
}
export const useCounterStore =
defineGlobalStore<CounterState>("counter", {
count: 0,
});
Use it anywhere inside the Vue application's setup/injection context:
const counter = useCounterStore();
counter.state.count++;
counter.set({
count: 10,
});
counter.reset();
Recommended usage
Put store definitions in dedicated modules:
stores/
├── auth.ts
├── cart.ts
└── preferences.ts
For example:
// stores/cart.ts
import { defineGlobalStore } from "vue3-global-store";
export interface CartState {
items: {
id: number;
name: string;
quantity: number;
}[];
}
export const useCartStore =
defineGlobalStore<CartState>("cart", {
items: [],
});
Then components simply use:
const cart = useCartStore();
No initial state needs to be repeated.
useGlobalStore<S>(key, initialState)
Directly creates or retrieves a store from the current app registry.
import { useGlobalStore } from "vue3-global-store";
const store = useGlobalStore("counter", {
count: 0,
});
If a store with the same key already exists, the existing store is returned.
The initialState is therefore only used when the key does not already exist in that app's registry.
Example
const first = useGlobalStore("cart", {
items: ["apple"],
});
const second = useGlobalStore("cart", {
items: ["banana"],
});
Both references point to the same store:
first === second; // true
The second initial state is ignored.
Recommendation
For stores shared between multiple components, prefer:
defineGlobalStore(...)
instead of repeating:
useGlobalStore(...)
at multiple call sites.
This gives you one canonical definition for the store.
🚀 Basic Usage
Defining a store
// stores/auth.ts
import { defineGlobalStore } from "vue3-global-store";
export const useAuthStore = defineGlobalStore("auth", {
user: null as null | {
id: number;
name: string;
},
token: "",
loggedIn: false,
});
Using it in a component
<script setup lang="ts">
import { useAuthStore } from "~/stores/auth";
const auth = useAuthStore();
</script>
<template>
<p v-if="auth.state.loggedIn">
Hello, {{ auth.state.user?.name }}
</p>
</template>
🧩 Store API
Every store provides:
| Property / Method | Signature | Description |
|---|---|---|
state |
T or Ref<T> |
Reactive store state |
set |
set(next: T) |
Replace the complete state while preserving the reactive reference |
patch |
patch(partial: Partial<T>) |
Shallow-merge properties into an object store |
update |
update(fn) |
Create a new state from a readonly snapshot |
reset |
reset() |
Restore the original initial state |
state
Object and array stores expose their reactive state directly:
const counter = useGlobalStore("counter", {
count: 0,
});
counter.state.count++;
For primitive stores, state is a Vue Ref:
const counter = useGlobalStore("counter", 0);
counter.state.value++;
This distinction exists because Vue's reactive() cannot be used for primitive values.
set(next)
set() replaces the store's state.
const store = useGlobalStore("user", {
name: "Amir",
age: 25,
});
store.set({
name: "Ali",
age: 30,
});
For object and array stores, the existing reactive object/array is mutated rather than replacing the reactive reference itself.
This means existing consumers continue observing the same reactive state.
For primitive stores:
const count = useGlobalStore("count", 0);
count.set(10);
console.log(count.state.value); // 10
set() expects the complete state.
It is not a partial update.
patch(partial)
patch() performs a shallow merge on object stores.
const store = useGlobalStore("user", {
name: "Amir",
age: 25,
});
store.patch({
name: "Ali",
});
The resulting state is:
{
name: "Ali",
age: 25,
}
Nested objects are not deeply merged.
patch() and arrays
patch() is intentionally not supported for array stores.
For arrays, use set() or update():
const cart = useGlobalStore("cart", [
"apple",
"banana",
]);
cart.update((items) => [
...items,
"coffee",
]);
update(updater)
update() receives a cloned, deeply readonly snapshot of the current state and expects the updater to return the next state.
const counter = useGlobalStore("counter", {
count: 0,
});
counter.update((current) => ({
...current,
count: current.count + 1,
}));
For nested objects:
const user = useGlobalStore("user", {
profile: {
name: "Amir",
},
});
user.update((current) => ({
...current,
profile: {
...current.profile,
name: "Ali",
},
}));
For arrays:
const cart = useGlobalStore("cart", {
items: [] as string[],
});
cart.update((current) => ({
...current,
items: [...current.items, "Coffee"],
}));
The updater receives a snapshot rather than the actual reactive state.
This prevents accidental direct mutation of the store inside the updater.
reset()
Restores the store to its original initial state.
const settings = useGlobalStore("settings", {
theme: "light",
locale: "en",
});
settings.patch({
theme: "dark",
});
settings.reset();
The state is restored to:
{
theme: "light",
locale: "en",
}
The initial state is cloned when the store is created, so later mutations do not modify the reset value.
🧪 Examples
Auth Store
// stores/auth.ts
import { defineGlobalStore } from "vue3-global-store";
export interface AuthState {
user: null | {
id: number;
name: string;
};
token: string;
loggedIn: boolean;
}
export const useAuthStore =
defineGlobalStore<AuthState>("auth", {
user: null,
token: "",
loggedIn: false,
});
export async function login(
email: string,
password: string
) {
const auth = useAuthStore();
const response = await api.login(
email,
password
);
auth.set({
user: response.user,
token: response.token,
loggedIn: true,
});
}
export function logout() {
useAuthStore().reset();
}
Cart Store
// stores/cart.ts
import { defineGlobalStore } from "vue3-global-store";
export interface CartItem {
id: number;
name: string;
quantity: number;
}
export interface CartState {
items: CartItem[];
}
export const useCartStore =
defineGlobalStore<CartState>("cart", {
items: [],
});
export function addItem(item: CartItem) {
const cart = useCartStore();
cart.update((current) => ({
...current,
items: [
...current.items,
item,
],
}));
}
Use it in a component:
import {
useCartStore,
addItem,
} from "~/stores/cart";
const cart = useCartStore();
addItem({
id: 1,
name: "Coffee",
quantity: 1,
});
Preferences Store
import { defineGlobalStore } from "vue3-global-store";
export type Theme = "light" | "dark";
export interface PreferencesState {
theme: Theme;
locale: string;
}
export const usePreferencesStore =
defineGlobalStore<PreferencesState>(
"preferences",
{
theme: "light",
locale: "en",
}
);
Usage:
const preferences = usePreferencesStore();
preferences.patch({
theme: "dark",
});
preferences.patch({
locale: "fa",
});
🏗️ Recommended Project Structure
A simple project can organize stores like this:
stores/
├── auth.ts
├── cart.ts
├── preferences.ts
└── user.ts
Each store module should generally contain:
- The state definition.
- The
defineGlobalStore()call. - Related actions/helpers.
Example:
// stores/user.ts
export const useUserStore = defineGlobalStore(
"user",
{
id: null as number | null,
name: "",
}
);
export function clearUser() {
useUserStore().reset();
}
This keeps components focused on UI rather than global state management.
🖥️ SSR and Nuxt 3
The registry is provided by the Vue application:
app.provide(registryKey, registry);
Instead of using a module-level singleton such as:
const registry = new Map();
This is important for SSR because server applications can handle multiple requests in the same process.
The store registry is therefore scoped to the Vue app instance rather than being shared globally by the module.
Important
Use stores within an active Vue injection context:
<script setup lang="ts">
const auth = useAuthStore();
</script>
or from composables/actions that are called from a valid Vue setup context.
Avoid resolving stores at module initialization time:
// Avoid this pattern
const auth = useAuthStore();
when it executes as the module itself is imported rather than during an active Vue application context.
🔐 SSR State Isolation
Each Vue application receives its own registry:
Request A
└── Vue App A
└── Registry A
└── auth store
Request B
└── Vue App B
└── Registry B
└── auth store
The two registries are independent.
This prevents a module-level global registry from accidentally sharing state between SSR requests.
For Nuxt, keep the plugin universal when stores are needed during SSR:
plugins/
└── global-store.ts
Do not rename it to:
global-store.client.ts
unless you intentionally want the plugin to run only on the client.
🧊 State Requirements
The store uses structuredClone() internally.
For predictable behavior, store state should primarily consist of:
- primitives
- plain objects
- arrays
- nested combinations of these
For example:
defineGlobalStore("user", {
id: 1,
name: "Amir",
roles: ["admin", "user"],
preferences: {
theme: "dark",
},
});
Avoid using functions, DOM elements, symbols, or other values that cannot be structured-cloned as store state.
The library intentionally keeps the state model simple rather than trying to support every possible JavaScript object type.
🧭 defineGlobalStore vs useGlobalStore
Both APIs return the same StoreAPI<S>.
The difference is how the store is defined.
defineGlobalStore
Recommended for reusable application stores:
export const useCartStore =
defineGlobalStore("cart", {
items: [],
});
Components then use:
const cart = useCartStore();
The initial state exists in one location.
useGlobalStore
Useful for direct or ad-hoc access:
const cart = useGlobalStore("cart", {
items: [],
});
The first call for a given key creates the store.
Later calls return that same store:
const a = useGlobalStore("cart", {
items: ["apple"],
});
const b = useGlobalStore("cart", {
items: ["banana"],
});
a === b; // true
The second initial state is ignored.
Rule of thumb
| Situation | Recommended API |
|---|---|
| Shared application store | defineGlobalStore |
| Store used by multiple components | defineGlobalStore |
| Store with related actions | defineGlobalStore |
| One-off/ad-hoc store | useGlobalStore |
| Direct access when a store definition already exists | exported composable |
For most application-level stores, prefer defineGlobalStore.
🧰 Registry Helpers
The library also exposes helpers for inspecting and managing the current app's registry.
hasStore(key)
hasStore("auth");
Returns:
boolean
listStores()
listStores();
Returns the registered keys:
[
"auth",
"cart",
"preferences",
]
removeStore(key)
removeStore("cart");
Returns:
boolean
true means a store was removed.
clearStores()
clearStores();
Removes all stores from the current app registry.
These helpers operate on the same app-scoped registry used by useGlobalStore().
🧠 TypeScript
The main generic is the state type:
interface CartState {
items: string[];
}
const useCartStore =
defineGlobalStore<CartState>("cart", {
items: [],
});
The returned composable is fully typed:
const cart = useCartStore();
cart.state.items;
cart.patch({
items: ["Coffee"],
});
TypeScript also catches invalid state replacements:
cart.set({
items: ["Coffee"],
});
StoreAPI<S>
StoreAPI<S> describes an already-created store:
import type {
StoreAPI,
} from "vue3-global-store";
function getCart(
store: StoreAPI<CartState>
) {
return store.state.items;
}
You normally don't need to write StoreAPI<S> when using the library directly because TypeScript infers it.
Use it when writing reusable utilities, explicit annotations, mocks, or return types.
🧱 Direct Mutation vs update()
Object and array state is reactive and can technically be mutated directly:
const cart = useCartStore();
cart.state.items.push({
id: 1,
name: "Coffee",
quantity: 1,
});
This works because state is a Vue reactive object.
However, for more complex state transitions, update() can make the intended change clearer:
cart.update((current) => ({
...current,
items: [
...current.items,
{
id: 1,
name: "Coffee",
quantity: 1,
},
],
}));
Both approaches are valid.
The library does not enforce immutable state updates.
⚖️ Design Philosophy
vue3-global-store intentionally stays small.
It provides:
Reactive State
+
App-scoped Registry
+
Small Store API
=
Lightweight Global State
It does not try to replace a full state-management framework.
There are no built-in:
- actions system
- middleware
- persistence
- devtools integration
- plugins per store
- request caching
- server synchronization
If your application needs those features, a larger state-management solution may be more appropriate.
For applications that simply need shared reactive state with a small API, this library keeps the implementation lightweight.
📚 API Summary
import {
createGlobalStorePlugin,
defineGlobalStore,
useGlobalStore,
hasStore,
listStores,
removeStore,
clearStores,
} from "vue3-global-store";
Plugin
createGlobalStorePlugin();
Store definition
defineGlobalStore<S>(
key: string,
initialState: S
): () => StoreAPI<S>;
Direct store access
useGlobalStore<S>(
key: string,
initialState: S
): StoreAPI<S>;
Registry
hasStore(key: string): boolean;
listStores(): string[];
removeStore(key: string): boolean;
clearStores(): void;
Store
store.state;
store.set(next);
store.patch(partial);
store.update(updater);
store.reset();
💡 Recommended Pattern
For most applications:
// stores/cart.ts
import {
defineGlobalStore,
} from "vue3-global-store";
export interface CartState {
items: string[];
}
export const useCartStore =
defineGlobalStore<CartState>("cart", {
items: [],
});
export function addItem(item: string) {
useCartStore().update((current) => ({
...current,
items: [
...current.items,
item,
],
}));
}
Then in components:
const cart = useCartStore();
This gives you a single source of truth for the store definition while keeping components simple.
📄 License
MIT
📋 Changelog
v1.0.2
Fixed
- Fixed
update()type issues with readonly states, arrays, and nested objects. - Improved TypeScript support for immutable state updates.
v1.0.1
Fixed
- Fixed
update()type issues with arrays and nested objects. - Improved
DeepReadonlysupport for complex states. - Fixed TypeScript compatibility problems during immutable updates.
Related Packages
A simple Nuxt module that will hydrate the VueX store when running in SPA mode (...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 1.0.2 | >=3.0.0 <4.0.0 | MIT | |
| 1.0.1 | >=3.0.0 <4.0.0 | MIT | |
| 1.0.0 | >=3.0.0 <4.0.0 | MIT |