@amirafa/vue3-global-store

SSR-safe global store utility for Vue 3 and Nuxt 3
526
Install
npm install @amirafa/vue3-global-store
Latest Version:1.0.2
Vue:3
License:MIT
Last Updated:Sep 14, 2026
Links: npm
Maintainer: amirafa

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, and reset.
  • 🧊 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:

  1. The state definition.
  2. The defineGlobalStore() call.
  3. 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 DeepReadonly support for complex states.
  • Fixed TypeScript compatibility problems during immutable updates.

Related Packages

@quantajs/vue

Vue bindings for QuantaJS stores.

347 11
nuxt-spa-store-init

A simple Nuxt module that will hydrate the VueX store when running in SPA mode (...

4,282 14
nuxt-ssr-api-logger

Nuxt module for logging SSR API requests.

22,791 11
@thunderid/nuxt

Nuxt SDK for ThunderID

6,771 7
nuxt-lazy-hydration

Lazy hydration of server-side rendered Vue.js components

2,795

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