React Hooks and State Management

React Hooks and State Management

React Hooks revolutionized how we write React components. They let you use state and other React features without writing a class. This guide covers everything you need to know about modern React development.

What are React Hooks?

Hooks are functions that let you “hook into” React features from function components. They were introduced in React 16.8 and solve common problems like wrapper hell and complex component logic.

Why Hooks Matter

Before hooks, stateful components required classes:

// Old way with classes
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.increment}>+</button>
      </div>
    );
  }
}

With hooks, same logic becomes much cleaner:

// Modern way with hooks
function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
    </div>
  );
}

Core Hooks

useState Hook

Manage state in function components:

import { useState } from 'react';

function TodoApp() {
  // Array destructuring: [currentValue, updateFunction]
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState('');

  const addTodo = () => {
    if (input.trim()) {
      setTodos([...todos, { 
        id: Date.now(), 
        text: input, 
        completed: false 
      }]);
      setInput('');
    }
  };

  return (
    <div>
      <input 
        value={input} 
        onChange={(e) => setInput(e.target.value)}
        placeholder="Add a todo..."
      />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </div>
  );
}

useReducer Hook

Manage complex state with actions:

import { useReducer } from 'react';

const initialState = {
  todos: [],
  filter: 'all' // 'all', 'active', 'completed'
};

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, action.payload]
      };
    
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      };
    
    case 'SET_FILTER':
      return {
        ...state,
        filter: action.payload
      };
    
    default:
      return state;
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, initialState);

  const addTodo = (text) => {
    dispatch({ 
      type: 'ADD_TODO', 
      payload: { 
        id: Date.now(), 
        text, 
        completed: false 
      } 
    });
  };

  const toggleTodo = (id) => {
    dispatch({ type: 'TOGGLE_TODO', payload: id });
  };

  const setFilter = (filter) => {
    dispatch({ type: 'SET_FILTER', payload: filter });
  };

  const filteredTodos = state.todos.filter(todo => {
    if (state.filter === 'active') return !todo.completed;
    if (state.filter === 'completed') return todo.completed;
    return true;
  });

  return (
    <div>
      <button onClick={() => setFilter('all')}>All</button>
      <button onClick={() => setFilter('active')}>Active</button>
      <button onClick={() => setFilter('completed')}>Completed</button>
      
      <ul>
        {filteredTodos.map(todo => (
          <li 
            key={todo.id}
            onClick={() => toggleTodo(todo.id)}
            style={{ 
              textDecoration: todo.completed ? 'line-through' : 'none' 
            }}
          >
            {todo.text}
          </li>
        ))}
      </ul>
    </div>
  );
}

Effect Hooks

useEffect Hook

Handle side effects like API calls and subscriptions:

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;

    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        const userData = await response.json();
        
        if (isMounted) {
          setUser(userData);
          setError(null);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
          setUser(null);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };

    fetchUser();

    // Cleanup function
    return () => {
      isMounted = false;
    };
  }, [userId]); // Dependency array - re-run when userId changes

  if (loading) return <div>Loading user...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user found</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

useEffect for Multiple Effects

import { useState, useEffect } from 'react';

function App() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  const [onlineStatus, setOnlineStatus] = useState(navigator.onLine);

  // Handle window resize
  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);

    // Cleanup
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // Empty array means run once

  // Handle online/offline status
  useEffect(() => {
    const handleOnline = () => setOnlineStatus(true);
    const handleOffline = () => setOnlineStatus(false);

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return (
    <div>
      <p>Window: {windowSize.width} x {windowSize.height}</p>
      <p>Status: {onlineStatus ? 'Online' : 'Offline'}</p>
    </div>
  );
}

Custom Hooks

Create reusable logic with custom hooks:

useLocalStorage Hook

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  // Get initial value from localStorage
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  // Update localStorage when value changes
  const setValue = (value) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue];
}

// Using the custom hook
function App() {
  const [name, setName] = useLocalStorage('name', '');
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  return (
    <div className={`app ${theme}`}>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Enter your name"
      />
      <select value={theme} onChange={(e) => setTheme(e.target.value)}>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
      <h1>Hello, {name || 'Guest'}!</h1>
    </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;
}

// Using debounce for search
function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 500);

  useEffect(() => {
    if (debouncedSearchTerm) {
      console.log('Searching for:', debouncedSearchTerm);
      // Make API call here
    }
  }, [debouncedSearchTerm]);

  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search..."
      />
      <p>Searching for: {debouncedSearchTerm}</p>
    </div>
  );
}

Advanced Hooks

useContext Hook

Share state between components without prop drilling:

import { createContext, useContext } from 'react';

// Create context
const ThemeContext = createContext();

// Theme provider component
function App() {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(currentTheme => currentTheme === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <Toolbar />
      <Content />
    </ThemeContext.Provider>
  );
}

// Consumer component using context
function Toolbar() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <div className={`toolbar ${theme}`}>
      <button onClick={toggleTheme}>
        Switch to {theme === 'light' ? 'dark' : 'light'} theme
      </button>
    </div>
  );
}

function Content() {
  const { theme } = useContext(ThemeContext);

  return (
    <div className={`content ${theme}`}>
      <p>This content uses the {theme} theme.</p>
    </div>
  );
}

useRef Hook

Access DOM elements and store mutable values:

import { useRef, useEffect } from 'react';

function TextInputWithFocus() {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current.focus();
  };

  useEffect(() => {
    // Focus input on component mount
    inputRef.current.focus();
  }, []);

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Click button to focus" />
      <button onClick={focusInput}>Focus Input</button>
    </div>
  );
}

// Using useRef for previous value
function PreviousValueExample() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();

  useEffect(() => {
    prevCountRef.current = count;
  });

  const prevCount = prevCountRef.current;

  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {prevCount || 'N/A'}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

State Management Patterns

Multiple useState vs useReducer

Choose based on complexity:

// Simple state - use useState
function SimpleForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [age, setAge] = useState('');
  
  // Good for few independent values
  return <form>...</form>;
}

// Complex state - use useReducer
function ComplexForm() {
  const [state, dispatch] = useReducer(formReducer, {
    user: { name: '', email: '', age: '' },
    validation: { errors: [], isValid: false },
    ui: { isSubmitting: false, showSuccess: false }
  });
  
  // Better for related state values
  return <form>...</form>;
}

Lifting State Up

Share state between sibling components:

function Parent() {
  const [sharedData, setSharedData] = useState('');

  return (
    <div>
      <ChildA 
        data={sharedData} 
        onDataChange={setSharedData} 
      />
      <ChildB data={sharedData} />
    </div>
  );
}

function ChildA({ data, onDataChange }) {
  return (
    <input
      value={data}
      onChange={(e) => onDataChange(e.target.value)}
      placeholder="Type something..."
    />
  );
}

function ChildB({ data }) {
  return <p>Data from sibling: {data}</p>;
}

Performance Optimization

useMemo Hook

Cache expensive calculations:

import { useMemo } from 'react';

function ExpensiveComponent({ items, filter }) {
  const expensiveCalculation = useMemo(() => {
    console.log('Running expensive calculation...');
    return items
      .filter(item => item.name.toLowerCase().includes(filter.toLowerCase()))
      .reduce((sum, item) => sum + item.price, 0);
  }, [items, filter]); // Re-calculate only when items or filter changes

  return (
    <div>
      <p>Total: ${expensiveCalculation}</p>
    </div>
  );
}

useCallback Hook

Cache function references:

import { useState, useCallback } from 'react';

function TodoList({ todos }) {
  const [newTodo, setNewTodo] = useState('');

  // Function won't be recreated on every render
  const addTodo = useCallback((text) => {
    if (text.trim()) {
      // Add todo logic here
      console.log('Adding:', text);
    }
  }, []); // Empty dependency array - function never changes

  const handleSubmit = useCallback((e) => {
    e.preventDefault();
    addTodo(newTodo);
    setNewTodo('');
  }, [newTodo, addTodo]);

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={newTodo}
        onChange={(e) => setNewTodo(e.target.value)}
        placeholder="Add todo..."
      />
      <button type="submit">Add</button>
      
      {todos.map(todo => (
        <TodoItem 
          key={todo.id} 
          todo={todo} 
          onUpdate={addTodo} // Stable function reference
        />
      ))}
    </form>
  );
}

Complete Example: Shopping Cart

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

// Cart context
const CartContext = createContext();

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_TO_CART':
      return {
        ...state,
        items: [...state.items, action.payload],
        total: state.total + action.payload.price
      };
    
    case 'REMOVE_FROM_CART':
      const item = state.items.find(item => item.id === action.payload);
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload),
        total: state.total - item.price
      };
    
    case 'CLEAR_CART':
      return { items: [], total: 0 };
    
    default:
      return state;
  }
}

function CartProvider({ children }) {
  const [cart, dispatch] = useReducer(cartReducer, { items: [], total: 0 });

  const addToCart = (product) => {
    dispatch({ type: 'ADD_TO_CART', payload: product });
  };

  const removeFromCart = (productId) => {
    dispatch({ type: 'REMOVE_FROM_CART', payload: productId });
  };

  const clearCart = () => {
    dispatch({ type: 'CLEAR_CART' });
  };

  const value = { cart, addToCart, removeFromCart, clearCart };

  return (
    <CartContext.Provider value={value}>
      {children}
    </CartContext.Provider>
  );
}

function ProductList() {
  const { addToCart } = useContext(CartContext);

  const products = [
    { id: 1, name: 'Laptop', price: 999 },
    { id: 2, name: 'Mouse', price: 29 },
    { id: 3, name: 'Keyboard', price: 79 }
  ];

  return (
    <div>
      <h2>Products</h2>
      {products.map(product => (
        <div key={product.id} className="product">
          <h3>{product.name}</h3>
          <p>${product.price}</p>
          <button onClick={() => addToCart(product)}>
            Add to Cart
          </button>
        </div>
      ))}
    </div>
  );
}

function ShoppingCart() {
  const { cart, removeFromCart, clearCart } = useContext(CartContext);

  return (
    <div>
      <h2>Shopping Cart ({cart.items.length} items)</h2>
      {cart.items.map(item => (
        <div key={item.id} className="cart-item">
          <span>{item.name} - ${item.price}</span>
          <button onClick={() => removeFromCart(item.id)}>
            Remove
          </button>
        </div>
      ))}
      <div className="cart-total">
        Total: ${cart.total}
      </div>
      <button onClick={clearCart}>Clear Cart</button>
    </div>
  );
}

function App() {
  return (
    <CartProvider>
      <div>
        <ProductList />
        <ShoppingCart />
      </div>
    </CartProvider>
  );
}

Best Practices

Rules of Hooks

  1. Only call hooks at the top level - Never inside loops, conditions, or nested functions
  2. Only call hooks from React functions - Never from regular JavaScript functions
  3. Custom hooks must start with “use” - Follow the naming convention
// Good
function MyComponent() {
  const [state, setState] = useState(initialState);
  // ... rest of component
}

// Bad - hook inside condition
function MyComponent() {
  if (someCondition) {
    const [state, setState] = useState(initialState); // ❌
  }
}

// Good - extract custom hook
function useCustomCondition(someCondition) {
  const [state, setState] = useState(initialState);
  return [state, setState];
}

Performance Tips

// Use useMemo for expensive calculations
const expensiveValue = useMemo(() => {
  return heavyCalculation(data);
}, [data]);

// Use useCallback for event handlers
const handleClick = useCallback((id) => {
  doSomething(id);
}, [doSomething]);

// Split state when possible
const [name, setName] = useState('');
const [email, setEmail] = useState('');
// Better than:
const [formData, setFormData] = useState({ name: '', email: '' });

External Resources:

Related Tutorials:

Last updated on