Gathering detailed insights and metrics for zustand
Gathering detailed insights and metrics for zustand
Gathering detailed insights and metrics for zustand
Gathering detailed insights and metrics for zustand
🐻 Bear necessities for state management in React
npm install zustand
Typescript
Module System
Min. Node Version
Node Version
NPM Version
93.3
Supply Chain
100
Quality
94.7
Maintenance
100
Vulnerability
100
License
Total
304,611,819
Last Day
215,018
Last Week
4,820,118
Last Month
18,982,680
Last Year
180,723,703
48,481 Stars
1,128 Commits
1,509 Forks
165 Watching
1 Branches
303 Contributors
Updated on 08 Dec 2024
Minified
Minified + Gzipped
TypeScript (98.78%)
JavaScript (1.22%)
Cumulative downloads
Total Downloads
Last day
25.6%
215,018
Compared to previous day
Last week
11.8%
4,820,118
Compared to previous week
Last month
1.5%
18,982,680
Compared to previous month
Last year
102.8%
180,723,703
Compared to previous year
4
A small, fast and scalable bearbones state-management solution using simplified flux principles. Has a comfy API based on hooks, isn't boilerplatey or opinionated.
Don't disregard it because it's cute. It has quite the claws, lots of time was spent dealing with common pitfalls, like the dreaded zombie child problem, react concurrency, and context loss between mixed renderers. It may be the one state-manager in the React space that gets all of these right.
You can try a live demo here.
1npm i zustand
:warning: This readme is written for JavaScript users. If you are a TypeScript user, be sure to check out our TypeScript Usage section.
Your store is a hook! You can put anything in it: primitives, objects, functions. State has to be updated immutably and the set
function merges state to help it.
1import { create } from 'zustand' 2 3const useBearStore = create((set) => ({ 4 bears: 0, 5 increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), 6 removeAllBears: () => set({ bears: 0 }), 7}))
Use the hook anywhere, no providers are needed. Select your state and the component will re-render on changes.
1function BearCounter() { 2 const bears = useBearStore((state) => state.bears) 3 return <h1>{bears} around here ...</h1> 4} 5 6function Controls() { 7 const increasePopulation = useBearStore((state) => state.increasePopulation) 8 return <button onClick={increasePopulation}>one up</button> 9}
You can, but bear in mind that it will cause the component to update on every state change!
1const state = useBearStore()
It detects changes with strict-equality (old === new) by default, this is efficient for atomic state picks.
1const nuts = useBearStore((state) => state.nuts) 2const honey = useBearStore((state) => state.honey)
If you want to construct a single object with multiple state-picks inside, similar to redux's mapStateToProps, you can use useShallow to prevent unnecessary rerenders when the selector output does not change according to shallow equal.
1import { create } from 'zustand' 2import { useShallow } from 'zustand/react/shallow' 3 4const useBearStore = create((set) => ({ 5 nuts: 0, 6 honey: 0, 7 treats: {}, 8 // ... 9})) 10 11// Object pick, re-renders the component when either state.nuts or state.honey change 12const { nuts, honey } = useBearStore( 13 useShallow((state) => ({ nuts: state.nuts, honey: state.honey })), 14) 15 16// Array pick, re-renders the component when either state.nuts or state.honey change 17const [nuts, honey] = useBearStore( 18 useShallow((state) => [state.nuts, state.honey]), 19) 20 21// Mapped picks, re-renders the component when state.treats changes in order, count or keys 22const treats = useBearStore(useShallow((state) => Object.keys(state.treats)))
For more control over re-rendering, you may provide any custom equality function (this example requires the use of createWithEqualityFn
).
1const treats = useBearStore( 2 (state) => state.treats, 3 (oldTreats, newTreats) => compare(oldTreats, newTreats), 4)
The set
function has a second argument, false
by default. Instead of merging, it will replace the state model. Be careful not to wipe out parts you rely on, like actions.
1import omit from 'lodash-es/omit' 2 3const useFishStore = create((set) => ({ 4 salmon: 1, 5 tuna: 2, 6 deleteEverything: () => set({}, true), // clears the entire store, actions included 7 deleteTuna: () => set((state) => omit(state, ['tuna']), true), 8}))
Just call set
when you're ready, zustand doesn't care if your actions are async or not.
1const useFishStore = create((set) => ({ 2 fishies: {}, 3 fetch: async (pond) => { 4 const response = await fetch(pond) 5 set({ fishies: await response.json() }) 6 }, 7}))
set
allows fn-updates set(state => result)
, but you still have access to state outside of it through get
.
1const useSoundStore = create((set, get) => ({ 2 sound: 'grunt', 3 action: () => { 4 const sound = get().sound 5 ...
Sometimes you need to access state in a non-reactive way or act upon the store. For these cases, the resulting hook has utility functions attached to its prototype.
:warning: This technique is not recommended for adding state in React Server Components (typically in Next.js 13 and above). It can lead to unexpected bugs and privacy issues for your users. For more details, see #2200.
1const useDogStore = create(() => ({ paw: true, snout: true, fur: true })) 2 3// Getting non-reactive fresh state 4const paw = useDogStore.getState().paw 5// Listening to all changes, fires synchronously on every change 6const unsub1 = useDogStore.subscribe(console.log) 7// Updating state, will trigger listeners 8useDogStore.setState({ paw: false }) 9// Unsubscribe listeners 10unsub1() 11 12// You can of course use the hook as you always would 13function Component() { 14 const paw = useDogStore((state) => state.paw) 15 ...
If you need to subscribe with a selector,
subscribeWithSelector
middleware will help.
With this middleware subscribe
accepts an additional signature:
1subscribe(selector, callback, options?: { equalityFn, fireImmediately }): Unsubscribe
1import { subscribeWithSelector } from 'zustand/middleware' 2const useDogStore = create( 3 subscribeWithSelector(() => ({ paw: true, snout: true, fur: true })), 4) 5 6// Listening to selected changes, in this case when "paw" changes 7const unsub2 = useDogStore.subscribe((state) => state.paw, console.log) 8// Subscribe also exposes the previous value 9const unsub3 = useDogStore.subscribe( 10 (state) => state.paw, 11 (paw, previousPaw) => console.log(paw, previousPaw), 12) 13// Subscribe also supports an optional equality function 14const unsub4 = useDogStore.subscribe( 15 (state) => [state.paw, state.fur], 16 console.log, 17 { equalityFn: shallow }, 18) 19// Subscribe and fire immediately 20const unsub5 = useDogStore.subscribe((state) => state.paw, console.log, { 21 fireImmediately: true, 22})
Zustand core can be imported and used without the React dependency. The only difference is that the create function does not return a hook, but the API utilities.
1import { createStore } from 'zustand/vanilla' 2 3const store = createStore((set) => ...) 4const { getState, setState, subscribe, getInitialState } = store 5 6export default store
You can use a vanilla store with useStore
hook available since v4.
1import { useStore } from 'zustand' 2import { vanillaStore } from './vanillaStore' 3 4const useBoundStore = (selector) => useStore(vanillaStore, selector)
:warning: Note that middlewares that modify set
or get
are not applied to getState
and setState
.
The subscribe function allows components to bind to a state-portion without forcing re-render on changes. Best combine it with useEffect for automatic unsubscribe on unmount. This can make a drastic performance impact when you are allowed to mutate the view directly.
1const useScratchStore = create((set) => ({ scratches: 0, ... })) 2 3const Component = () => { 4 // Fetch initial state 5 const scratchRef = useRef(useScratchStore.getState().scratches) 6 // Connect to the store on mount, disconnect on unmount, catch state-changes in a reference 7 useEffect(() => useScratchStore.subscribe( 8 state => (scratchRef.current = state.scratches) 9 ), []) 10 ...
Reducing nested structures is tiresome. Have you tried immer?
1import { produce } from 'immer' 2 3const useLushStore = create((set) => ({ 4 lush: { forest: { contains: { a: 'bear' } } }, 5 clearForest: () => 6 set( 7 produce((state) => { 8 state.lush.forest.contains = null 9 }), 10 ), 11})) 12 13const clearForest = useLushStore((state) => state.clearForest) 14clearForest()
Alternatively, there are some other solutions.
You can persist your store's data using any kind of storage.
1import { create } from 'zustand' 2import { persist, createJSONStorage } from 'zustand/middleware' 3 4const useFishStore = create( 5 persist( 6 (set, get) => ({ 7 fishes: 0, 8 addAFish: () => set({ fishes: get().fishes + 1 }), 9 }), 10 { 11 name: 'food-storage', // name of the item in the storage (must be unique) 12 storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used 13 }, 14 ), 15)
See the full documentation for this middleware.
Immer is available as middleware too.
1import { create } from 'zustand' 2import { immer } from 'zustand/middleware/immer' 3 4const useBeeStore = create( 5 immer((set) => ({ 6 bees: 0, 7 addBees: (by) => 8 set((state) => { 9 state.bees += by 10 }), 11 })), 12)
1const types = { increase: 'INCREASE', decrease: 'DECREASE' } 2 3const reducer = (state, { type, by = 1 }) => { 4 switch (type) { 5 case types.increase: 6 return { grumpiness: state.grumpiness + by } 7 case types.decrease: 8 return { grumpiness: state.grumpiness - by } 9 } 10} 11 12const useGrumpyStore = create((set) => ({ 13 grumpiness: 0, 14 dispatch: (args) => set((state) => reducer(state, args)), 15})) 16 17const dispatch = useGrumpyStore((state) => state.dispatch) 18dispatch({ type: types.increase, by: 2 })
Or, just use our redux-middleware. It wires up your main-reducer, sets the initial state, and adds a dispatch function to the state itself and the vanilla API.
1import { redux } from 'zustand/middleware' 2 3const useGrumpyStore = create(redux(reducer, initialState))
Install the Redux DevTools Chrome extension to use the devtools middleware.
1import { devtools } from 'zustand/middleware' 2 3// Usage with a plain action store, it will log actions as "setState" 4const usePlainStore = create(devtools((set) => ...)) 5// Usage with a redux store, it will log full action types 6const useReduxStore = create(devtools(redux(reducer, initialState)))
One redux devtools connection for multiple stores
1import { devtools } from 'zustand/middleware' 2 3// Usage with a plain action store, it will log actions as "setState" 4const usePlainStore1 = create(devtools((set) => ..., { name, store: storeName1 })) 5const usePlainStore2 = create(devtools((set) => ..., { name, store: storeName2 })) 6// Usage with a redux store, it will log full action types 7const useReduxStore = create(devtools(redux(reducer, initialState)), , { name, store: storeName3 }) 8const useReduxStore = create(devtools(redux(reducer, initialState)), , { name, store: storeName4 })
Assigning different connection names will separate stores in redux devtools. This also helps group different stores into separate redux devtools connections.
devtools takes the store function as its first argument, optionally you can name the store or configure serialize options with a second argument.
Name store: devtools(..., {name: "MyStore"})
, which will create a separate instance named "MyStore" in the devtools.
Serialize options: devtools(..., { serialize: { options: true } })
.
devtools will only log actions from each separated store unlike in a typical combined reducers redux store. See an approach to combining stores https://github.com/pmndrs/zustand/issues/163
You can log a specific action type for each set
function by passing a third parameter:
1const useBearStore = create(devtools((set) => ({ 2 ... 3 eatFish: () => set( 4 (prev) => ({ fishes: prev.fishes > 1 ? prev.fishes - 1 : 0 }), 5 undefined, 6 'bear/eatFish' 7 ), 8 ...
You can also log the action's type along with its payload:
1 ... 2 addFishes: (count) => set( 3 (prev) => ({ fishes: prev.fishes + count }), 4 undefined, 5 { type: 'bear/addFishes', count, } 6 ), 7 ...
If an action type is not provided, it is defaulted to "anonymous". You can customize this default value by providing an anonymousActionType
parameter:
1devtools(..., { anonymousActionType: 'unknown', ... })
If you wish to disable devtools (on production for instance). You can customize this setting by providing the enabled
parameter:
1devtools(..., { enabled: false, ... })
The store created with create
doesn't require context providers. In some cases, you may want to use contexts for dependency injection or if you want to initialize your store with props from a component. Because the normal store is a hook, passing it as a normal context value may violate the rules of hooks.
The recommended method available since v4 is to use the vanilla store.
1import { createContext, useContext } from 'react' 2import { createStore, useStore } from 'zustand' 3 4const store = createStore(...) // vanilla store without hooks 5 6const StoreContext = createContext() 7 8const App = () => ( 9 <StoreContext.Provider value={store}> 10 ... 11 </StoreContext.Provider> 12) 13 14const Component = () => { 15 const store = useContext(StoreContext) 16 const slice = useStore(store, selector) 17 ...
Basic typescript usage doesn't require anything special except for writing create<State>()(...)
instead of create(...)
...
1import { create } from 'zustand' 2import { devtools, persist } from 'zustand/middleware' 3import type {} from '@redux-devtools/extension' // required for devtools typing 4 5interface BearState { 6 bears: number 7 increase: (by: number) => void 8} 9 10const useBearStore = create<BearState>()( 11 devtools( 12 persist( 13 (set) => ({ 14 bears: 0, 15 increase: (by) => set((state) => ({ bears: state.bears + by })), 16 }), 17 { 18 name: 'bear-storage', 19 }, 20 ), 21 ), 22)
A more complete TypeScript guide is here.
Some users may want to extend Zustand's feature set which can be done using third-party libraries made by the community. For information regarding third-party libraries with Zustand, visit the doc.
No vulnerabilities found.
Reason
30 commit(s) and 6 issue activity found in the last 90 days -- score normalized to 10
Reason
no dangerous workflow patterns detected
Reason
no binaries found in the repo
Reason
license file detected
Details
Reason
packaging workflow detected
Details
Reason
Found 21/30 approved changesets -- score normalized to 7
Reason
4 existing vulnerabilities detected
Details
Reason
detected GitHub workflow tokens with excessive permissions
Details
Reason
dependency not pinned by hash detected -- score normalized to 0
Details
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
project is not fuzzed
Details
Reason
security policy file not detected
Details
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
Score
Last Scanned on 2024-11-25
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn More