← Back to Guides

Complete Guide to React Hooks in 2025

📖 12 min read | 📅 Updated: January 2025 | 🏷️ Web Development

Introduction to React Hooks

React Hooks revolutionized how we write React components by allowing us to use state and other React features without writing class components. Introduced in React 16.8, Hooks have become the standard way to build React applications in 2025.

In this comprehensive guide, we'll cover all essential React Hooks, their use cases, best practices, and common pitfalls to avoid. Whether you're new to React or looking to deepen your understanding, this guide will provide you with practical knowledge and real-world examples.

1. useState - Managing Component State

useState is the most fundamental Hook that lets you add state to functional components. It returns an array with two elements: the current state value and a function to update it.

Basic Usage

import { useState } from 'react';

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

Advanced State Management

You can use useState with objects and arrays:

const [user, setUser] = useState({
  name: 'John',
  age: 30,
  email: 'john@example.com'
});

// Update single property
setUser(prevUser => ({
  ...prevUser,
  age: 31
}));
💡 Best Practice: When updating state based on previous state, always use the functional form of setState to avoid race conditions.

2. useEffect - Side Effects & Lifecycle

useEffect lets you perform side effects in functional components. It's similar to componentDidMount, componentDidUpdate, and componentWillUnmount combined.

Basic Data Fetching

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    async function fetchUser() {
      setLoading(true);
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      setUser(data);
      setLoading(false);
    }
    
    fetchUser();
  }, [userId]); // Re-run when userId changes
  
  if (loading) return <div>Loading...</div>;
  return <div>{user.name}</div>;
}

Cleanup Functions

Always cleanup subscriptions and timers to prevent memory leaks:

useEffect(() => {
  const timer = setInterval(() => {
    console.log('Tick');
  }, 1000);
  
  // Cleanup function
  return () => clearInterval(timer);
}, []);
⚠️ Warning: Forgetting dependencies in the dependency array can lead to bugs. Use ESLint's exhaustive-deps rule to catch these issues.

3. useContext - Sharing Data Globally

useContext allows you to consume context values without wrapping components in Consumer components. Perfect for theme management, authentication, and global state.

import { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  const theme = useContext(ThemeContext);
  return <div className={theme}>Current theme: {theme}</div>;
}

4. useReducer - Complex State Logic

useReducer is preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.

import { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return initialState;
    default:
      throw new Error('Unknown action');
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </>
  );
}

5. Custom Hooks - Reusable Logic

Custom Hooks let you extract component logic into reusable functions. They must start with "use" and can call other Hooks.

Example: useFetch Hook

import { useState, useEffect } from 'react';

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

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

6. useCallback & useMemo - Performance Optimization

These Hooks help optimize performance by memoizing values and functions to prevent unnecessary re-renders.

useCallback

import { useCallback } from 'react';

function ParentComponent() {
  const [count, setCount] = useState(0);
  
  // Memoized callback
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []); // Only recreate if dependencies change
  
  return <ChildComponent onClick={handleClick} />;
}

useMemo

import { useMemo } from 'react';

function ExpensiveComponent({ items }) {
  const total = useMemo(() => {
    return items.reduce((sum, item) => sum + item.price, 0);
  }, [items]); // Only recalculate when items change
  
  return <div>Total: ${total}</div>;
}
💡 Pro Tip: Don't over-optimize! Use useCallback and useMemo only when you have measurable performance issues. Premature optimization can make code harder to read.

7. useRef - DOM References & Mutable Values

useRef returns a mutable ref object that persists for the full lifetime of the component. Useful for accessing DOM elements and storing mutable values that don't trigger re-renders.

import { useRef, useEffect } from 'react';

function TextInput() {
  const inputRef = useRef(null);
  
  useEffect(() => {
    // Focus input on mount
    inputRef.current.focus();
  }, []);
  
  return <input ref={inputRef} type="text" />;
}

Best Practices & Common Pitfalls

Rules of Hooks

Dependency Array Best Practices

Performance Considerations

Conclusion

React Hooks have fundamentally changed how we write React applications, making code more readable, reusable, and easier to test. By mastering the core Hooks and understanding when to create custom Hooks, you'll be able to build robust and maintainable React applications.

Remember to follow the Rules of Hooks, properly manage dependencies, and optimize only when necessary. With practice, Hooks will become second nature and you'll wonder how you ever lived without them!

🚀 Next Steps: