React Hooks - Modern React State Management Explained

React Hooks, introduced in React 16.8, revolutionized functional components by bringing state and lifecycle features without classes.

From useState for simple state to useEffect for side effects and custom hooks for reusable logic, Hooks make React code cleaner and more intuitive.

This tutorial covers essential React Hooks with practical examples to help you build modern React applications confidently.

useState Hook

useState is the foundation of state management in functional components, returning an array with current state and a setter function.

JSX
Basic useState examples for different data types
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  const [todos, setTodos] = useState([]);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

Always use functional updates for state based on previous state to avoid stale closures.

useEffect Hook

useEffect handles side effects in functional components, replacing componentDidMount, componentDidUpdate, and componentWillUnmount.

JSX
useEffect for data fetching and cleanup
import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
      
    // Cleanup function
    return () => {
      console.log('Cleanup');
    };
  }, [userId]); // Dependency array
  
  return <div>{user?.name}</div>;
}

The dependency array controls when useEffect runs - empty array for mount only, specific values for targeted updates.

useRef and useContext

useRef creates mutable refs for DOM access or storing values across renders. useContext provides global state without prop drilling.

JSX
useRef for DOM manipulation and useContext for theme
// ThemeContext.js
const ThemeContext = createContext();

// useRef example
function TextInput() {
  const inputRef = useRef(null);
  
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  
  return <input ref={inputRef} />;
}

// useContext example
function Button() {
  const theme = useContext(ThemeContext);
  return <button style={theme}>Click me</button>;
}

useReducer Hook

useReducer is perfect for complex state logic, similar to Redux but local to components.

JSX
useReducer for todo application
function todosReducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return [...state, action.payload];
    case 'DELETE':
      return state.filter(todo => todo.id !== action.id);
    default:
      return state;
  }
}

function TodoApp() {
  const [todos, dispatch] = useReducer(todosReducer, []);
  
  const addTodo = (text) => {
    dispatch({ type: 'ADD', payload: { id: Date.now(), text } });
  };
  
  return (
    <div>
      {todos.map(todo => (
        <div key={todo.id}>
          {todo.text}
          <button onClick={() => dispatch({ type: 'DELETE', id: todo.id })}>
            Delete
          </button>
        </div>
      ))}
    </div>
  );
}

Custom Hooks

Custom Hooks let you extract reusable stateful logic into independent functions.

JSX
Custom useFetch hook
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);
  
  return { data, loading, error };
}

// Usage
function UserList() {
  const { data: users, loading, error } = useFetch('/api/users');
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

Custom Hooks must start with 'use' and can call other Hooks.

Other Essential Hooks

  • useMemo for expensive calculations and preventing unnecessary re-renders
  • useCallback for memoizing functions passed to child components
  • useLayoutEffect for DOM mutations before browser paint
  • useImperativeHandle for exposing component methods to parents
  • useTransition for marking non-urgent state updates
  • useDeferredValue for debouncing expensive renders
JSX
useMemo and useCallback examples
function ExpensiveComponent({ items }) {
  const expensiveValue = useMemo(() => {
    return items.reduce((sum, item) => sum + item.value, 0);
  }, [items]);
  
  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []);
  
  return <div>Total: {expensiveValue}</div>;
}

Hooks Best Practices

  • Only call Hooks at the top level - never in loops, conditions, or nested functions
  • Keep effects small and focused on single concerns
  • Use ESLint plugin react-hooks to catch mistakes
  • Extract logic into custom Hooks for reusability
  • Always provide exhaustive dependencies in useEffect/useCallback/useMemo

Conclusion

React Hooks have become the standard way to build React applications, eliminating the need for class components in most cases.

Mastering useState, useEffect, useReducer, and custom Hooks will make you a proficient React developer capable of building complex applications.

Practice these patterns in real projects and explore the React documentation for advanced patterns and optimizations.