A thin wrapper around Zustand that generates getters, setters, and hooks for you.
import { createStore } from 'zustand-lite'
const store = createStore({ count: 0 })
.extendSetters(({ get, set }) => ({
increment: () => set.count(get().count + 1),
}))
store.use.count() // React hook
store.get().count // Direct access
store.set.increment() // ActionThat's it. No providers, no boilerplate, full TypeScript support.
Zustand Lite is a zero-boilerplate state management built specifically for frontend developers who want powerful and scalable global state without the usual complexity. Designed for simplicity, it gives you everything you need out-of-the-box — from selectors to setters to middleware — while remaining lightweight and extensible. With seamless support for plugins, devtools, and state encapsulation, managing state becomes a breeze, not a chore.
Zustand is great, but you still end up writing repetitive code: selectors for each field, actions that follow the same patterns, hooks that look almost identical.
Zustand Lite fixes that. You define your state once, and it generates:
store.use.fieldName()- React hooks with proper subscriptionsstore.set.fieldName(value)- Type-safe settersstore.get()- Synchronous access to current state
Plus a chainable API for computed values, custom actions, and plugins.
npm install zustand-lite zustandimport { createStore } from 'zustand-lite'
export const store = createStore({ name: '', email: '' })
// In your component
function Profile() {
const name = store.use.name()
const email = store.use.email()
return <div>{name} ({email})</div>
}
// Update from anywhere
store.set.name('John')
store.set.email('john@example.com')const store = createStore({ count: 0 })
.extendSetters(({ get, set }) => ({
increment: () => set.count(get().count + 1),
decrement: () => set.count(get().count - 1),
reset: () => set.count(0),
}))
// Use them directly
store.set.increment()
store.set.reset()
// Or in components
<button onClick={store.set.increment}>+</button>const store = createStore({
items: [] as { price: number; qty: number }[]
})
.extendGetters(({ get }) => ({
total: () => get().items.reduce((sum, item) => sum + item.price * item.qty, 0),
itemCount: () => get().items.length,
}))
// Computed values work as hooks too
function CartSummary() {
const total = store.use.total()
const count = store.use.itemCount()
return <div>{count} items, ${total}</div>
}Nested state? No problem. Zustand Lite auto-generates deep selectors:
const store = createStore({
user: {
profile: {
name: 'John'
}
}
})
// These are all valid and properly subscribed
store.use.user()
store.use.user.profile()
store.use.user.profile.name()const store = createStore({ a: 1, b: 2, c: 3, d: 4 })
function Component() {
// Only re-renders when a or c change
const { a, c } = store.use(['a', 'c'])
}const store = createStore({ data: { id: 1, name: 'test', updatedAt: Date.now() } })
// Default uses shallow equality
const data = store.use.data()
// Custom equality for auto-generated selectors
const data = store.use.data((a, b) => a.id === b.id)
// Custom equality for ad-hoc selectors
const data = store.use(
(state) => state.data,
(a, b) => a.id === b.id
)For getters with parameters, pass { eq } as the last argument:
const store = createStore({ items: [{ id: 1, name: 'Item', meta: {} }] })
.extendGetters(({ get }) => ({
getById: (id: number) => get().items.find(i => i.id === id),
}))
function Item({ id }: { id: number }) {
// Only re-render when id or name changes, ignore meta
const item = store.use.getById(id, {
eq: (a, b) => a?.id === b?.id && a?.name === b?.name
})
}Add more state fields after creation:
const store = createStore({ a: 'a' })
.extendByState({ b: 'b' }) // Plain object
.extendByState(({ get }) => ({ c: get().a + get().b })) // Derived from existing stateChain multiple extendGetters or extendSetters to override previous definitions. The new definition can access the previous one via get.previousGetter():
const store = createStore({ price: 100 })
.extendGetters(({ get }) => ({
displayPrice: () => get().price,
}))
.extendGetters(({ get }) => ({
// Override: add currency formatting, but use previous getter
displayPrice: () => `$${get.displayPrice().toFixed(2)}`,
}))
store.get.displayPrice() // "$100.00"Multiple ways to update state:
const store = createStore({ a: 1, b: 2 })
// Auto-generated setters
store.set.a(10)
// Partial update (shallow merge)
store.set({ a: 10 })
// Function update
store.set((state) => ({ a: state.a + 1 }))
// Replace entire state (second arg = true)
store.set({ a: 100, b: 200 }, true)Sometimes you want internal state that components can't access directly:
const store = createStore({
publicValue: 'visible',
_internalCache: new Map(),
})
.extendGetters(({ get }) => ({
getCached: (key: string) => get()._internalCache.get(key),
}))
.restrictState(['_internalCache'])
// Works
store.get().publicValue
store.get.getCached('key')
// TypeScript error - _internalCache is hidden
store.get()._internalCacheExtract reusable patterns into plugins:
import { definePlugin } from 'zustand-lite'
// A loading state plugin
const withLoading = definePlugin((store) =>
store
.extendByState({ isLoading: false, error: null as string | null })
.extendSetters(({ set }) => ({
startLoading: () => { set.isLoading(true); set.error(null) },
stopLoading: () => set.isLoading(false),
setError: (error: string) => { set.error(error); set.isLoading(false) },
}))
)
// Use it
const store = createStore({ data: null })
.composePlugin(withLoading)
.extendSetters(({ set }) => ({
async fetchData() {
set.startLoading()
try {
const data = await api.getData()
set.data(data)
} catch (e) {
set.setError(e.message)
} finally {
set.stopLoading()
}
},
}))const store = createStore(
{ count: 0 },
{
name: 'CounterStore',
middlewares: {
devtools: true, // Redux DevTools integration
persist: true, // localStorage persistence
},
}
)Actions show up in DevTools with clear labels like CounterStore/count or CounterStore/myOwnAction.
Creates a store with auto-generated hooks and setters.
Options:
| Key | Type | Description |
|---|---|---|
name |
string |
Name for DevTools |
middlewares |
{ devtools?, persist? } |
Enable middleware |
| Method | Description |
|---|---|
.extendByState(obj | fn) |
Add more state fields |
.extendGetters(fn) |
Add computed values |
.extendSetters(fn) |
Add custom actions |
.composePlugin(plugin) |
Apply a plugin |
.restrictState(keys?) |
Hide fields from public API |
| Property | Description |
|---|---|
store.use.field() |
React hook for field |
store.use(selector, eq?) |
Hook with custom selector |
store.use(['a', 'b']) |
Hook for multiple fields |
store.get() |
Current state (no subscription) |
store.get.getter() |
Call a computed getter |
store.set.field(value) |
Update a field |
store.set(partial) |
Merge partial state |
store.set(fn) |
Update with function |
store.api |
Raw Zustand API |
No mocks needed. Just use the store directly:
import { store } from './store'
test('increment works', () => {
store.set.count(0)
store.set.increment()
expect(store.get().count).toBe(1)
})
test('component updates', () => {
render(<Counter />)
act(() => store.set.count(5))
expect(screen.getByText('5')).toBeInTheDocument()
})- Custom equality for parameterized getters
- Auto-generate deep setters (currently only first level)
- Subscribe with selector middleware
Built on Zustand. Inspired by zustand-x.
MIT
