Global State: Redux Toolkit & Zustand
Manage app-wide state — cart, user, theme — with the two libraries real React jobs use.
What you will learn
- Explain when you need a global state library
- Set up a slice and store with Redux Toolkit
- Compare it with the lighter Zustand
When context is not enough
You learned useContext for sharing data without prop drilling. For many apps that is plenty. But as an app grows — a shopping cart touched from five screens, a logged-in user read everywhere, lots of frequent updates — teams reach for a dedicated state-management library. These give you one organised place for app-wide state, predictable update rules, and great debugging tools. When a job ad lists “state management”, this is what they mean.
Two libraries dominate: Redux Toolkit (the official, batteries-included version of Redux — the long-time industry standard) and Zustand (a newer, much lighter option). We will set up the same idea — a counter shared across the app — in each, so you can see the difference. Both are real project code, shown with Output.
Redux Toolkit: slice + store
Redux organises state into slices. A slice bundles together one chunk of state and the functions that change it (called reducers). You create a slice, put it in the store (the single object that holds all global state), then read and update it from components.
npm install @reduxjs/toolkit react-redux// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; }, // looks like mutation, but it is safe here
decrement: (state) => { state.value -= 1; },
reset: (state) => { state.value = 0; }
}
});
export const { increment, decrement, reset } = counterSlice.actions;
export default counterSlice.reducer;The slice defines initialState (the counter starts at 0) and three reducers — increment, decrement, reset — each describing how the state changes. Writing state.value += 1 looks like you are mutating state, but Redux Toolkit lets you do that safely and turns it into a proper immutable update behind the scenes. The slice exports actions (the functions components call) and a reducer (which goes into the store).
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice.js';
export const store = configureStore({
reducer: { counter: counterReducer }
});
// main.jsx — wrap the app once with <Provider store={store}>The store combines all your slices’ reducers into one global state object. Just like context and React Query, you wrap the whole app once in <Provider store={store}> so any component can reach the store.
// Counter.jsx
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice.js';
function Counter() {
const value = useSelector((state) => state.counter.value); // read
const dispatch = useDispatch(); // get the dispatcher
return (
<div>
<h2>Count: {value}</h2>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
</div>
);
}Two hooks connect a component to the store:
- Read with
useSelector:useSelector((state) => state.counter.value)picks the exact piece of global state this component needs. When that value changes, only this component re-renders. - Change with
useDispatch:const dispatch = useDispatch()gives you the dispatcher, anddispatch(increment())sends theincrementaction to the store, which runs the matching reducer and updates the count.
Note: Output:
Count: 0 with + and - buttons. Any component anywhere in the app that reads state.counter.value shows the same number, and clicking the buttons updates all of them at once — that is the point of global state.
Zustand: the lightweight alternative
Many modern teams prefer Zustand because it needs far less setup — no slices, no provider, no boilerplate. You create a store as a single hook and use it directly.
npm install zustand// useCounterStore.js
import { create } from 'zustand';
const useCounterStore = create((set) => ({
value: 0,
increment: () => set((s) => ({ value: s.value + 1 })),
reset: () => set({ value: 0 })
}));
// Counter.jsx
function Counter() {
const value = useCounterStore((s) => s.value);
const increment = useCounterStore((s) => s.increment);
return <button onClick={increment}>Count: {value}</button>;
}With Zustand, create((set) => ({ ... })) defines the state (value) and the functions that change it (increment, reset) all in one place; set updates the store. Components read straight from the hook — useCounterStore((s) => s.value) — with no provider to wrap and no separate slice/store files. That brevity is why it is increasingly popular for small-to-medium apps.
| Redux Toolkit | Zustand | |
|---|---|---|
| Setup / boilerplate | More (slices, store, Provider) | Very little |
| Provider needed | Yes (<Provider>) | No |
| DevTools & ecosystem | Excellent, industry standard | Good, lighter |
| Best for | Large/complex apps & teams | Small-to-medium apps |
Tip: Reach for a global store only when state is genuinely shared across many distant parts of the app. For data that comes from a server, prefer React Query — that is server state, not the kind of client state these libraries are best at.
Watch out: Do not put everything in global state. Keep local UI state (a form field, an open/closed menu) in useState. Global state is for data many unrelated components must share.
Q. In Redux Toolkit, how does a component read a value from the store?
✍️ Practice
- Build a Redux Toolkit counter slice and read/update it from two separate components.
- Rebuild the same counter with Zustand and note how much less setup it took.
🏠 Homework
- Add a “theme” (light/dark) to a global store (Redux Toolkit or Zustand) and read it from two components. Write two sentences on when you would choose a global store over useContext.