L1: Setup Zustand Store
Install Zustand and create your first store
Let's set up Zustand for global state management! 🐻
What is Zustand?
Zustand (German for "state") is a small, fast, and scalable state management solution for React. It's:
- ✅ Tiny - Only 1KB! 8x smaller than Redux Toolkit
- ✅ Simple - No boilerplate, no Provider, just hooks
- ✅ Fast - Optimized rendering by default
- ✅ Flexible - Use it however you want
Why Zustand?
Problem with useState:
// HomePage.jsx
const [listings, setListings] = useState([]);
// PropertyCard.jsx
// ❌ Can't access listings here without prop drilling!
// FavoritesPage.jsx
// ❌ Can't access listings here either!With Zustand:
// store.js
const useListingsStore = create((set) => ({
listings: [],
setListings: (listings) => set({ listings })
}));
// HomePage.jsx, PropertyCard.jsx, FavoritesPage.jsx
const listings = useListingsStore((state) => state.listings);
// ✅ Works everywhere! No prop drilling!Redux Toolkit requires:
- Store configuration file
- Slice files with reducers
- Provider wrapper component
- useSelector and useDispatch hooks
- Understanding actions, reducers, dispatch
Zustand requires:
- One store file
- Just use the hook
Example comparison:
// Redux Toolkit (3 files, ~30 lines)
// store.js
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({ reducer: { counter: counterReducer }});
// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: { increment: (state) => { state.value += 1; }}
});
// App.jsx
<Provider store={store}><App /></Provider>
// Zustand (1 file, ~5 lines)
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}));
// App.jsx
// Nothing needed! Just use useStore()90% less code! 🎉
Context API requires:
- Creating context
- Provider wrapper
- Context consumer hook
- Verbose setup
Zustand:
- Just create a hook
- Use it anywhere
Context:
const ThemeContext = createContext();
<ThemeContext.Provider value={theme}>
<App />
</ThemeContext.Provider>
const theme = useContext(ThemeContext);Zustand:
const useTheme = create((set) => ({ theme: 'light' }));
const theme = useTheme((state) => state.theme);Much simpler!
Step 1: Install Zustand
Install Zustand package:
npm install zustandThat's it! No other dependencies needed. ✨
Bundle size: Zustand adds only ~1KB to your bundle! Redux Toolkit adds ~8KB.
Step 2: Understand Zustand Concepts
Before creating a store, let's understand the key concepts:
Step 3: Create Your First Store
Let's create a simple counter store to learn the basics:
touch src/state/useCounterStore.jsAdd this code:
import { create } from 'zustand';
const useCounterStore = create((set) => ({
// Initial state
count: 0,
// Actions
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}));
export default useCounterStore;That's a complete store! No reducers, no actions, no dispatch - just state and functions. ✨
Step 4: Use the Store
Use it in any component:
import useCounterStore from '@/state/useCounterStore';
function Counter() {
// Select state
const count = useCounterStore((state) => state.count);
// Select actions
const increment = useCounterStore((state) => state.increment);
const decrement = useCounterStore((state) => state.decrement);
const reset = useCounterStore((state) => state.reset);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>Reset</button>
</div>
);
}
export default Counter;No Provider, no dispatch, just hooks! 🎉
Understanding Selectors
The function you pass to useCounterStore() is called a selector:
// Select single value
const count = useCounterStore((state) => state.count);
// Select multiple values
const { count, increment } = useCounterStore((state) => ({
count: state.count,
increment: state.increment
}));
// Select everything (not recommended - causes unnecessary re-renders)
const store = useCounterStore();Zustand Architecture
Here's how Zustand works:
┌─────────────────────────────────────┐
│ Zustand Store │
│ │
│ State: { count: 0, name: 'John' } │
│ │
│ Actions: │
│ - increment() │
│ - decrement() │
│ - setName(name) │
│ │
└─────────────────────────────────────┘
↑ ↓
useStore() set()
↑ ↓
┌─────────────────────────────────────┐
│ React Components │
│ │
│ Component A → reads count │
│ Component B → calls increment │
│ Component C → reads name │
│ │
└─────────────────────────────────────┘Flow:
- Components call
useStore(selector)to read state - Components call actions to update state
- Actions call
set()to update store - Store notifies subscribed components
- Only components using changed values re-render
Comparison with Redux
Redux Toolkit:
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer
}
});
// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; }
}
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
// App.jsx
import { Provider } from 'react-redux';
<Provider store={store}><App /></Provider>Zustand:
// useCounterStore.js
import { create } from 'zustand';
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 }))
}));
export default useCounterStore;
// App.jsx
// Nothing needed!
<App />70% less code! ✨
Redux Toolkit:
import { useSelector } from 'react-redux';
const count = useSelector((state) => state.counter.value);Zustand:
import useCounterStore from '@/state/useCounterStore';
const count = useCounterStore((state) => state.count);Very similar! Both use selectors.
Redux Toolkit:
import { useDispatch } from 'react-redux';
import { increment } from '@/state/counterSlice';
const dispatch = useDispatch();
dispatch(increment());Zustand:
import useCounterStore from '@/state/useCounterStore';
const increment = useCounterStore((state) => state.increment);
increment();No dispatch needed! Actions are just functions.
Redux Toolkit:
import { createAsyncThunk } from '@reduxjs/toolkit';
export const fetchUser = createAsyncThunk(
'user/fetch',
async (userId) => {
const response = await api.getUser(userId);
return response.data;
}
);
// Then handle in extraReducers...Zustand:
const useUserStore = create((set) => ({
user: null,
isLoading: false,
fetchUser: async (userId) => {
set({ isLoading: true });
const response = await api.getUser(userId);
set({ user: response.data, isLoading: false });
}
}));Just write async functions! No special thunks needed.
What's Next?
Perfect! You've set up Zustand. In the next lesson, we'll:
- Create Listings Store - Build the real store for our app
- Define state shape - items, favorites, status, error
- Add actions - toggleFavorite, setItems, etc.
✅ Lesson Complete! You've learned Zustand basics and created your first store!
Key Takeaways
- ✅ Zustand is tiny - Only 1KB vs 8KB for Redux Toolkit
- ✅ No Provider needed - Store exists outside React tree
- ✅ Just hooks - Use
useStore(selector)anywhere - ✅ Simple API -
create()andset()are all you need - ✅ Automatic optimization - Selectors control re-renders
- ✅ 90% less code than Redux Toolkit
- ✅ Perfect for small-medium apps and rapid development