The Complete Guide to React Hooks

React Hooks were introduced in React 16.8, enabling you to use state and other React features inside functional components. They fundamentally changed how we write components.

Why Hooks?

Before Hooks, if you needed to manage state or handle side effects in a component, you had to use a class component:

class Counter extends React.Component {
  state = { count: 0 }

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

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

Rewritten with Hooks, the same component becomes far more concise:

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

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

useState

useState is the most fundamental Hook — it lets you add local state to a functional component.

const [state, setState] = useState(initialValue)

Basic Usage

function LoginForm() {
  const [username, setUsername] = useState('')
  const [password, setPassword] = useState('')

  const handleSubmit = (e) => {
    e.preventDefault()
    console.log({ username, password })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        placeholder="Username"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit">Log in</button>
    </form>
  )
}

Managing Multiple Fields with an Object

When you have many fields, group them into a single state object:

function ProfileForm() {
  const [form, setForm] = useState({
    name: '',
    email: '',
    bio: '',
  })

  const handleChange = (field) => (e) => {
    setForm((prev) => ({ ...prev, [field]: e.target.value }))
  }

  return (
    <form>
      <input value={form.name} onChange={handleChange('name')} />
      <input value={form.email} onChange={handleChange('email')} />
      <textarea value={form.bio} onChange={handleChange('bio')} />
    </form>
  )
}

Note: Unlike setState in class components, the useState setter does not automatically merge objects — you need to spread ...prev manually.


useEffect

useEffect handles side effects such as data fetching, event subscriptions, and DOM manipulation.

useEffect(() => {
  // side effect logic

  return () => {
    // cleanup function (optional)
  }
}, [dependencies])

Data Fetching

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

  useEffect(() => {
    let cancelled = false

    async function fetchUser() {
      setLoading(true)
      const res = await fetch(`/api/users/${userId}`)
      const data = await res.json()

      if (!cancelled) {
        setUser(data)
        setLoading(false)
      }
    }

    fetchUser()

    return () => {
      cancelled = true
    }
  }, [userId])

  if (loading) return <p>Loading...</p>
  if (!user) return <p>User not found</p>

  return <div>{user.name}</div>
}

The Three Forms of the Dependency Array

Form Behavior
No second argument Runs after every render
[] empty array Runs once on mount only
[a, b] Runs when a or b changes

useContext

useContext lets you read a React Context value directly, without threading props through every layer of the tree.

Building a Theme Toggle

1. Define the Context:

// ThemeContext.js
import { createContext, useContext, useState } from 'react'

const ThemeContext = createContext()

export 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>
  )
}

export function useTheme() {
  return useContext(ThemeContext)
}

2. Consume it in any component:

function Header() {
  const { theme, toggleTheme } = useTheme()

  return (
    <header className={`header header--${theme}`}>
      <button onClick={toggleTheme}>
        Switch to {theme === 'light' ? 'dark' : 'light'} mode
      </button>
    </header>
  )
}

useRef

useRef has two main uses: accessing DOM elements directly, and storing a mutable value that does not trigger a re-render when it changes.

function VideoPlayer() {
  const videoRef = useRef(null)

  return (
    <div>
      <video ref={videoRef} src="/video.mp4" />
      <button onClick={() => videoRef.current.play()}>Play</button>
      <button onClick={() => videoRef.current.pause()}>Pause</button>
    </div>
  )
}

Custom Hooks

Custom Hooks are one of the most powerful features in React — extract reusable stateful logic into a standalone function and share it across multiple components.

useFetch

function useFetch(url) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    let cancelled = false

    async function fetchData() {
      try {
        setLoading(true)
        const res = await fetch(url)
        if (!res.ok) throw new Error(`HTTP ${res.status}`)
        const json = await res.json()
        if (!cancelled) setData(json)
      } catch (err) {
        if (!cancelled) setError(err.message)
      } finally {
        if (!cancelled) setLoading(false)
      }
    }

    fetchData()
    return () => { cancelled = true }
  }, [url])

  return { data, loading, error }
}

Using it is clean and straightforward:

function PostList() {
  const { data: posts, loading, error } = useFetch('/api/posts')

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

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Rules of Hooks

There are two rules you must always follow:

  1. Only call Hooks at the top level — never inside conditions, loops, or nested functions
  2. Only call Hooks from React functions — not from plain JavaScript functions
// ❌ Wrong: calling a Hook inside a condition
function Bad() {
  if (someCondition) {
    const [count, setCount] = useState(0) // not allowed!
  }
}

// ✅ Correct: always call at the top level
function Good() {
  const [count, setCount] = useState(0)

  if (someCondition) {
    return <div>{count}</div>
  }
}

Summary

Hook Purpose
useState Manage local state
useEffect Handle side effects (fetching, subscriptions, DOM)
useContext Read Context values without prop drilling
useRef Access DOM elements / store non-rendering mutable values
useCallback Memoize function references to optimize child renders
useMemo Memoize expensive computed values
Custom Hook Extract and reuse stateful logic across components

React Hooks significantly reduce code complexity and make logic reuse feel natural. Start with useState and useEffect, then work your way toward building your own custom Hooks.

Comments