@sinakhx/useZustandStore
custom helpers for using zustand in react apps. it can be used for creating local (component scoped) stores using Zustand. So that:
- you won't need to worry about garbage collecting your store on page components' unmount lifecycle.
- you can get rid of using multiple selectors to acces different parts of the store (as it's using react-tracked under the hood)
- you avoid making your codebase weird with currying, Providers, mind-boggling type annotations, etc.
Installation
npm install @sinakhx/use-zustand-store
Usage
Creating a store is exactly the same way as creating a store in Zustand. You only need to change Zustand's create
function with this library's createZustandStore
function. Everything else is the same. (It's just a wrapper to avoid nesting due to currying)
Example counter app:
counterStore.ts
import { createZustandStore, mutateStoreItem } from '@sinakhx/use-zustand-store'
interface ICounterStore {
count: number
increment: () => void
}
export const counterStore = createZustandStore<ICounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
CounterComponent.tsx
import { useZustandStore } from '@sinakhx/use-zustand-store'
import { counterStore } from './counterStore'
const CounterComponent = () => {
const store = useZustandStore(counterStore)
return <button onClick={store.increment}>{store.count}</button>
}
export default CounterComponent
Now the store is bound to the component. By changing the page route (unmounting the component), the store gets garbage collected & by going back to the page (mounting the component again), a fresh store is created.
That's done! Happy coding!
Simpler store mutations
Instead of using Immer or nested destructuring to mutate the store, you can use the mutateStoreItem
helper.
The following example demonstrates how to reduce multiple useState
hooks to a single store.
tableStore.ts
import { createZustandStore, mutateStoreItem } from '@sinakhx/use-zustand-store'
type TableRow = {
id: number
name: string
age: number
}
interface ITableStore {
rows: Array<TableRow>
setRows: (rows: TableRow[]) => void
selectedRow: TableRow | null
setSelectedRow: (row: TableRow | null) => void
handleDeleteRow: (id: number) => void
}
const counterStore = createZustandStore<ITableStore>((set, get) => ({
rows: [],
setRows: (rows) => set(mutateStoreItem({ rows })),
selectedRow: null,
setSelectedRow: (row) => set(mutateStoreItem({ selectedRow: row })),
handleDeleteRow: (id) => {
const newRows = get().rows.filter((row) => row.id !== id)
get().setRows(newRows)
},
}))
mutateStoreItem
is using optics-ts to access the store's state. As a result one can also easily mutate a nested store item by providing its path as object key. e.g: set(mutateStoreItem({ 'user.info.name': 'John' }))
.
Advanced usage: initializing store with props
counterStore.ts
import { createZustandStore } from '@sinakhx/use-zustand-store'
interface ICounterStore {
count: number
increment: () => void
}
interface ICounterProps {
initialCount: number
}
export const counterStoreFactory = ({ initialCount } : ICounterProps) => createZustandStore<ICounterStore>((set) => ({
count: initialCount,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
CounterComponent.tsx
import { useZustandStore } from '@sinakhx/use-zustand-store'
import { counterStoreFactory } from './counterStore'
interface ICounterProps {
initialCount: number
}
const CounterComponent = ({ initialCount }: ICounterProps) => {
const store = useZustandStore(counterStoreFactory({ initialCount }))
return <button onClick={store.increment}>{store.count}</button>
}
export default CounterComponent
Still need global stores in other scenarios? no problem!
In that case, you can create a global version of the useZustandStore
hook by using the createTrackedSelector
helper from react-tracked
counterStore.ts
import { createZustandStore, createTrackedSelector } from '@sinakhx/use-zustand-store'
interface ICounterStore {
count: number
increment: () => void
}
const counterStore = createZustandStore<ICounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
export const useGlobalCounterStore = createTrackedSelector(counterStore())
CounterComponent.tsx
// import { useZustandStore } from '@sinakhx/use-zustand-store'
import { useGlobalCounterStore } from './counterStore'
const CounterComponent = () => {
const store = useGlobalCounterStore()
return <button onClick={store.increment}>{store.count}</button>
}
export default CounterComponent
now the store is independent from the components & will keep its state regardless of the route changes.
Contributing
Please feel free to open an issue or create a pull request to add a new feature or fix a bug. (see contributing for more details)
License
The MIT License (MIT)
© 2022 Sina Khodabandehloo