Skip to content
React Hooks Explained — Complete Guide with Examples & Best Practices

React Hooks Explained — Complete Guide with Examples & Best Practices

DodaTech Updated Jun 6, 2026 10 min read

React hooks are functions that let you use state and effects in function components - like a utility belt giving your components memory without writing a class.

What You’ll Learn

  • What hooks are and why they replaced class components
  • The Rules of Hooks (and why they exist)
  • Every built-in hook: useState, useEffect, useContext, useRef, useReducer, useMemo, useCallback
  • How to create reusable custom hooks
  • Real-world patterns used in production apps

Why React Hooks Matters

Hooks are how modern React apps are built — every component you write will likely use at least one. At DodaTech, hooks power the Durga Antivirus Pro dashboard (real-time threat updates with useEffect, scan queue state with useReducer) and the Doda Browser extension (tab management state, keyboard shortcut hooks). Without hooks, you’d write more code, repeat yourself, and deal with confusing this binding in class components.

Your Learning Path

    flowchart LR
  A[React Basics<br/>Components & JSX] --> B[React Hooks]
  B --> C[React Advanced<br/>Patterns]
  B --> D[React Reference]
  B --> E[Next.js]
  style B fill:#3b82f6,color:#fff,stroke:#2563eb,stroke-width:3px
  
Prerequisites: You need a solid understanding of React components, JavaScript ES6+ (arrow functions, destructuring, spread operator), and basic Node.js / npm. Complete https://tutorials.dodatech.com/frameworks/react/react-basics/ first.

What Are Hooks?

Before React 16.8 (2019), state and lifecycle features were only available in class components:

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  render() {
    return <p>{this.state.count}</p>;
  }
}

This was verbose and confusing (this, super, constructor). Hooks solve this by letting you “hook into” React features from function components — no classes needed.

Analogy: Think of a component as a kitchen appliance. Without hooks, each appliance had its own unique control panel (class lifecycle methods). With hooks, every appliance gets a standard utility belt — you just pick the tools you need.

Rules of Hooks

  1. Call hooks at the top level — not inside loops, conditions, or nested functions
  2. Call hooks from React function components or custom hooks only
  3. Consistent order — the order of hook calls must be the same between renders
function MyComponent() {
  const [count, setCount] = useState(0);        // ✅ Top level
  useEffect(() => { document.title = `${count}`; }, [count]);
}

function Bad({ feature }) {
  if (feature) {
    const [data, setData] = useState(null);     // ❌ Inside condition
  }
}

Why this rule? React relies on the order of hooks to associate each call with its internal state. If you conditionally skip a hook, the order shifts and React returns the wrong state.

useState — Local Component Memory

The simplest hook. It lets your component remember a value between renders:

import { useState } from "react";

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

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

Line by line:

  • const [count, setCount] = useState(0);useState(0) returns [0, setterFunction]. We destructure them into count and setCount.
  • setCount(prev => prev + 1) — The functional form passes the previous state as an argument. This guarantees you update from the latest value even with multiple rapid updates.
  • Every setCount call triggers a re-render: React calls the component again, and useState returns the current stored value.

State with Objects

function UserProfile() {
  const [user, setUser] = useState({ name: "", email: "" });
  const updateName = (name) => {
    setUser(prev => ({ ...prev, name }));  // Merge with spread
  };
}

State updates replace, they don’t merge. Use the spread operator (...prev) to copy existing properties before overriding.

useEffect — Side Effect Manager

useEffect lets you perform side effects — anything that interacts with the outside world: API calls, timers, event listeners, browser storage.

import { useState, useEffect } from "react";

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

  useEffect(() => {
    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => { setUser(data); setLoading(false); });
  }, [userId]);

  if (loading) return <p>Loading...</p>;
  return <h2>{user.name}</h2>;
}

Line by line:

  • useEffect(() => { ... }, [userId]); — First argument is the effect function. The dependency array [userId] tells React: “Only re-run when userId changes.”
  • Inside: fetch data, parse JSON, call setUser / setLoading
  • On first render, the effect runs. If userId changes, it runs again. Otherwise, it never re-runs — preventing unnecessary API calls.

Effect Cleanup

Some effects need cleanup — removing event listeners, clearing timers:

useEffect(() => {
  const handler = (e) => setPosition({ x: e.clientX, y: e.clientY });
  window.addEventListener("mousemove", handler);
  return () => window.removeEventListener("mousemove", handler);
}, []);

Why cleanup matters: Without it, every time the effect runs (or the component unmounts), a new listener is added but the old one is never removed — causing memory leaks.

Dependency Array Patterns

ArrayWhen it runsUse case
[] (empty)On mount only, cleanup on unmountEvent listeners, analytics
[dep]On mount + when dep changesFetching data based on a prop
omittedOn every renderRarely useful; usually a mistake

useContext — Avoid Prop Drilling

Prop drilling is passing a prop through multiple component levels just so a deeply nested child can use it. Context solves this by making a value available to any component in the tree.

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

const ThemeContext = createContext("light");

function App() {
  const [theme, setTheme] = useState("light");
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  const { theme, setTheme } = useContext(ThemeContext);
  return <p>Current theme: {theme}</p>;
}

Step by step:

  1. createContext("light") — Creates a context with default value "light"
  2. <ThemeContext.Provider value={...}> — Wraps the tree and provides the value
  3. useContext(ThemeContext) — Any descendant reads the context value
  4. When setTheme is called in the provider, all consumers re-render

useRef — Mutable Values Without Re-renders

useRef gives you a mutable object with a .current property that persists across renders. Unlike state, changing a ref does not trigger a re-render.

function TextInput() {
  const inputRef = useRef(null);
  const focusInput = () => inputRef.current.focus();
  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus Input</button>
    </>
  );
}

useRef vs useState

FeatureuseRefuseState
Triggers re-render?NoYes
Persists across renders?YesYes
Use caseDOM access, intervals, mutable valuesData that affects the UI

useReducer — Complex State Logic

When state logic is complex (multiple sub-values, interdependent updates), useReducer centralizes it:

function reducer(state, action) {
  switch (action.type) {
    case "increment": return { count: state.count + 1 };
    case "decrement": return { count: state.count - 1 };
    case "reset": return { count: action.payload ?? 0 };
    default: return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "reset", payload: 100 })}>Reset</button>
    </>
  );
}

Line by line:

  • function reducer(state, action) — A pure function: takes current state and an action, returns the next state. No side effects.
  • dispatch({ type: "increment" }) — Sends a message to the reducer. Convention: object with type and optional payload.
  • useReducer(reducer, { count: 0 }) — Takes the reducer and initial state, returns [state, dispatch].

useState vs useReducer

FeatureuseStateuseReducer
Best forSimple values (numbers, strings)Complex objects, multiple sub-values
Update logicInline in event handlersCentralized in reducer function
TestingHarder to test inline logicReducer is a pure function — easy to test

useMemo & useCallback — Performance Optimizations

useMemo caches a computed value. useCallback caches a function reference. Both only recompute when dependencies change.

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

function ExpensiveList({ items, filter }) {
  const filteredItems = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);

  const handleDelete = useCallback((id) => {
    console.log(`Delete ${id}`);
  }, []);

  return <ul>{filteredItems.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}

useMemo vs useCallback

FeatureuseMemouseCallback
ReturnsCached valueCached function
SyntaxuseMemo(() => value, deps)useCallback(fn, deps)
EquivalentuseMemo(() => fn, deps)
Use whenExpensive computationPassing stable fn to memoized children

When to use them: Only when you have a measurable performance problem. They add overhead (comparison logic, memory) and make code harder to read.

Custom Hooks — Your Own Utility Functions

A custom hook is a JavaScript function that starts with use and calls other hooks. It extracts reusable logic into a single function.

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

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetch(url)
      .then(res => res.json())
      .then(data => { if (!cancelled) { setData(data); setLoading(false); }})
      .catch(err => { if (!cancelled) { setError(err); setLoading(false); }});
    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

function MyComponent() {
  const { data, loading } = useFetch("https://api.example.com/users");
  if (loading) return <p>Loading...</p>;
  return <p>Users: {data?.length}</p>;
}

Key insight: cancelled = true inside the cleanup function prevents setting state on an unmounted component — fixing the “Can’t perform a React state update on an unmounted component” warning.

Common Mistakes

1. Missing dependencies in useEffect

useEffect(() => { fetch(`/api/users/${userId}`); }, []); // Missing userId

Fix: Include all reactive values. Use the exhaustive-deps ESLint rule to catch this.

2. Creating infinite loops

useEffect(() => { setCount(count + 1); });  // No dep array = loop

Fix: Always specify a dependency array.

3. Stale closures in useCallback

const logCount = useCallback(() => { console.log(count); }, []);

logCount always logs the initial count because the function captured count at creation time. Fix: Add count to the dependency array.

4. Forgetting cleanup in useEffect

Subscriptions, timers, and event listeners left uncleaned cause memory leaks. Always return a cleanup function for persistent listeners.

5. Premature optimization with useMemo/useCallback

Only wrap when you have a measurable performance problem. They add overhead.

6. Calling hooks conditionally

if (user) { const [name, setName] = useState(""); } // Order shifts!

7. Stale state in setInterval

useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000); // Always 0
}, []);

Fix: Use setCount(prev => prev + 1) (functional form).

Practice Questions

  1. Why were hooks introduced in React 16.8?

    To solve problems with class components: hard-to-reuse logic, confusing this binding, and complex lifecycle methods. Hooks let function components use state and effects without classes.

  2. What happens if you call a hook inside a condition?

    React throws an error because it relies on hook call order to associate state with component instances.

  3. What is the difference between useEffect and useLayoutEffect?

    useEffect fires asynchronously after the browser paints. useLayoutEffect fires synchronously before paint. Use useLayoutEffect only when you need to measure layout.

  4. When should you use useReducer over useState?

    When state logic is complex (multiple sub-values, interdependent updates) or when you want a testable reducer function.

  5. What is a stale closure?

    A callback capturing an outdated variable value. Avoid it by using the functional form of state setters or including all dependencies in the array.

Challenge

Create a useLocalStorage(key, initialValue) custom hook that persists state in localStorage and syncs across browser tabs using the storage event.

FAQ

What is the difference between useEffect and useLayoutEffect?
useEffect runs after paint (async, non-blocking). useLayoutEffect runs before paint (synchronous). Use useLayoutEffect only for DOM measurements.
When should I use useReducer instead of useState?
When state has multiple sub-values, updates are interdependent, or you want to centralize update logic in a testable reducer function.
What is a custom hook?
A JavaScript function starting with use that calls other hooks. It extracts reusable component logic into a single, shareable function.
Do I need useMemo for all computed values?
No. Only use it for expensive computations or values passed as props to memoized children.
Why is my useEffect running twice in development?
React Strict Mode double-invokes effects in development to surface bugs. Effects only run once in production.
What is the difference between useRef and useState?
Changing useRef.current does not cause re-renders. Changing state with useState does. Use refs for values that don’t affect UI.

Try It Yourself

Edit this complete React hooks sandbox — see useState, useEffect, and custom hooks in action:

▶ Try It Yourself Edit the code and click Run

What’s Next

Now that you’ve mastered hooks, dive into advanced patterns:

LessonDescription
https://tutorials.dodatech.com/frameworks/react/react-advanced/Error boundaries, portals, code splitting
https://tutorials.dodatech.com/frameworks/react/reference/Complete cheatsheet for all hooks and APIs
TypeScript for ReactType-safe hooks and components
Node.js Backend for ReactBuild APIs your React frontend consumes

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

What’s Next

Congratulations on completing this React Hooks tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro