POPOK ACADEMY LOGOPOPOK
ACADEMY
React
August 15, 202512 min read

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.

MNMakara Nuol
Intermediate
React
Hooks
JavaScript
Frontend
State Management

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.

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

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

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.

Share this article

Send this note to someone learning the same topic.

Share

Recent posts