Mastering Zustand State Management in React Applications

- Published on
- /6 mins read/---
Introduction
State management is a crucial aspect of modern React applications. While there are many state management solutions available, Zustand has gained popularity due to its simplicity, flexibility, and performance. In this article, we'll explore different ways to use Zustand effectively in your React applications.
Understanding Zustand
Zustand is a small, fast, and scalable state management solution. It provides a simple yet powerful API that can be used both inside and outside of React components. Let's explore the different approaches to using Zustand.
Usage Patterns
1. Hook-based Usage (Inside React Components)
The most common way to use Zustand is through its hook API within React components:
import create from 'zustand';
// Define your store
interface StoreState {
count: number;
increment: () => void;
decrement: () => void;
}
const useStore = create<StoreState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 }))
}));
// Using the entire store
const CounterComponent: React.FC = () => {
const state = useStore();
return (
<div>
<p>Count: {state.count}</p>
<button onClick={state.increment}>+</button>
<button onClick={state.decrement}>-</button>
</div>
);
};
2. Using Selectors for Performance
For better performance, use selectors to subscribe to specific parts of the store:
interface TodoStore {
todos: Todo[];
filter: 'all' | 'active' | 'completed';
addTodo: (text: string) => void;
toggleTodo: (id: string) => void;
}
const useTodoStore = create<TodoStore>((set) => ({
todos: [],
filter: 'all',
addTodo: (text) => set((state) => ({
todos: [...state.todos, { id: Date.now(), text, completed: false }]
})),
toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
}))
}));
// Using selectors
const TodoList: React.FC = () => {
// Only re-renders when todos change
const todos = useTodoStore((state) => state.todos);
// Only re-renders when filter changes
const filter = useTodoStore((state) => state.filter);
const filteredTodos = useMemo(() => {
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
default:
return todos;
}
}, [todos, filter]);
return (
<ul>
{filteredTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
};
3. Accessing State Outside React
Sometimes you need to access or update state outside of React components:
interface AuthStore {
user: User | null;
token: string | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
}
const useAuthStore = create<AuthStore>((set) => ({
user: null,
token: null,
login: async (credentials) => {
const response = await api.login(credentials);
set({ user: response.user, token: response.token });
},
logout: () => set({ user: null, token: null })
}));
// Utility function outside React
const setupAxiosInterceptors = () => {
axios.interceptors.request.use((config) => {
const token = useAuthStore.getState().token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
};
// Subscribe to state changes
const unsubscribe = useAuthStore.subscribe(
(state) => state.token,
(token) => {
if (token) {
localStorage.setItem('token', token);
} else {
localStorage.removeItem('token');
}
}
);
4. Hybrid Approach
Sometimes you might need the latest state value without causing re-renders:
const ChatComponent: React.FC = () => {
const sendMessage = useCallback(async (text: string) => {
// Get latest state without subscription
const { user } = useAuthStore.getState();
if (!user) return;
await api.sendMessage({
text,
userId: user.id,
timestamp: Date.now()
});
}, []);
return <MessageInput onSend={sendMessage} />;
};
Best Practices
1. Store Organization
Keep your stores focused and organized:
// Split into multiple stores
const useUserStore = create<UserStore>((set) => ({
user: null,
setUser: (user) => set({ user })
}));
const useSettingsStore = create<SettingsStore>((set) => ({
theme: 'light',
language: 'en',
updateSettings: (settings) => set(settings)
}));
// Combine stores when needed
const useAppStore = () => {
const user = useUserStore((state) => state.user);
const settings = useSettingsStore((state) => ({
theme: state.theme,
language: state.language
}));
return { user, settings };
};
2. TypeScript Integration
Leverage TypeScript for better type safety:
interface StoreState {
count: number;
increment: () => void;
decrement: () => void;
}
interface StoreActions {
reset: () => void;
}
const useStore = create<StoreState & StoreActions>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}));
3. Performance Optimization
Use middleware for debugging and optimization:
import { devtools, persist } from 'zustand/middleware';
const useStore = create<StoreState>()(
devtools(
persist(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 }))
}),
{
name: 'app-storage',
getStorage: () => localStorage
}
)
)
);
Advanced Patterns
1. Computed Values
Implement computed values using selectors:
interface TodoStore {
todos: Todo[];
getCompletedCount: () => number;
getActiveCount: () => number;
}
const useTodoStore = create<TodoStore>((set, get) => ({
todos: [],
getCompletedCount: () => get().todos.filter(t => t.completed).length,
getActiveCount: () => get().todos.filter(t => !t.completed).length
}));
// Usage with memoization
const TodoStats: React.FC = () => {
const completedCount = useTodoStore((state) => state.getCompletedCount());
const activeCount = useTodoStore((state) => state.getActiveCount());
return (
<div>
<p>Completed: {completedCount}</p>
<p>Active: {activeCount}</p>
</div>
);
};
2. Async Actions
Handle async operations gracefully:
interface DataStore {
data: Data[];
loading: boolean;
error: Error | null;
fetchData: () => Promise<void>;
}
const useDataStore = create<DataStore>((set) => ({
data: [],
loading: false,
error: null,
fetchData: async () => {
set({ loading: true, error: null });
try {
const response = await api.fetchData();
set({ data: response, loading: false });
} catch (error) {
set({ error, loading: false });
}
}
}));
Conclusion
Zustand provides a flexible and powerful state management solution for React applications. By following these patterns and best practices, you can:
- Maintain clean and organized state management
- Optimize performance with selectors
- Handle state both inside and outside React components
- Leverage TypeScript for better type safety
- Implement advanced patterns for complex scenarios
Choose the approach that best fits your specific use case, and remember that Zustand's simplicity is one of its greatest strengths.
References
On this page
- Introduction
- Understanding Zustand
- Usage Patterns
- 1. Hook-based Usage (Inside React Components)
- 2. Using Selectors for Performance
- 3. Accessing State Outside React
- 4. Hybrid Approach
- Best Practices
- 1. Store Organization
- 2. TypeScript Integration
- 3. Performance Optimization
- Advanced Patterns
- 1. Computed Values
- 2. Async Actions
- Conclusion
- References