From zustand
Guides type-safe Zustand stores in TypeScript: interfaces, typed selectors, getters, async actions, type inference, and best practices.
npx claudepluginhub thebushidocollective/han --plugin zustandThis skill is limited to using the following tools:
Zustand has excellent TypeScript support out of the box. This skill covers type-safe patterns and best practices for using Zustand with TypeScript.
Creates Zustand stores with TypeScript types, subscribeWithSelector middleware, separated state/actions, individual selectors, and non-React subscriptions. For React state management.
Creates and manages Zustand stores for React state management, covering creation, selectors, actions, updates, and performance best practices like shallow equality.
Creates TypeScript Zustand stores using subscribeWithSelector middleware, state/action separation, selectors, and non-React subscriptions for React state management.
Share bugs, ideas, or general feedback.
Zustand has excellent TypeScript support out of the box. This skill covers type-safe patterns and best practices for using Zustand with TypeScript.
Define your store interface and use it with create:
import { create } from 'zustand'
interface BearStore {
bears: number
increasePopulation: () => void
removeAllBears: () => void
updateBears: (newBears: number) => void
}
const useBearStore = create<BearStore>()((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
updateBears: (newBears) => set({ bears: newBears }),
}))
Zustand can infer types automatically:
const useStore = create((set) => ({
count: 0,
text: '',
increment: () => set((state) => ({ count: state.count + 1 })),
setText: (text: string) => set({ text }),
}))
// Types are inferred automatically
type Store = ReturnType<typeof useStore.getState>
// {
// count: number
// text: string
// increment: () => void
// setText: (text: string) => void
// }
Always define explicit interfaces for better type safety and IDE support:
interface User {
id: string
name: string
email: string
}
interface UserStore {
// State
users: User[]
selectedUserId: string | null
isLoading: boolean
error: string | null
// Computed
selectedUser: User | null
// Actions
fetchUsers: () => Promise<void>
selectUser: (id: string) => void
clearSelection: () => void
}
const useUserStore = create<UserStore>()((set, get) => ({
users: [],
selectedUserId: null,
isLoading: false,
error: null,
get selectedUser() {
const { users, selectedUserId } = get()
return users.find((u) => u.id === selectedUserId) ?? null
},
fetchUsers: async () => {
set({ isLoading: true, error: null })
try {
const users = await api.fetchUsers()
set({ users, isLoading: false })
} catch (error) {
set({ error: error.message, isLoading: false })
}
},
selectUser: (id) => set({ selectedUserId: id }),
clearSelection: () => set({ selectedUserId: null }),
}))
Create typed selector functions for reusable logic:
interface TodoStore {
todos: Todo[]
filter: 'all' | 'active' | 'completed'
addTodo: (text: string) => void
toggleTodo: (id: string) => void
setFilter: (filter: TodoStore['filter']) => void
}
const useTodoStore = create<TodoStore>()(/* ... */)
// Typed selector functions
const selectFilteredTodos = (state: TodoStore) => {
if (state.filter === 'all') return state.todos
if (state.filter === 'active') return state.todos.filter((t) => !t.completed)
return state.todos.filter((t) => t.completed)
}
const selectActiveTodoCount = (state: TodoStore) =>
state.todos.filter((t) => !t.completed).length
// Usage
function TodoList() {
const filteredTodos = useTodoStore(selectFilteredTodos)
const activeCount = useTodoStore(selectActiveTodoCount)
return (
<div>
<p>{activeCount} active todos</p>
{filteredTodos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
)
}
Type-safe store slices for large applications:
import { StateCreator } from 'zustand'
interface BearSlice {
bears: number
addBear: () => void
eatFish: () => void
}
interface FishSlice {
fishes: number
addFish: () => void
}
interface SharedSlice {
addBoth: () => void
getBoth: () => number
}
const createBearSlice: StateCreator<
BearSlice & FishSlice,
[],
[],
BearSlice
> = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
})
const createFishSlice: StateCreator<
BearSlice & FishSlice,
[],
[],
FishSlice
> = (set) => ({
fishes: 0,
addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})
const createSharedSlice: StateCreator<
BearSlice & FishSlice,
[],
[],
SharedSlice
> = (set, get) => ({
addBoth: () => {
get().addBear()
get().addFish()
},
getBoth: () => get().bears + get().fishes,
})
const useBoundStore = create<BearSlice & FishSlice & SharedSlice>()(
(...a) => ({
...createBearSlice(...a),
...createFishSlice(...a),
...createSharedSlice(...a),
})
)
Create reusable store factories with generics:
import { create, StoreApi } from 'zustand'
interface AsyncState<T> {
data: T | null
isLoading: boolean
error: string | null
}
interface AsyncActions<T> {
fetch: () => Promise<void>
reset: () => void
}
type AsyncStore<T> = AsyncState<T> & AsyncActions<T>
function createAsyncStore<T>(
fetcher: () => Promise<T>
): StoreApi<AsyncStore<T>> {
return create<AsyncStore<T>>()((set) => ({
data: null,
isLoading: false,
error: null,
fetch: async () => {
set({ isLoading: true, error: null })
try {
const data = await fetcher()
set({ data, isLoading: false })
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Unknown error',
isLoading: false,
})
}
},
reset: () => set({ data: null, isLoading: false, error: null }),
}))
}
// Usage
interface User {
id: string
name: string
}
const useUserStore = createAsyncStore<User[]>(() =>
fetch('/api/users').then((r) => r.json())
)
Type middleware correctly for full type safety:
import { create } from 'zustand'
import { persist, devtools } from 'zustand/middleware'
import type { PersistOptions } from 'zustand/middleware'
interface MyStore {
count: number
increment: () => void
}
type MyPersist = (
config: StateCreator<MyStore>,
options: PersistOptions<MyStore>
) => StateCreator<MyStore>
const useStore = create<MyStore>()(
devtools(
persist(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}),
{
name: 'my-store',
}
)
)
)
interface Entity {
id: string
name: string
createdAt: Date
}
interface CrudStore<T extends Entity> {
items: T[]
selectedId: string | null
isLoading: boolean
error: string | null
// Computed
selectedItem: T | null
// Actions
fetchAll: () => Promise<void>
fetchOne: (id: string) => Promise<void>
create: (data: Omit<T, 'id' | 'createdAt'>) => Promise<void>
update: (id: string, data: Partial<T>) => Promise<void>
delete: (id: string) => Promise<void>
select: (id: string | null) => void
}
function createCrudStore<T extends Entity>(
apiEndpoint: string
): StoreApi<CrudStore<T>> {
return create<CrudStore<T>>()((set, get) => ({
items: [],
selectedId: null,
isLoading: false,
error: null,
get selectedItem() {
const { items, selectedId } = get()
return items.find((item) => item.id === selectedId) ?? null
},
fetchAll: async () => {
set({ isLoading: true, error: null })
try {
const response = await fetch(apiEndpoint)
const items = await response.json()
set({ items, isLoading: false })
} catch (error) {
set({ error: error.message, isLoading: false })
}
},
fetchOne: async (id) => {
set({ isLoading: true, error: null })
try {
const response = await fetch(`${apiEndpoint}/${id}`)
const item = await response.json()
set((state) => ({
items: state.items.some((i) => i.id === id)
? state.items.map((i) => (i.id === id ? item : i))
: [...state.items, item],
isLoading: false,
}))
} catch (error) {
set({ error: error.message, isLoading: false })
}
},
create: async (data) => {
set({ isLoading: true, error: null })
try {
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
const newItem = await response.json()
set((state) => ({
items: [...state.items, newItem],
isLoading: false,
}))
} catch (error) {
set({ error: error.message, isLoading: false })
}
},
update: async (id, data) => {
set({ isLoading: true, error: null })
try {
const response = await fetch(`${apiEndpoint}/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
const updatedItem = await response.json()
set((state) => ({
items: state.items.map((item) =>
item.id === id ? updatedItem : item
),
isLoading: false,
}))
} catch (error) {
set({ error: error.message, isLoading: false })
}
},
delete: async (id) => {
set({ isLoading: true, error: null })
try {
await fetch(`${apiEndpoint}/${id}`, { method: 'DELETE' })
set((state) => ({
items: state.items.filter((item) => item.id !== id),
selectedId: state.selectedId === id ? null : state.selectedId,
isLoading: false,
}))
} catch (error) {
set({ error: error.message, isLoading: false })
}
},
select: (id) => set({ selectedId: id }),
}))
}
// Usage
interface Product extends Entity {
price: number
description: string
}
const useProductStore = createCrudStore<Product>('/api/products')
Use discriminated unions for type-safe action patterns:
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'set'; value: number }
| { type: 'reset' }
interface CounterStore {
count: number
dispatch: (action: Action) => void
}
const useCounterStore = create<CounterStore>()((set) => ({
count: 0,
dispatch: (action) => {
switch (action.type) {
case 'increment':
set((state) => ({ count: state.count + 1 }))
break
case 'decrement':
set((state) => ({ count: state.count - 1 }))
break
case 'set':
set({ count: action.value })
break
case 'reset':
set({ count: 0 })
break
}
},
}))
// Usage with full type safety
const dispatch = useCounterStore((state) => state.dispatch)
dispatch({ type: 'set', value: 10 }) // ✅ Type-safe
dispatch({ type: 'set', value: 'abc' }) // ❌ TypeScript error
Organize related state and actions:
interface Store {
auth: {
user: User | null
token: string | null
login: (credentials: Credentials) => Promise<void>
logout: () => void
}
cart: {
items: CartItem[]
addItem: (item: Product) => void
removeItem: (id: string) => void
clear: () => void
}
}
const useStore = create<Store>()((set) => ({
auth: {
user: null,
token: null,
login: async (credentials) => {
const { user, token } = await api.login(credentials)
set((state) => ({
auth: { ...state.auth, user, token },
}))
},
logout: () =>
set((state) => ({
auth: { ...state.auth, user: null, token: null },
})),
},
cart: {
items: [],
addItem: (product) =>
set((state) => ({
cart: {
...state.cart,
items: [...state.cart.items, { ...product, quantity: 1 }],
},
})),
removeItem: (id) =>
set((state) => ({
cart: {
...state.cart,
items: state.cart.items.filter((item) => item.id !== id),
},
})),
clear: () =>
set((state) => ({
cart: { ...state.cart, items: [] },
})),
},
}))
// Usage
const login = useStore((state) => state.auth.login)
const cartItems = useStore((state) => state.cart.items)
Create a typed event system:
type Events = {
'user:login': { userId: string; timestamp: Date }
'user:logout': { userId: string }
'cart:add': { productId: string; quantity: number }
'cart:remove': { productId: string }
}
type EventListener<T extends keyof Events> = (data: Events[T]) => void
interface EventStore {
listeners: {
[K in keyof Events]?: EventListener<K>[]
}
on: <T extends keyof Events>(event: T, listener: EventListener<T>) => void
off: <T extends keyof Events>(event: T, listener: EventListener<T>) => void
emit: <T extends keyof Events>(event: T, data: Events[T]) => void
}
const useEventStore = create<EventStore>()((set, get) => ({
listeners: {},
on: (event, listener) => {
set((state) => ({
listeners: {
...state.listeners,
[event]: [...(state.listeners[event] || []), listener],
},
}))
},
off: (event, listener) => {
set((state) => ({
listeners: {
...state.listeners,
[event]: (state.listeners[event] || []).filter((l) => l !== listener),
},
}))
},
emit: (event, data) => {
const listeners = get().listeners[event] || []
listeners.forEach((listener) => listener(data))
},
}))
any Types// Bad
const useStore = create<any>()((set) => ({
data: null,
setData: (data: any) => set({ data }),
}))
// Good
interface Store {
data: User | null
setData: (data: User | null) => void
}
const useStore = create<Store>()((set) => ({
data: null,
setData: (data) => set({ data }),
}))
// Bad: Ignoring return type
const useStore = create((set) => ({
fetch: async () => {
const data = await api.fetch()
set({ data })
// Missing return type
},
}))
// Good: Explicit return types
interface Store {
fetch: () => Promise<void>
}
const useStore = create<Store>()((set) => ({
fetch: async (): Promise<void> => {
const data = await api.fetch()
set({ data })
},
}))
// Bad: Confusing structure
interface Store {
count: number
increment: () => void
name: string
setName: (name: string) => void
}
// Good: Separate concerns
interface StoreState {
count: number
name: string
}
interface StoreActions {
increment: () => void
setName: (name: string) => void
}
type Store = StoreState & StoreActions