pinia-lite
v1.0.0
Published
A tiny, dependency-free reimplementation of the core Pinia API (defineStore, storeToRefs, createPinia) for Vue 3.
Maintainers
Readme
pinia-lite
A tiny, dependency-free reimplementation of the core Pinia API for Vue 3 — defineStore, storeToRefs, and a createPinia() plugin — in about 60 lines of code.
It covers the basics you reach for day to day: a single shared store instance per id, reactive state without .value boilerplate, and cleanup when a component unmounts. It does not aim to replace Pinia — no devtools, no plugins/middleware, no SSR story, no $patch/$reset, no options-API store syntax. If you need those, use real Pinia.
Install
npm install pinia-litevue (^3.0.0) is a peer dependency.
Quick start
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia-lite'
import App from './App.vue'
createApp(App).use(createPinia()).mount('#app')// stores/counter.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia-lite'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, double, increment }
})<!-- AnyComponent.vue -->
<script setup>
import { useCounterStore } from './stores/counter'
const counter = useCounterStore()
</script>
<template>
<button @click="counter.increment()">{{ counter.count }} / {{ counter.double }}</button>
</template>No .value needed in templates or in counter.count — the store is wrapped in reactive() for you, same as Pinia.
How it works
- One store instance per id.
defineStore(id, setup)returns auseXxxStore()composable. The first call runs your setup function inside aneffectScopeand stores the result in a module-level registry (Map), keyed byid. Every later call touseXxxStore()— from any component — returns that same reactive instance. - Reactive by default. The raw object your setup function returns is wrapped in
reactive(), so you can destructure-and-use its properties directly instead of writing.valueeverywhere refs normally require. Use the exportedstoreToRefs(store)(a thin wrapper over Vue'stoRefs) when you do want individual refs, e.g. for destructuring in<script setup>without losing reactivity. - Cleanup via
$dispose(). Every store gets a$dispose()method that stops itseffectScope(tearing down anywatch/computedit created), removes it from the registry, and blanks out any ref-backed state. By default this runs automatically inonUnmounted()if the store was created inside a component — pass{ autoDispose: false }to opt out if you want the store to outlive the component that first created it. createPinia()is a real Vue plugin.app.use(createPinia())provides the pinia instance and setsapp.config.globalProperties.$pinia, matching Pinia's install API. It also wrapsapp.unmount()so every store still in the registry gets disposed when the app itself is torn down — handy for tests and hot-module reloads.
API
| Export | Description |
|---|---|
| defineStore(id, setup) | Returns a useStore(options?) composable. options.autoDispose (default true) controls whether the store disposes itself on the owning component's unmount. |
| storeToRefs(store) | Converts a reactive store into an object of individual refs, for destructuring without losing reactivity. |
| createPinia() | Returns a Vue plugin object ({ install }) with a $disposeAll() escape hatch that tears down every store currently in the registry. |
What's intentionally missing
Compared to Pinia, pinia-lite does not provide:
- Devtools integration
- Plugins (
pinia.use(...)) $patch/$reset/$subscribe/$onAction- Options-API store syntax (
defineStore(id, { state, getters, actions })) - SSR-safe per-request store isolation (the registry is a single module-level
Map, shared across everyappinstance)
If your project needs any of the above, reach for Pinia itself — this project is meant for small apps/prototypes where a full state library is more than you need.
License
MIT
