React.js Hooks: The Complete Practical Guide for Developers in 2026

javascript dev.to

If you want to write modern React applications and you want companies to
Hire React.js developers like you, then mastering Hooks is not optional
anymore. Hooks are the foundation of every React codebase written after
2019.They replaced class components, simplified state management, and
made code reuse actually practical. This guide covers every major Hook
with real working code, clear explanations, and common mistakes to avoid.


What Are React Hooks?

Hooks are functions that let you use React features like state and
lifecycle inside regular function components. Before Hooks existed,
you had to write class components to do any of that.

Here is the same Counter component, written both ways:

Old way using a Class Component:

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

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={() =>
          this.setState({ count: this.state.count + 1 })
        }>
          Add
        </button>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Modern way using a Hook:

import { useState } from "react";

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Add</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Same result. Much less code. Far easier to read and test.


All the Hooks Covered in This Guide

Hook What It Does
useState Adds state to a component
useEffect Runs side effects after render
useContext Reads shared data without prop drilling
useRef Accesses DOM elements or stores mutable values
useMemo Caches expensive calculations
useCallback Caches a function reference
useReducer Manages complex state with actions
Custom Hooks Reuses logic across multiple components

1. useState

useState gives your component memory. It stores a value and
triggers a re-render whenever that value changes.

Syntax:

const [value, setValue] = useState(initialValue);
Enter fullscreen mode Exit fullscreen mode

Real example with a todo list:

import { useState } from "react";

function TodoApp() {
  const [task, setTask] = useState("");
  const [todos, setTodos] = useState([]);

  function addTodo() {
    if (!task.trim()) return;
    setTodos([...todos, task]);
    setTask("");
  }

  return (
    <div>
      <input
        value={task}
        onChange={(e) => setTask(e.target.value)}
        placeholder="Type a task"
      />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>{todo}</li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Most common mistake: Mutating state directly instead of using the
setter function.

// Wrong
todos.push("new task");

// Right
setTodos([...todos, "new task"]);
Enter fullscreen mode Exit fullscreen mode

2. useEffect

useEffect runs code after the component renders. You use it for API
calls, timers, subscriptions, and updating the document title.

Syntax:

useEffect(() => {
  // side effect code
  return () => {
    // cleanup code (optional)
  };
}, [dependencies]);
Enter fullscreen mode Exit fullscreen mode

Real example fetching users from an API:

import { useState, useEffect } from "react";

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/users")
      .then((res) => res.json())
      .then((data) => {
        setUsers(data);
        setLoading(false);
      });
  }, []);

  if (loading) return <p>Loading...</p>;

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

How the dependency array controls when the effect runs:

useEffect(() => { ... });           // Runs after every render
useEffect(() => { ... }, []);       // Runs once on mount only
useEffect(() => { ... }, [userId]); // Runs when userId changes
Enter fullscreen mode Exit fullscreen mode

Real example with cleanup to prevent memory leaks:

useEffect(() => {
  const timer = setInterval(() => {
    console.log("tick");
  }, 1000);

  return () => clearInterval(timer);
}, []);
Enter fullscreen mode Exit fullscreen mode

Always return a cleanup function when you set up timers,
subscriptions, or event listeners.


3. useContext

Prop drilling means passing data through many layers of components
just to reach the one that actually needs it. useContext solves
this by letting any component read shared data directly.

Step 1: Create the context and a provider:

import { createContext, useContext, useState } from "react";

const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Wrap your app:

// main.jsx
<ThemeProvider>
  <App />
</ThemeProvider>
Enter fullscreen mode Exit fullscreen mode

Step 3: Read the context from any component:

function Navbar() {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <nav style={{ background: theme === "dark" ? "#222" : "#fff" }}>
      <span>My App</span>
      <button onClick={() =>
        setTheme(theme === "dark" ? "light" : "dark")
      }>
        Toggle Theme
      </button>
    </nav>
  );
}
Enter fullscreen mode Exit fullscreen mode

No props passed through intermediate components. The data is
available anywhere inside the provider.


4. useRef

useRef has two distinct uses that are worth understanding separately.

Use 1: Access a DOM element directly

import { useRef, useEffect } from "react";

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

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} placeholder="This auto-focuses" />;
}
Enter fullscreen mode Exit fullscreen mode

Use 2: Store a value that does not trigger a re-render

function StopwatchApp() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);

  function start() {
    intervalRef.current = setInterval(() => {
      setSeconds((s) => s + 1);
    }, 1000);
  }

  function stop() {
    clearInterval(intervalRef.current);
  }

  return (
    <div>
      <p>{seconds}s</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The key difference: changing a useRef value does not cause the
component to re-render. That is what makes it the right tool for
storing timers and interval IDs.


5. useMemo

useMemo caches the result of an expensive calculation so it only
runs again when its dependencies change, not on every render.

import { useState, useMemo } from "react";

function ProductList({ products, filter }) {
  const filteredProducts = useMemo(() => {
    return products.filter((p) =>
      p.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [products, filter]);

  return (
    <ul>
      {filteredProducts.map((p) => (
        <li key={p.id}>{p.name} - {p.price}</li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

The filter calculation only runs when products or filter changes,
not when unrelated state changes cause a re-render.

Do not add useMemo everywhere. Only use it when you have measured
a real performance issue. Adding it unnecessarily makes code harder
to read for no actual benefit.


6. useCallback

useCallback caches a function so that its reference stays the same
between renders. This matters when you pass functions as props to
components wrapped in React.memo.

import { useState, useCallback, memo } from "react";

function Parent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  const handleClick = useCallback(() => {
    console.log("Button clicked");
  }, []);

  return (
    <div>
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ChildButton onClick={handleClick} />
    </div>
  );
}

const ChildButton = memo(({ onClick }) => {
  console.log("Child rendered");
  return <button onClick={onClick}>Click Me</button>;
});
Enter fullscreen mode Exit fullscreen mode

Without useCallback, every re-render of Parent creates a new
function reference. ChildButton would re-render every time even
though nothing about it actually changed.


7. useReducer

When state logic becomes complex, especially when one action needs
to update multiple values at once, useReducer is cleaner than
multiple useState calls.

import { useReducer } from "react";

const initialState = { count: 0, step: 1 };

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + state.step };
    case "DECREMENT":
      return { ...state, count: state.count - state.step };
    case "RESET":
      return initialState;
    case "SET_STEP":
      return { ...state, step: action.payload };
    default:
      return state;
  }
}

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

  return (
    <div>
      <p>Count: {state.count} | Step: {state.step}</p>
      <button onClick={() => dispatch({ type: "DECREMENT" })}>-</button>
      <button onClick={() => dispatch({ type: "INCREMENT" })}>+</button>
      <button onClick={() => dispatch({ type: "RESET" })}>Reset</button>
      <input
        type="number"
        value={state.step}
        onChange={(e) =>
          dispatch({ type: "SET_STEP", payload: Number(e.target.value) })
        }
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The reducer function is a pure function that takes current state and
an action, and returns the next state. This pattern makes complex
state changes predictable and easy to test.


8. Custom Hooks

This is where Hooks become truly powerful. A custom hook is a regular
JavaScript function whose name starts with use and that calls other
hooks inside it. You use custom hooks to extract and reuse logic across
multiple components.

Custom Hook: useFetch

import { useState, useEffect } from "react";

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

  useEffect(() => {
    setLoading(true);
    setError(null);

    fetch(url)
      .then((res) => {
        if (!res.ok) throw new Error("Request failed");
        return res.json();
      })
      .then((json) => {
        setData(json);
        setLoading(false);
      })
      .catch((err) => {
        setError(err.message);
        setLoading(false);
      });
  }, [url]);

  return { data, loading, error };
}
Enter fullscreen mode Exit fullscreen mode

Using the custom hook in multiple components:

function PostList() {
  const { data, loading, error } =
    useFetch("https://jsonplaceholder.typicode.com/posts");

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

  return (
    <ul>
      {data.slice(0, 5).map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

function UserCount() {
  const { data, loading } =
    useFetch("https://jsonplaceholder.typicode.com/users");

  if (loading) return <p>Loading...</p>;
  return <p>Total users: {data?.length}</p>;
}
Enter fullscreen mode Exit fullscreen mode

Two different components. Same fetch logic. Zero code duplication.
This is what makes custom hooks one of the most valuable patterns
in React development.


Rules of Hooks

These two rules are enforced by React and breaking them will cause bugs
that are difficult to trace.
Rule 1: Only call Hooks at the top level of a component.
Never inside if statements, loops, or nested functions.
Rule 2: Only call Hooks inside React function components
or other custom hooks.

Wrong:

function Bad({ isLoggedIn }) {
  if (isLoggedIn) {
    const [data, setData] = useState(null); // breaks Rule 1
  }
}
Enter fullscreen mode Exit fullscreen mode

Right:

function Good({ isLoggedIn }) {
  const [data, setData] = useState(null); // always at the top level

  if (!isLoggedIn) return null;

  return <div>{data}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Quick Reference

useState Store and update values, triggers re-render
useEffect Side effects: API calls, timers, subscriptions
useContext Read shared global data without prop drilling
useRef DOM access or persistent values without re-render
useMemo Cache expensive calculated values
useCallback Cache function references for child components
useReducer Complex state with multiple related values
Custom Hook Extract and reuse any logic across components


Projects to Build and Practice

The fastest way to learn Hooks is to use them in real projects. Here
are good starting points matched to the hooks they teach:

  • Todo App: useState, useReducer
  • Weather App: useEffect, custom useFetch hook
  • Dark Mode Toggle: useContext, useState
  • Search with Filter: useMemo, useState
  • Click-outside Detector: useRef, useEffect
  • Shopping Cart: useReducer, useContext combined

Pick one, build it from scratch, and you will understand Hooks
at a level that reading alone cannot give you.


Final Thoughts

Most developers learn Hooks by memorizing the API. That approach works
until something breaks and you cannot explain why. The better approach
is to understand what each Hook is solving. useState solves the problem
of memory inside a function. useEffect solves the problem of running
code in response to changes. useRef solves the problem of holding a
value without triggering a render.

When you understand the problem each Hook exists to solve, picking the
right one for any situation becomes straightforward.

Start with useState and useEffect. Build something real. Add the other
hooks only when you actually need them.


Found this useful? Drop a reaction and follow for weekly React posts,
project walkthroughs, and practical guides.


Source: dev.to

arrow_back Back to Tutorials