DEV Community

Hector Williams
Hector Williams

Posted on

A Beginner’s Guide to React Hooks

Introduction
React has two kind of components: Class and Functional. Class components allow us to mimic classes in object oriented programming. Functional components allow us to mimic functional programming. React Hooks were introduced in version 16.8 to simplify how developers manage state, side effects, and other component features — all within functional components. The name hooks is appropiate because they allow functional components to hook into state and lifecycle methods.

Before hooks, we used class components to handle state and lifecycle methods. But now, with hooks, we can write cleaner, more concise, and reusable code. As a result, we no longer need to use class components even though there are no plans to replace them.

In this post, you’ll learn:

-What are React Hooks

-The Most Common Hooks (with examples)

-Common Pitfalls Beginners should avoid

-Why Hooks Are Game-Changing

What Are React Hooks?
Hooks are functions that let you “hook into” React features from functional components. They eliminate the need for most class-based components, enabling state and lifecycle logic in function-based components.You can think of hooks as giving superpowers to your functional components.

The Most Common Hooks (with Examples)

🔹 useState – Manage Local Component State

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

-useState returns a state variable and a function to update it.

-You can have multiple useState calls for different pieces of state.

🔹useEffect – Perform Side Effects (e.g., API calls)

import { useEffect, useState } from "react";

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

  useEffect(() => {
    fetch("https://api.example.com/users")
      .then((res) => res.json())
      .then((data) => setUsers(data));
  }, []); // empty array = run once on mount

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

-useEffect replaces componentDidMount, componentDidUpdate, and componentWillUnmount.

-Cleanup functions let you clear timeouts or remove listeners.

🔹 useContext – Share Data Across Components
Instead of prop drilling, use useContext with the Context API to share state globally.

const ThemeContext = React.createContext();

function ThemedComponent() {
  const theme = useContext(ThemeContext);
  return <div style={{ background: theme.background }}>Hello!</div>;
}
Enter fullscreen mode Exit fullscreen mode

🔹useRef – Keep a Persistent Reference
useRef gives you a mutable reference that doesn’t cause re-renders when updated. It’s great for:

Accessing DOM elements

Storing timers, IDs, or previous values

Example: Focus an input on mount

import { useRef, useEffect } from "react";

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

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

  return <input ref={inputRef} />;
}
Enter fullscreen mode Exit fullscreen mode

You can also use useRef to store values that survive re-renders without triggering updates.

🔹 useReducer – Manage Complex State Logic
useReducer is an alternative to useState, useful when:

State logic is complex (e.g., involves many sub-values)

You want better control over state transitions

Example: Toggle a boolean

import { useReducer } from "react";

function reducer(state, action) {
  switch (action.type) {
    case "toggle":
      return { on: !state.on };
    default:
      return state;
  }
}

function ToggleButton() {
  const [state, dispatch] = useReducer(reducer, { on: false });

  return (
    <button onClick={() => dispatch({ type: "toggle" })}>
      {state.on ? "ON" : "OFF"}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Think of it like a mini Redux inside your component.

🔹 useCallback – Memoize Functions
useCallback returns a memoized version of a function, so it doesn't get recreated on every render. It’s useful when:

You pass functions to child components

You want to avoid unnecessary re-renders

Example:

import { useState, useCallback } from "react";

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

  const increment = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []);

  return <button onClick={increment}>Increment</button>;
}
Enter fullscreen mode Exit fullscreen mode

This ensures increment stays the same between renders (unless dependencies change).

🔹 useMemo – Memoize Expensive Calculations
useMemo returns a cached value of a computation, avoiding recalculating it unless its dependencies change.

Example:

import { useMemo, useState } from "react";

function ExpensiveComponent({ number }) {
  const expensiveResult = useMemo(() => {
    let result = 0;
    for (let i = 0; i < 100000000; i++) {
      result += number;
    }
    return result;
  }, [number]);

  return <div>Result: {expensiveResult}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Use it sparingly — only when a calculation really is performance-heavy.

🔹 Custom Hooks – Reuse Logic Across Components
Custom hooks let you extract and reuse logic in a clean, modular way.

A custom hook is just a regular function that uses hooks and starts with use.

Example: useWindowWidth hook

import { useState, useEffect } from "react";

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", handleResize);

    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return width;
}
Enter fullscreen mode Exit fullscreen mode

Now use it in any component:

function ResponsiveComponent() {
  const width = useWindowWidth();
  return <p>Window width: {width}px</p>;
}
Custom hooks help you avoid repetition and keep your components focused.
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls Beginners Should Avoid

Understanding how to use hooks is great — but knowing what not to do can save you hours of debugging. Here are some of the most common mistakes new React developers make when working with hooks:

🚫 1. Using Hooks Inside Loops, Conditions, or Nested Functions
Why this is a problem:
React relies on the order of hooks to match between renders. If you put a hook inside a loop or a condition, the number and order of hooks can change — causing unpredictable behavior or runtime errors.

✅ Always call hooks at the top level of your component.

❌ Incorrect:

function MyComponent() {
  if (someCondition) {
    const [count, setCount] = useState(0); // ❌ Don’t do this!
  }
}

Enter fullscreen mode Exit fullscreen mode

✅ Correct:

function MyComponent() {
  const [count, setCount] = useState(0); // ✅ Always at the top

  if (someCondition) {
    // use count safely inside logic
  }
}
Enter fullscreen mode Exit fullscreen mode

🔁 2. Missing or Misusing the useEffect Dependency Array
Why it matters:
The dependency array tells React when to run the effect. If you forget it, the effect runs after every render — which could cause infinite loops or performance issues.

✅ Correct:

useEffect(() => {
  fetchData();
}, []); // only runs once on mount

Enter fullscreen mode Exit fullscreen mode

❌ Incorrect (runs every render):

useEffect(() => {
  fetchData(); // ❌ this runs every render!
});

Enter fullscreen mode Exit fullscreen mode

❌ Incorrect (missing required dependency):

useEffect(() => {
  console.log(value); // ❌ `value` should be in the dependency array
}, []);

Enter fullscreen mode Exit fullscreen mode

♻️ 3. Updating State Inside useEffect Without a Condition
If your useEffect sets state without proper guards, it can lead to an infinite loop — because state changes cause re-renders, which re-trigger the effect.

❌ Bad Example:

useEffect(() => {
  setCount(count + 1); // ❌ infinite loop
}, [count]); // this dependency causes re-run every time count updates
Enter fullscreen mode Exit fullscreen mode

✅ Use a guard or control logic:

useEffect(() => {
  if (count === 0) {
    setCount(1);
  }
}, [count]);
Enter fullscreen mode Exit fullscreen mode

🪞 4. Confusing Initial Render Behavior
Some hooks like useEffect(() => {}, []) run after the first render — not before. If you're trying to calculate layout dimensions or DOM elements, consider useLayoutEffect instead.

✅ Use useEffect for:

API calls

Setting document title

Event listeners (with cleanup)

✅ Use useLayoutEffect for:

Reading layout or DOM before paint

📦 5. Thinking useState Updates Instantly
Beginners often expect setState from useState to update immediately — but it doesn’t. It schedules the update for the next render cycle.

❌ Wrong assumption:

setCount(count + 1);
console.log(count); // still prints old value
Enter fullscreen mode Exit fullscreen mode

✅ Proper approach:

setCount(prev => prev + 1);
Enter fullscreen mode Exit fullscreen mode

Or use useEffect to respond to changes:

useEffect(() => {
  console.log("Count changed:", count);
}, [count]);
Enter fullscreen mode Exit fullscreen mode

👻 6. Overusing Hooks for Everything
Hooks are powerful — but not every bit of logic needs a useEffect or a useState. For simple values or computed results, just use variables.

❌ Overkill:

const [message, setMessage] = useState("Hello");
Enter fullscreen mode Exit fullscreen mode

✅ Better:

const message = "Hello";
💡 7. Not Cleaning Up in useEffect
When you use timers, subscriptions, or event listeners inside useEffect, you must clean them up to avoid memory leaks or buggy behavior.
Enter fullscreen mode Exit fullscreen mode

✅ Example with cleanup:

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

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

🧭 Final Advice
📌 Rule of thumb:
Always use hooks at the top of your component
Understand the lifecycle and effects
Lean on the React DevTools and console for debugging
When in doubt, isolate logic into custom hooks

Why Hooks Are Game-Changing
React Hooks didn’t just replace class components — they revolutionized the way we build components by making code simpler, cleaner, and more powerful. Here’s why Hooks matter so much in modern React development:

🔄 1. Simplified Component Logic
Hooks let you manage state, side effects, context, and more — all within functional components.

Before hooks, you had to write class components like this:

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

  componentDidMount() {
    // Side effect
  }

  render() {
    return <button>{this.state.count}</button>;
  }
}
Enter fullscreen mode Exit fullscreen mode

With hooks, you achieve the same functionality using just a few lines of code:

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

  return <button>{count}</button>;
}
Enter fullscreen mode Exit fullscreen mode

✅ Less boilerplate
✅ More readable and expressive
✅ Easier to debug and test

🔁 2. No More Confusing this Keyword
In class components, managing this could be frustrating, especially when binding event handlers.

this.handleClick = this.handleClick.bind(this); // 😩
Enter fullscreen mode Exit fullscreen mode

Hooks remove that pain. With functional components, this is never used — your logic lives in functions and closures, not bound methods.

✅ Cleaner mental model
✅ Fewer bugs from improper binding

🧩 3. Easier Code Reuse with Custom Hooks
With classes, sharing logic meant:

Lifting state up

Using Higher-Order Components (HOCs)

Or render props (which could get messy)

Hooks introduced custom hooks, which let you reuse stateful logic across components in a much simpler way:

function useAuth() {
  const [user, setUser] = useState(null);
  // logic here
  return user;
}
Enter fullscreen mode Exit fullscreen mode

✅ Reusable
✅ Easy to test
✅ Encourages cleaner, modular architecture

🚀 4. Better Separation of Concerns
Hooks let you split related logic into cohesive blocks, not scattered across lifecycle methods.

In class components:

componentDidMount() {
  addEventListener();
  fetchData();
}

componentWillUnmount() {
  removeEventListener();
}
Enter fullscreen mode Exit fullscreen mode

In a function component with hooks:

useEffect(() => {
  addEventListener();
  fetchData();
  return () => removeEventListener();
}, []);
Enter fullscreen mode Exit fullscreen mode

✅ Each effect is focused
✅ Related logic stays together
✅ Easier to understand at a glance

⚡ 5. Improved Performance Tools
Hooks like useMemo, useCallback, and useReducer let you fine-tune performance without resorting to class methods like shouldComponentUpdate.

Example:

const memoizedValue = useMemo(() => expensiveCalc(x), [x]);
✅ Reduces unnecessary renders
✅ Gives you more control over performance-sensitive apps
Enter fullscreen mode Exit fullscreen mode

🧠 6. Functional Programming Paradigm
Hooks embrace the functional programming style:

Pure functions

Declarative UI logic

No side effects unless explicitly defined

This leads to:

Predictable behavior

Less hidden state

Code that’s easier to reason about

🛠️ 7. Hooks Are the Future of React
React’s ecosystem, tools, libraries, and even documentation now assume that you’re using hooks. New developers are encouraged to skip class components entirely and focus on hooks.

Examples:

React Query

Redux Toolkit

Next.js

TanStack Router
All use and support hooks natively.

Hooks offer a simpler, cleaner, and more powerful way to build components — and they’ve become the preferred standard in the React ecosystem.

Conclusion
React Hooks have completely transformed the way we write components — making our code cleaner, more modular, and easier to understand. Whether you're managing state with useState, handling side effects with useEffect, or sharing logic through custom hooks, Hooks give you the power and flexibility to build better React applications using functional components.

If you're just starting out, focus on mastering the basics: useState, useEffect, and useRef. As your projects grow, tools like useReducer, useCallback, and custom hooks will help you scale your code in a clean, reusable way.

🚀 Hooks aren’t just a new syntax — they’re a better way of thinking in React.

Now go ahead — refactor a class component, build a small app, or just play around with the hooks in a CodeSandbox. The more you use them, the more natural they'll feel.

🔗 Want to Learn More?
React Docs – Introducing Hooks
React Hooks at a Glance
Kent C. Dodds – useEffect Deep Dive

Top comments (0)