@sinakhx/use-zustand-store
TypeScript icon, indicating that this package has built-in type declarations

0.3.0 • Public • Published

@sinakhx/useZustandStore

npm license types

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

Package Sidebar

Install

npm i @sinakhx/use-zustand-store

Weekly Downloads

0

Version

0.3.0

License

MIT

Unpacked Size

16.8 kB

Total Files

7

Last publish

Collaborators

  • sinakhx