Shortcuts
BlogDecember 15, 2024

Mohamed Elbarry
State Management in React
State in React represents data that can change over time and affects what the user sees. Understanding how to manage state effectively is crucial for building maintainable and performant React applications. The useState hook is the foundation of state management in React:
useState Hook
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');

const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);

return (

<div>
<h2>Count: {count}</h2>
<input
  value={name}
  onChange={(e) => setName(e.target.value)}
  placeholder='Enter name'
/>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
); }
For complex state logic, useReducer provides more control:
useReducer Hook
function reducer(state, action) {
switch (action.type) {
  case 'INCREMENT':
    return { ...state, count: state.count + 1 };
  case 'DECREMENT':
    return { ...state, count: state.count - 1 };
  case 'SET_NAME':
    return { ...state, name: action.payload };
  case 'RESET':
    return initialState;
  default:
    return state;
}
}

function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);

const increment = () => dispatch({ type: 'INCREMENT' });
const decrement = () => dispatch({ type: 'DECREMENT' });
const setName = (name) => dispatch({ type: 'SET_NAME', payload: name });

return (

<div>
<h2>Count: {state.count}</h2>
<input
  value={state.name}
  onChange={(e) => setName(e.target.value)}
  placeholder='Enter name'
/>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
); }
Context API
// Create context
const AppContext = createContext(null);

// Provider component
export function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, initialState);

return (

<AppContext.Provider value={{ state, dispatch }}>{children}</AppContext.Provider>
); }

// Custom hook to use context
export function useApp() {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp must be used within an AppProvider');
}
return context;
}
Custom Form Hook
export function useForm({ initialValues, validate, onSubmit }) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);

const setValue = useCallback((name, value) => {
setValues(prev => ({ ...prev, [name]: value }));

  // Clear error when user starts typing
  if (errors[name]) {
    setErrors(prev => ({ ...prev, [name]: undefined }));
  }

}, [errors]);

const handleSubmit = useCallback(async (e) => {
e.preventDefault();

  if (!validateForm()) return;

  setIsSubmitting(true);
  try {
    await onSubmit(values);
  } finally {
    setIsSubmitting(false);
  }

}, [values, validateForm, onSubmit]);

return {
values,
errors,
touched,
isSubmitting,
setValue,
handleSubmit
};
}
Zustand
import { create } from 'zustand';

interface UserStore {
user: User | null;
setUser: (user: User | null) => void;
logout: () => void;
}

export const useUserStore =
create <
UserStore >
((set) => ({
  user: null,
  setUser: (user) => set({ user }),
  logout: () => set({ user: null }),
}));

// Usage in components
function UserProfile() {
const { user, setUser, logout } = useUserStore();

if (!user) return <div>Please log in</div>;

return (

<div>
<h2>Welcome, {user.name}!</h2>
<button onClick={logout}>Logout</button>
</div>
); }
Redux Toolkit
import { createSlice, configureStore } from '@reduxjs/toolkit';

// Slice
const usersSlice = createSlice({
name: 'users',
initialState: {
users: [],
loading: false,
error: null
},
reducers: {
addUser: (state, action) => {
state.users.push(action.payload);
},
removeUser: (state, action) => {
state.users = state.users.filter(user => user.id !== action.payload);
}
}
});

export const { addUser, removeUser } = usersSlice.actions;

// Store
export const store = configureStore({
reducer: {
users: usersSlice.reducer
}
});
React Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// Query hooks
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json()),
staleTime: 5 _ 60 _ 1000, // 5 minutes
});
}

// Mutation hooks
function useCreateUser() {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (userData) =>
fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
}).then(res => res.json()),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}
State Structure
// Good: Normalized state structure
interface AppState {
users: {
  byId: Record<string, User>;
  allIds: string[];
};
posts: {
  byId: Record<string, Post>;
  allIds: string[];
};
ui: {
  loading: boolean;
  error: string | null;
  currentPage: number;
};
}
  • Start simple - Use useState and useReducer for local state
  • Lift state up - Move shared state to common ancestors
  • Use Context sparingly - Only for truly global state
  • Consider external libraries - For complex state management needs
  • Normalize your state - Keep data structure flat and normalized
  • Optimize performance - Use memoization and selectors
  • Separate concerns - Keep UI state separate from business logic
The best state management solution is the one that fits your application's needs while remaining maintainable and performant.
Share this post: