Web Development 17 min read

React Hooks: A Complete Guide for Modern Development

Master React Hooks with this comprehensive guide covering useState, useEffect, custom hooks, and advanced patterns for building powerful React applications.

A

Alex Johnson

Author

Share:
React Hooks: A Complete Guide for Modern Development

React Hooks: A Complete Guide for Modern Development

React Hooks revolutionized how we write React components. This comprehensive guide will take you from basic hook usage to advanced patterns that will make your React applications more powerful and maintainable.

What are React Hooks?

Hooks are functions that let you “hook into” React features from functional components. They allow you to use state and other React features without writing class components.

Basic Hooks

useState: Managing Component State

The most fundamental hook for managing state in functional components:

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      <button onClick={() => setCount(prev => prev - 1)}>
        Decrement
      </button>
    </div>
  );
}

useState with Objects

function UserProfile() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  });
  
  const updateUser = (field, value) => {
    setUser(prev => ({
      ...prev,
      [field]: value
    }));
  };
  
  return (
    <form>
      <input 
        value={user.name}
        onChange={(e) => updateUser('name', e.target.value)}
        placeholder="Name"
      />
      <input 
        value={user.email}
        onChange={(e) => updateUser('email', e.target.value)}
        placeholder="Email"
      />
    </form>
  );
}

useEffect: Side Effects

Handle side effects like API calls, subscriptions, and DOM manipulation:

import React, { useState, useEffect } from 'react';

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetchUsers();
  }, []); // Empty dependency array = run once on mount
  
  const fetchUsers = async () => {
    try {
      const response = await fetch('/api/users');
      const userData = await response.json();
      setUsers(userData);
    } catch (error) {
      console.error('Failed to fetch users:', error);
    } finally {
      setLoading(false);
    }
  };
  
  if (loading) return <div>Loading...</div>;
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

useEffect Cleanup

function Timer() {
  const [seconds, setSeconds] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);
    
    // Cleanup function
    return () => clearInterval(interval);
  }, []);
  
  return <div>Seconds: {seconds}</div>;
}

Conditional Effects

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  
  useEffect(() => {
    if (query) {
      searchAPI(query).then(setResults);
    }
  }, [query]); // Run when query changes
  
  return (
    <div>
      {results.map(result => (
        <div key={result.id}>{result.title}</div>
      ))}
    </div>
  );
}

Advanced Hooks

useContext: Global State Management

import React, { createContext, useContext, useState } from 'react';

const ThemeContext = createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  
  return (
    <button 
      onClick={toggleTheme}
      className={`btn btn-${theme}`}
    >
      Current theme: {theme}
    </button>
  );
}

useReducer: Complex State Logic

import React, { useReducer } from 'react';

const initialState = {
  items: [],
  loading: false,
  error: null
};

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        items: [...state.items, action.payload]
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload
            ? { ...item, completed: !item.completed }
            : item
        )
      };
    case 'SET_LOADING':
      return { ...state, loading: action.payload };
    case 'SET_ERROR':
      return { ...state, error: action.payload };
    default:
      return state;
  }
}

function TodoList() {
  const [state, dispatch] = useReducer(todoReducer, initialState);
  
  const addTodo = (text) => {
    dispatch({
      type: 'ADD_TODO',
      payload: {
        id: Date.now(),
        text,
        completed: false
      }
    });
  };
  
  return (
    <div>
      {state.items.map(todo => (
        <div key={todo.id}>
          <span 
            style={{ 
              textDecoration: todo.completed ? 'line-through' : 'none' 
            }}
          >
            {todo.text}
          </span>
          <button 
            onClick={() => dispatch({ 
              type: 'TOGGLE_TODO', 
              payload: todo.id 
            })}
          >
            Toggle
          </button>
        </div>
      ))}
    </div>
  );
}

useMemo: Performance Optimization

import React, { useState, useMemo } from 'react';

function ExpensiveComponent({ items, filter }) {
  const [count, setCount] = useState(0);
  
  // Expensive calculation only runs when items or filter change
  const filteredItems = useMemo(() => {
    console.log('Filtering items...');
    return items.filter(item => 
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);
  
  // This calculation runs on every render
  const expensiveValue = useMemo(() => {
    console.log('Calculating expensive value...');
    return filteredItems.reduce((sum, item) => sum + item.value, 0);
  }, [filteredItems]);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
      <p>Expensive Value: {expensiveValue}</p>
      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

useCallback: Function Memoization

import React, { useState, useCallback } from 'react';

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  // Without useCallback, this function is recreated on every render
  const handleClick = useCallback(() => {
    console.log('Button clicked!');
  }, []); // Empty deps = never recreated
  
  const handleNameChange = useCallback((newName) => {
    setName(newName);
  }, []); // This could also be empty since setName is stable
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
      
      <ChildComponent 
        onClick={handleClick}
        onNameChange={handleNameChange}
        name={name}
      />
    </div>
  );
}

const ChildComponent = React.memo(({ onClick, onNameChange, name }) => {
  console.log('ChildComponent rendered');
  
  return (
    <div>
      <button onClick={onClick}>Click Me</button>
      <input 
        value={name}
        onChange={(e) => onNameChange(e.target.value)}
        placeholder="Enter name"
      />
    </div>
  );
});

Custom Hooks

useLocalStorage Hook

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });
  
  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error);
    }
  };
  
  return [storedValue, setValue];
}

// Usage
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Current theme: {theme}
    </button>
  );
}

useFetch Hook

import { useState, useEffect } from 'react';

function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const response = await fetch(url, options);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    
    fetchData();
  }, [url, JSON.stringify(options)]);
  
  return { data, loading, error };
}

// Usage
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

useDebounce Hook

import { useState, useEffect } from 'react';

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);
  
  return debouncedValue;
}

// Usage
function SearchInput() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 500);
  
  useEffect(() => {
    if (debouncedSearchTerm) {
      // Perform search
      console.log('Searching for:', debouncedSearchTerm);
    }
  }, [debouncedSearchTerm]);
  
  return (
    <input
      type="text"
      value={searchTerm}
      onChange={(e) => setSearchTerm(e.target.value)}
      placeholder="Search..."
    />
  );
}

Hook Patterns and Best Practices

Rules of Hooks

  1. Only call hooks at the top level: Never in loops, conditions, or nested functions
  2. Only call hooks from React functions: Components or custom hooks
// ❌ Wrong - conditional hook
function BadComponent({ condition }) {
  if (condition) {
    const [state, setState] = useState(0); // This breaks the rules!
  }
  return <div>Bad component</div>;
}

// ✅ Correct - conditional logic inside hook
function GoodComponent({ condition }) {
  const [state, setState] = useState(condition ? 0 : null);
  
  if (condition) {
    return <div>State: {state}</div>;
  }
  return <div>No state needed</div>;
}

Optimizing Re-renders

import React, { useState, useCallback, useMemo } from 'react';

function OptimizedComponent({ items }) {
  const [filter, setFilter] = useState('');
  const [sortOrder, setSortOrder] = useState('asc');
  
  // Memoize expensive calculations
  const filteredAndSortedItems = useMemo(() => {
    const filtered = items.filter(item => 
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
    
    return filtered.sort((a, b) => {
      if (sortOrder === 'asc') {
        return a.name.localeCompare(b.name);
      }
      return b.name.localeCompare(a.name);
    });
  }, [items, filter, sortOrder]);
  
  // Memoize callback functions
  const handleFilterChange = useCallback((e) => {
    setFilter(e.target.value);
  }, []);
  
  const handleSortToggle = useCallback(() => {
    setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
  }, []);
  
  return (
    <div>
      <input 
        value={filter}
        onChange={handleFilterChange}
        placeholder="Filter items..."
      />
      <button onClick={handleSortToggle}>
        Sort {sortOrder === 'asc' ? '↑' : '↓'}
      </button>
      
      <ItemList items={filteredAndSortedItems} />
    </div>
  );
}

const ItemList = React.memo(({ items }) => (
  <ul>
    {items.map(item => (
      <li key={item.id}>{item.name}</li>
    ))}
  </ul>
));

Error Boundaries with Hooks

import React, { useState, useEffect } from 'react';

function useErrorHandler() {
  const [error, setError] = useState(null);
  
  const resetError = () => setError(null);
  
  const captureError = (error) => {
    setError(error);
    console.error('Captured error:', error);
  };
  
  return { error, resetError, captureError };
}

function ComponentWithErrorHandling() {
  const { error, resetError, captureError } = useErrorHandler();
  const [data, setData] = useState(null);
  
  useEffect(() => {
    fetch('/api/data')
      .then(response => response.json())
      .then(setData)
      .catch(captureError);
  }, [captureError]);
  
  if (error) {
    return (
      <div>
        <h2>Something went wrong!</h2>
        <p>{error.message}</p>
        <button onClick={resetError}>Try Again</button>
      </div>
    );
  }
  
  return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}

Testing Hooks

import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';

test('should increment counter', () => {
  const { result } = renderHook(() => useCounter());
  
  act(() => {
    result.current.increment();
  });
  
  expect(result.current.count).toBe(1);
});

test('should decrement counter', () => {
  const { result } = renderHook(() => useCounter(10));
  
  act(() => {
    result.current.decrement();
  });
  
  expect(result.current.count).toBe(9);
});

Common Mistakes and Solutions

Stale Closures

// ❌ Problem: Stale closure
function Timer() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setCount(count + 1); // Always adds 1 to initial count!
    }, 1000);
    
    return () => clearInterval(interval);
  }, []); // Empty deps cause stale closure
  
  return <div>{count}</div>;
}

// ✅ Solution: Use functional update
function Timer() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setCount(prevCount => prevCount + 1); // Always gets current count
    }, 1000);
    
    return () => clearInterval(interval);
  }, []); // Empty deps are now safe
  
  return <div>{count}</div>;
}

Infinite Re-renders

// ❌ Problem: Missing dependencies
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, []); // Missing userId dependency
  
  return <div>{user?.name}</div>;
}

// ✅ Solution: Include all dependencies
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]); // Include userId
  
  return <div>{user?.name}</div>;
}

Conclusion

React Hooks have transformed how we write React applications, making functional components more powerful and code more reusable. Key takeaways:

  1. Start with basic hooks: Master useState and useEffect first
  2. Create custom hooks: Encapsulate reusable logic
  3. Optimize when needed: Use useMemo and useCallback judiciously
  4. Follow the rules: Hooks have specific requirements for proper operation
  5. Think in hooks: Design your components around hook patterns

Remember, hooks are just functions that let you hook into React features. The more you practice with them, the more natural they’ll become in your React development workflow.

What hook pattern would you like to implement in your next project? Share your experiences in the comments!

Tags

#React #JavaScript #Hooks #Frontend

Related Articles

Building a REST API with Node.js and Express
Backend Development

Building a REST API with Node.js and Express

Learn how to create a robust REST API from scratch using Node.js, Express, and best practices for modern web development.

#Node.js #Express #API +2 more