The new Zustand library changes everything for state management in web dev.
The simplicity completely blows Redux away. It’s like Assembly vs Python.
Forget action types, dispatch
, Providers and all that verbose garbage.
Just use a hook! 👇
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
Effortless and intuitive with all the benefits of Redux and Flux — immutability, data-UI decoupling…
With none of the boilerplate — it’s just an object.
Redux had to be patched with hook support but Zustand was built from the ground up with hooks in mind.
function App() {
const store = useStore();
return (
<div>
<div>Count: {store.count}</div>
<button onClick={store.increment}>Increment</button>
</div>
);
}
export default App;
Share the store across multiple components and select only what you want:
function Counter() {
// ✅ Only `count`
const count = useStore((state) => state.count);
return <div>Count: {count}</div>;
}
function Controls() {
// ✅ Only `increment`
const increment = useStore((state) => state.increment);
return <button onClick={increment}>Increment</button>;
}
Create multiple stores to decentralize data and scale intuitively.
Let’s be real, that single-state stuff doesn’t always make sense. And it defies encapsulation.
It’s often more natural to let a branch of components have their localized global state.
// ✅ More global store to handle the count data
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
// ✅ More local store to handle user input logic
const useControlStore = create((set) => ({
input: '',
setInput: () => set((state) => ({ input: state.input })),
}));
function Controls() {
return (
<div>
<CountInput />
<Button />
</div>
);
}
function Button() {
const increment = useStore((state) => state.increment);
const input = useControlStore((state) => state.input);
return (
<button onClick={() => increment(Number(input))}>
Increment by {input}
</button>
);
}
function CountInput() {
const input = useControlStore((state) => state.input);
return <input value={input} />;
}
Meet useShallow()
, a powerful way to get derived states — instantly updates when any of original states change.
import { create } from 'zustand';
import { useShallow } from 'zustand/react/shallow';
const useLibraryStore = create((set) => ({
fiction: 0,
nonFiction: 0,
borrowedBooks: {},
// ...
}));
// ✅ Object pick
const { fiction, nonFiction } = useLibraryStore(
useShallow((state) => ({
fiction: state.fiction,
nonFiction: state.nonFiction,
}))
);
// ✅ Array pick
const [fiction, nonFiction] = useLibraryStore(
useShallow((state) => [state.fiction, state.nonFiction])
);
// ✅ Mapped picks
const borrowedBooks = useLibraryStore(
useShallow((state) => Object.keys(state.borrowedBooks))
);
And what if you don’t want instant updates — only at certain times?
It’s easier than ever — just pass a second argument to your store hook.
const user = useUserStore(
(state) => state.user,
(oldUser, newUser) => compare(oldUser.id, newUser.id)
);
And how about derived updates based on previous states, like in React’s useState
?
Don’t worry! In Zustand states update partially by default:
const useStore = create((set) => ({
user: {
username: 'tariibaba',
site: 'codingbeautydev.com',
color: 'blue💙',
},
premium: false,
// `user` object is not affected
// `state` is the curr state before the update
unsubscribe: () => set((state) => ({ premium: false })),
}));
It only works at the first level though — you have to handle deeper partial updates by yourself:
const useStore = create((set) => ({
user: {
username: 'tariibaba',
site: 'codingbeautydev.com',
color: 'blue💙',
},
premium: false,
updateUsername: (username) =>
// 👇 deep updates necessary to retain other object properties
set((state) => ({ user: { ...state.user, username } })),
}));
If you don’t want it just pass the object directly with true
as the two arguments.
const useStore = create((set) => ({
user: {
username: 'tariibaba',
site: 'codingbeautydev.com',
color: 'blue💙',
},
premium: false,
// Clear data with `true`
resetAccount: () => set({}, true),
}));
Zustand even has built-in support for async actions — no need for Redux Thunk or any external library.
const useStore = create((set) => ({
user: {
username: 'tariibaba',
site: 'codingbeautydev.com',
color: 'blue💙',
},
premium: false,
// ✅ async actions
updateFavColor: async (color) => {
await fetch('https://api.tariibaba.com', {
method: 'PUT',
body: color,
});
set((state) => ({ user: { ...state.user, color } }));
},
}));
It’s also easy to get state within actions, thanks to get
— the 2nd param in create()
‘s callback:
// ✅ `get` lets us use state directly in actions
const useStore = create((set, get) => ({
user: {
username: 'tariibaba',
site: 'codingbeautydev.com',
color: 'blue💙',
},
messages: [],
sendMessage: ({ message, to }) => {
const newMessage = {
message,
to,
// ✅ `get` gives us `user` object
from: get().user.username,
};
set((state) => ({
messages: [...state.messages, newMessage],
}));
},
}));
It’s all about hooks in Zustand, but if you want you can read and subscribe to values in state directly.
// Get a non-observed state with getState()
const count = useStore.getState().count;
useStore.subscribe((state) => {
console.log(`new value: ${state.count}`);
});
This makes it great for cases where the property changes a lot but you only need the latest value for intermediate logic, not direct UI:
export default function App() {
const widthRef = useRef(useStore.getState().windowWidth);
useEffect(() => {
useStore.subscribe((state) => {
widthRef.current = state.windowWidth;
});
}, []);
useEffect(() => {
setInterval(() => {
console.log(`Width is now: ${widthRef.current}`);
}, 1000);
}, []);
// ...
}
Zustand outshines Redux and Mobx and all the others in almost every way. Use it for your next project and you won’t regret it.
Every Crazy Thing JavaScript Does
A captivating guide to the subtle caveats and lesser-known parts of JavaScript.