React
Intermediate
React
Hooks
JavaScript
Frontend
State Management

Mastering React Hooks: A Complete Guide

Deep dive into React Hooks and learn how to use useState, useEffect, useContext, and custom hooks to build powerful React applications.

Makara Nuol
August 15, 2025
12 min read

Mastering React Hooks: A Complete Guide

React Hooks revolutionized how we write React components by allowing us to use state and other React features in functional components. This guide will take you through the most important hooks and show you how to create your own custom hooks.

What Are React Hooks?

Hooks are functions that let you "hook into" React state and lifecycle features from functional components. They were introduced in React 16.8 and have become the standard way to write React components.

ℹ️

Rules of Hooks

  1. Only call hooks at the top level of your React functions
  2. Only call hooks from React functions (components or custom hooks)
  3. Hook names should start with "use"

useState Hook

The useState hook lets you add state to functional components:

💻Basic useState Example
import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  )
}

Complex State with useState

💻Object State
function UserProfile() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  })

  const updateName = (name: string) => {
    setUser(prevUser => ({
      ...prevUser,
      name
    }))
  }

  return (
    <div>
      <input 
        value={user.name}
        onChange={(e) => updateName(e.target.value)}
        placeholder="Name"
      />
      <p>Hello, {user.name}!</p>
    </div>
  )
}

useEffect Hook

The useEffect hook lets you perform side effects in functional components:

💻Basic useEffect
import { useState, useEffect } from 'react'

function DataFetcher() {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    async function fetchData() {
      try {
        const response = await fetch('/api/data')
        const result = await response.json()
        setData(result)
      } catch (error) {
        console.error('Error fetching data:', error)
      } finally {
        setLoading(false)
      }
    }

    fetchData()
  }, []) // Empty dependency array means this runs once

  if (loading) return <div>Loading...</div>
  
  return <div>{JSON.stringify(data)}</div>
}

Cleanup with useEffect

💻Effect Cleanup
function Timer() {
  const [seconds, setSeconds] = useState(0)

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(prev => prev + 1)
    }, 1000)

    // Cleanup function
    return () => clearInterval(interval)
  }, [])

  return <div>Timer: {seconds} seconds</div>
}

useContext Hook

The useContext hook lets you consume context values:

💻Context Setup and Usage
import { createContext, useContext, useState } from 'react'

// Create context
const ThemeContext = createContext()

// Provider component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

// Consumer component
function ThemedButton() {
  const { theme, setTheme } = useContext(ThemeContext)

  return (
    <button 
      style={{ 
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#333' : '#fff'
      }}
      onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
    >
      Toggle Theme
    </button>
  )
}

useReducer Hook

For complex state logic, useReducer is often preferable to useState:

💻useReducer Example
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()
  }
}

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

  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  )
}

Custom Hooks

Custom hooks let you extract component logic into reusable functions:

💻Custom Hook for API Calls
import { useState, useEffect } from 'react'

function useApi(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('Failed to fetch')
        const result = await response.json()
        setData(result)
      } catch (err) {
        setError(err.message)
      } finally {
        setLoading(false)
      }
    }

    fetchData()
  }, [url])

  return { data, loading, error }
}

// Usage
function UserList() {
  const { data: users, loading, error } = useApi('/api/users')

  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error}</div>

  return (
    <ul>
      {users?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

Advanced Hook Patterns

Custom Hook for Local Storage

💻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) {
      return initialValue
    }
  })

  const setValue = (value) => {
    try {
      setStoredValue(value)
      window.localStorage.setItem(key, JSON.stringify(value))
    } catch (error) {
      console.error('Error saving to localStorage:', 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>
  )
}

Performance Optimization

useMemo and useCallback

💻Performance Hooks
import { useState, useMemo, useCallback } from 'react'

function ExpensiveComponent({ items, onItemClick }) {
  // Memoize expensive calculations
  const expensiveValue = useMemo(() => {
    return items.reduce((sum, item) => sum + item.value, 0)
  }, [items])

  // Memoize callback functions
  const handleClick = useCallback((id) => {
    onItemClick(id)
  }, [onItemClick])

  return (
    <div>
      <p>Total: {expensiveValue}</p>
      {items.map(item => (
        <button key={item.id} onClick={() => handleClick(item.id)}>
          {item.name}
        </button>
      ))}
    </div>
  )
}

Best Practices

💡

Hook Best Practices

  1. Keep hooks at the top level of your components
  2. Use custom hooks to share logic between components
  3. Use useCallback and useMemo sparingly, only when you have performance issues
  4. Prefer multiple useState calls over a single complex state object
  5. Use useReducer for complex state logic

Testing Hooks

💻Testing Custom 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)
})

Common Pitfalls

⚠️

Common Mistakes

  1. Forgetting dependencies in useEffect - This can cause stale closures
  2. Calling hooks conditionally - This breaks the rules of hooks
  3. Not cleaning up effects - This can cause memory leaks
  4. Overusing useMemo/useCallback - These have their own overhead

Conclusion

React Hooks provide a powerful and flexible way to manage state and side effects in functional components. By mastering these patterns and creating your own custom hooks, you can write more reusable and maintainable React code.

The key to success with hooks is understanding when and how to use each one effectively. Start with the basics (useState and useEffect) and gradually incorporate more advanced patterns as your applications grow in complexity.

Found this helpful?

Share this article with others who might benefit from it.