State Management
3 min readState Management
TL;DR
Comprehensive guide to state management patterns in React applications.
How it works
Local Component State
// Simple local state
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Lifting State Up
function Parent() {
const [sharedValue, setSharedValue] = useState('');
return (
<>
<ChildA value={sharedValue} onChange={setSharedValue} />
<ChildB value={sharedValue} />
</>
);
}
Context API
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const value = {
user,
theme,
setUser,
setTheme
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
function useApp() {
const context = useContext(AppContext);
if (!context) throw new Error('useApp must be within AppProvider');
return context;
}
Redux Toolkit
// Store setup
import { configureStore, createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => { state.value += 1; },
decrement: state => { state.value -= 1; },
incrementByAmount: (state, action) => {
state.value += action.payload;
}
}
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
const store = configureStore({
reducer: {
counter: counterSlice.reducer
}
});
// Component usage
import { useSelector, useDispatch } from 'react-redux';
function Counter() {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<p>{count}</p>
<button onClick={() => dispatch(increment())}>+</button>
</div>
);
}
Zustand
import create from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}));
function Counter() {
const { count, increment, decrement } = useStore();
return (
<div>
<p>{count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
TanStack Query (React Query)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function Users() {
const queryClient = useQueryClient();
// Fetch data
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json())
});
// Mutation
const mutation = useMutation({
mutationFn: (newUser) => fetch('/api/users', {
method: 'POST',
body: JSON.stringify(newUser)
}),
onSuccess: () => {
queryClient.invalidateQueries(['users']);
}
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{data.map(user => <div key={user.id}>{user.name}</div>)}
<button onClick={() => mutation.mutate({ name: 'New User' })}>
Add User
</button>
</div>
);
}
Quick recall Q&A
Use Context for low-frequency updates (theme, auth). Use Redux for complex state with frequent updates, time-travel debugging, or when you need middleware.
Split contexts by update frequency, memoize context values, use useMemo for context value objects, or use state management libraries like Zustand.
Local state is specific to a component and its children. Global state is accessible throughout the app. Keep state as local as possible.
Use React Query for server state (fetching, caching, syncing). Use Redux for client state (UI state, user interactions). They can work together.
Use controlled components with useState, uncontrolled with useRef, or form libraries like Formik/React Hook Form for complex forms.