🐻 Bear necessities for state management in React
Installations
npm install zustand
Developer Guide
Typescript
Yes
Module System
CommonJS, ESM
Min. Node Version
>=12.20.0
Node Version
18.20.5
NPM Version
10.8.2
Score
93.3
Supply Chain
100
Quality
92.7
Maintenance
100
Vulnerability
100
License
Releases
Contributors
Languages
TypeScript (98.78%)
JavaScript (1.22%)
Developer
Download Statistics
Total Downloads
315,377,154
Last Day
420,243
Last Week
3,893,564
Last Month
19,682,383
Last Year
186,198,683
GitHub Statistics
48,860 Stars
1,139 Commits
1,529 Forks
164 Watching
1 Branches
305 Contributors
Bundle Size
1.21 kB
Minified
588.00 B
Minified + Gzipped
Package Meta Information
Latest Version
5.0.2
Package Id
zustand@5.0.2
Unpacked Size
86.71 kB
Size
20.71 kB
File Count
50
NPM Version
10.8.2
Node Version
18.20.5
Publised On
04 Dec 2024
Total Downloads
Cumulative downloads
Total Downloads
315,377,154
Last day
-53.3%
420,243
Compared to previous day
Last week
-18.9%
3,893,564
Compared to previous week
Last month
9.6%
19,682,383
Compared to previous month
Last year
101.7%
186,198,683
Compared to previous year
Daily Downloads
Weekly Downloads
Monthly Downloads
Yearly Downloads
Peer Dependencies
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.
First create a store
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}))
Then bind your components, and that's it!
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}
Why zustand over redux?
- Simple and un-opinionated
- Makes hooks the primary means of consuming state
- Doesn't wrap your app in context providers
- Can inform components transiently (without causing render)
Why zustand over context?
- Less boilerplate
- Renders components only on changes
- Centralized, action-based state management
Recipes
Fetching everything
You can, but bear in mind that it will cause the component to update on every state change!
1const state = useBearStore()
Selecting multiple state slices
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)
Overwriting state
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}))
Async actions
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}))
Read from state in actions
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 ...
Reading/writing state and reacting to changes outside of components
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 ...
Using subscribe with selector
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})
Using zustand without React
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
.
Transient updates (for often occurring state-changes)
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 ...
Sick of reducers and changing nested states? Use Immer!
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.
Persist middleware
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 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)
Can't live without redux-like reducers and action types?
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))
Redux devtools
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 } })
.
Logging Actions
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, ... })
React context
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 ...
TypeScript Usage
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.
Best practices
- You may wonder how to organize your code for better maintenance: Splitting the store into separate slices.
- Recommended usage for this unopinionated library: Flux inspired practice.
- Calling actions outside a React event handler in pre-React 18.
- Testing
- For more, have a look in the docs folder
Third-Party Libraries
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.
Comparison with other libraries
No vulnerabilities found.
Reason
30 commit(s) and 7 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
- Info: project has a license file: LICENSE:0
- Info: FSF or OSI recognized license: MIT License: LICENSE:0
Reason
packaging workflow detected
Details
- Info: Project packages its releases by way of GitHub Actions.: .github/workflows/publish.yml:8
Reason
Found 26/30 approved changesets -- score normalized to 8
Reason
5 existing vulnerabilities detected
Details
- Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275
- Warn: Project is vulnerable to: GHSA-mwcw-c2x4-8c55
- Warn: Project is vulnerable to: GHSA-gcx4-mw62-g8wm
- Warn: Project is vulnerable to: GHSA-64vr-g452-qvp3
- Warn: Project is vulnerable to: GHSA-9cwx-2883-4wfx
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
detected GitHub workflow tokens with excessive permissions
Details
- Warn: no topLevel permission defined: .github/workflows/compressed-size-action.yml:1
- Warn: no topLevel permission defined: .github/workflows/cr.yml:1
- Warn: no topLevel permission defined: .github/workflows/docs.yml:1
- Warn: no topLevel permission defined: .github/workflows/lint-and-type.yml:1
- Warn: no topLevel permission defined: .github/workflows/publish.yml:1
- Warn: no topLevel permission defined: .github/workflows/test-multiple-builds.yml:1
- Warn: no topLevel permission defined: .github/workflows/test-multiple-versions.yml:1
- Warn: no topLevel permission defined: .github/workflows/test-old-typescript.yml:1
- Info: no jobLevel write permissions found
Reason
dependency not pinned by hash detected -- score normalized to 0
Details
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/compressed-size-action.yml:9: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/compressed-size-action.yml/main?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/compressed-size-action.yml:10: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/compressed-size-action.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/compressed-size-action.yml:11: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/compressed-size-action.yml/main?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/compressed-size-action.yml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/compressed-size-action.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/cr.yml:9: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/cr.yml/main?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/cr.yml:10: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/cr.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/cr.yml:11: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/cr.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/docs.yml:40: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/docs.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/lint-and-type.yml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/lint-and-type.yml/main?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/lint-and-type.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/lint-and-type.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/lint-and-type.yml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/lint-and-type.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:11: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/publish.yml/main?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/publish.yml:12: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/publish.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/publish.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test-multiple-builds.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/test-multiple-builds.yml/main?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/test-multiple-builds.yml:19: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/test-multiple-builds.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test-multiple-builds.yml:20: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/test-multiple-builds.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test-multiple-versions.yml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/test-multiple-versions.yml/main?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/test-multiple-versions.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/test-multiple-versions.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test-multiple-versions.yml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/test-multiple-versions.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test-multiple-versions.yml:37: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/test-multiple-versions.yml/main?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/test-multiple-versions.yml:38: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/test-multiple-versions.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test-multiple-versions.yml:39: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/test-multiple-versions.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test-old-typescript.yml:28: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/test-old-typescript.yml/main?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/test-old-typescript.yml:29: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/test-old-typescript.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test-old-typescript.yml:30: update your workflow using https://app.stepsecurity.io/secureworkflow/pmndrs/zustand/test-old-typescript.yml/main?enable=pin
- Info: 0 out of 17 GitHub-owned GitHubAction dependencies pinned
- Info: 0 out of 9 third-party GitHubAction dependencies pinned
Reason
project is not fuzzed
Details
- Warn: no fuzzer integrations found
Reason
security policy file not detected
Details
- Warn: no security policy file detected
- Warn: no security file to analyze
- Warn: no security file to analyze
- Warn: no security file to analyze
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
- Warn: 0 commits out of 28 are checked with a SAST tool
Score
5.5
/10
Last Scanned on 2024-12-16
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