Info Zustand state manager is the simplest state manager ever. Installation npm install zustand App Store
import create from 'zustand'
const useStore = create(set => ({
counter: 0,
add: (num) => set((state) => ({ counter: state.counter + (num || 1) })),
reset: () => set({ counter: 0 }),
isLogged: false,
logInOut: () => set((state) => ({ isLogged: !state.isLogged })),
deleteStates: () => set({}, true)
}))
Set set({ counter: 0 }) pushes counter state into the store object, merges by default set({ counter: 0 }, true) replaces whole store with counter state set((state) => ({ counter: state.counter + 1 })) pushes updated counter state into the store object Reactive binding Select your state and the component will re-render on changes. Whole store Get the whole store.
const state = useStore();
It will cause the component to update on every store's state change Specific state
const counter = useStore(state => state.counter)
Custom equality for re-render
const counterWithCustomEqualityFn = useStore(
state => state.counter,
(prevState, newState) => {
if (newState > 5) return false // render
if (newState <= 5) return true // no render
}
)
Async actions
const useStore = create(set => ({
fishes: {},
fetch: async pond => {
const response = await fetch(pond)
set({ fishes: await response.json() })
}
}))
Read from state in actions
const useStore = create((set, get) => ({
sound: "grunt",
action: () => {
const sound = get().sound
// ...
}
})
Non-reactive reading
const useStore = create(() => ({ paw: true, snout: true, fur: true }))
// Getting non-reactive fresh state
const paw = useStore.getState().paw
Set state outside store
useStore.setState({ paw: false })
Subscribe for all changes
// Listening to all changes, fires synchronously on every change
const unsubscribe = useStore.subscribe(console.log)
// Unsubscribe listeners
unsubscribe()
// Destroying the store (removing all listeners)
useStore.destroy()
Subscribe for state changes Need to add middleware
import create from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'
const useStore = create(subscribeWithSelector((set, get) => ({
isLogged: false,
logInOut: () => set((state) => ({ isLogged: !state.isLogged })),
})))
const unsubscribe = useStore.subscribe(state => state.isLogged, (prev, now) => { console.log('triggered log in/out', 'prev state', prev, 'state now', now)})
function Component() {
const isLogged = useStore(state => state.isLogged)
const logInOut = useStore(state => state.logInOut)
return (
<div style={style}>
<div>isLogged: <strong>{isLogged?.toString()}</strong></div>
<button onClick={() => logInOut()}>Log in/out</button>
</div>
)
}
Immer
import create from 'zustand'
import produce from 'immer'
import { immer } from 'zustand/middleware/immer'
const useStore = create(immer((set, get) => ({
counter: 0,
add: (num) => set((state) => ({ counter: state.counter + (num || 1) })),
addWithImmer: (num) => set((state) => produce(state, (state) => {
state.counter = state.counter + (num || 1)
})),
addWithImmerCurried: (num) => set(produce((state) => {
state.counter = state.counter + (num || 1)
})),
addWithImmerMW: (num) => set((state) => { state.counter = state.counter + (num || 1) }),
})))
Devtools Add redux devtools as a middleware
import { subscribeWithSelector, devtools} from 'zustand/middleware'
const useStore = create(immer(subscribeWithSelector(devtools((set, get) => ({
counter: 0,
add: (num) => set((state) => ({ counter: state.counter + (num || 1) }), false, 'ACTION NAME'),
// false means 'do not overwrite' the state, but merge it
})))))
Devtools can not dispatch an action, but can register it if we provide actions name as 3rd argument in set function. Whole code
import create from 'zustand'
import { subscribeWithSelector, devtools } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'
import axios from 'axios'
import sleeper from '/functions/sleeper'
import produce from 'immer'
// #region STORE
const useStore = create(immer(subscribeWithSelector(devtools((set, get) => ({
counter: 0,
add: (num) => set((state) => ({ counter: state.counter + (num || 1) })),
addWithImmer: (num) => set((state) => produce(state, (state) => {
state.counter = state.counter + (num || 1)
})),
addWithImmerCurried: (num) => set(produce((state) => {
state.counter = state.counter + (num || 1)
})),
addWithImmerMW: (num) => set((state) => { state.counter = state.counter + (num || 1) }),
reset: () => set({ counter: 0 }),
isLogged: false,
logInOut: () => set((state) => ({ isLogged: !state.isLogged }), false, 'log in or out'),
deleteStates: () => set({}, true),
users: [],
usersLoading: false,
fetchUsers: async () => {
set({ usersLoading: true })
const res = await axios.get('https://jsonplaceholder.typicode.com/users')
await sleeper(3000)()
const users = res.data
set({ users, usersLoading: false })
},
alertCounter: () => alert(get().counter.toString())
}), { name: 'Zustand store' }))))
function App() {
const fishes = useStore((state) => state.fishes);
const eatFish = useStore((state) => state.eatFish);
return (
<div className="App">
<p>Fishes : {fishes}</p>
<p>
<button onClick={eatFish}>Eat</button>
</p>
</div>
)
}
const unsubscribe = useStore.subscribe(state => state.isLogged, (prev, now) => { console.log('triggered log in/out', 'prev state', prev, 'state now', now) })
// #endregion
// #region Component
function Component() {
const counter = useStore(state => state.counter)
const counterWithCustomEqualityFn = useStore(
state => state.counter,
(prevState, newState) => {
if (newState > 5) return false // render
if (newState <= 5) return true // no render
}
)
const add = useStore(state => state.add)
const addWithImmer = useStore(state => state.addWithImmer)
const addWithImmerCurried = useStore(state => state.addWithImmerCurried)
const addWithImmerMW = useStore(state => state.addWithImmerMW)
const reset = useStore(state => state.reset)
const isLogged = useStore(state => state.isLogged)
const logInOut = useStore(state => state.logInOut)
const deleteStates = useStore(state => state.deleteStates)
const users = useStore(state => state.users)
const usersLoading = useStore(state => state.usersLoading)
const fetchUsers = useStore(state => state.fetchUsers)
const alertCounter = useStore(state => state.alertCounter)
const style = { border: '2px solid grey', padding: '10px', margin: '10px', maxWidth: '500px' }
return (
<div style={style}>
<div>Counter: <strong>{counter}</strong></div>
<div>Counter with custom equality func: <strong>{counterWithCustomEqualityFn}</strong></div>
<button onClick={() => add()}>Increment +1</button>
<button onClick={() => addWithImmer()}>Increment with Immer +1</button>
<button onClick={() => addWithImmerCurried()}>Increment with Immer Curried +1</button>
<button onClick={() => addWithImmerMW()}>Increment with Immer Middleware +1</button>
<button onClick={() => add(3)}>Increment +3</button>
<button onClick={() => add(-5)}>Decrement -5</button>
<button onClick={() => { useStore.setState({ counter: 666 }) }}>Set counter state outside component</button>
<button onClick={() => alertCounter()}>Alert counter</button><br />
<button onClick={() => reset()}>Reset</button><br />
<div>isLogged: <strong>{isLogged?.toString()}</strong></div>
<button onClick={() => logInOut()}>Log in/out</button><br />
<div>Fetch users</div>
<button onClick={() => fetchUsers()}>Fetch users</button><br />
<div>
{usersLoading && 'Loading...'}
{!usersLoading && !!users?.length && users.map(user => <div key={user.id}>{user.name}</div>)}
</div>
<div>Replace the store's object content</div>
<button onClick={() => deleteStates()}>Delete states</button><br />
</div>
)
}
// #endregion
Split store into slices
// posts/zustandStore/counterSlice.js
export const counterSlice = (set, get) => ({
counter: 0,
add: (num) => set(
(state) => ({ counter: state.counter + (num || 1) }),
false,
'add'
)
})
// posts/zustandStore/loginOutSlice.js
export const loginOutSlice = (set, get) => ({
isLogged: false,
logInOut: () => set(
(state) => ({ isLogged: !state.isLogged }),
false,
'log in / out'
)
})
// posts/zustandStore/zustandStore.js
import create from 'zustand'
import { devtools } from 'zustand/middleware'
import { counterSlice } from './counterSlice'
import { loginOutSlice } from './loginOutSlice'
export const zustandStore = create(devtools((set, get) => ({
...counterSlice(set, get),
...loginOutSlice(set, get)
}), { name: 'Store with slices' }))
import { zustandStore } from './zustandStore/zustandStore'
function ComponentWithStoreFromSlices() {
const counter = zustandStore(state => state.counter)
const add = zustandStore(state => state.add)
const isLogged = zustandStore(state => state.isLogged)
const logInOut = zustandStore(state => state.logInOut)
const style = { border: '2px solid grey', padding: '10px', margin: '10px', maxWidth: '500px' }
return (
<div style={style}>
<div>Counter: <strong>{counter}</strong></div>
<button onClick={() => add()}>Increment +1</button>
<div>isLogged: <strong>{isLogged?.toString()}</strong></div>
<button onClick={() => logInOut()}>Log in/out</button><br />
</div>
)
}