React Hooks: useState and useEffect

React Hooks, introduced in React 16.8, revolutionized how developers write React components. Instead of reaching for class components to manage state or lifecycle events, you can now do everything in clean, concise function components. If you're new to hooks, useState and useEffect are the two you'll use in almost every component you write.

What is useState?

useState lets you add reactive state to a function component. When state changes, React automatically re-renders the component to reflect the new value.

Basic Syntax

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0); // 0 is the initial value

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

useState returns an array with two items: the current state value and a setter function. The naming convention [value, setValue] uses array destructuring.

State with Objects

When your state is an object, always spread the existing state when updating to avoid overwriting other properties:

const [user, setUser] = useState({ name: "", email: "" });

// Correct — merges the update
setUser(prev => ({ ...prev, name: "Alice" }));

// Wrong — wipes out the email field
setUser({ name: "Alice" });

What is useEffect?

useEffect handles side effects — anything that reaches outside the component's render cycle, such as fetching data, subscribing to events, updating the document title, or setting timers.

Basic Syntax

import { useState, useEffect } from "react";

function PageTitle({ title }) {
  useEffect(() => {
    document.title = title; // Side effect: updating the DOM
  }, [title]); // Re-run only when 'title' changes

  return <h1>{title}</h1>;
}

The Dependency Array

The second argument to useEffect controls when it runs:

  • No array: Runs after every render.
  • Empty array []: Runs once after the first render (like componentDidMount).
  • Array with values [a, b]: Runs when any listed value changes.

Fetching Data with useEffect

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

  useEffect(() => {
    let cancelled = false;

    async function fetchUser() {
      const res = await fetch(`/api/users/${userId}`);
      const data = await res.json();
      if (!cancelled) {
        setUser(data);
        setLoading(false);
      }
    }

    fetchUser();

    return () => {
      cancelled = true; // Cleanup: prevent state update if component unmounts
    };
  }, [userId]);

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

Note the cleanup function returned from useEffect. It runs when the component unmounts or before the effect re-runs, preventing memory leaks and stale updates.

Common Mistakes to Avoid

  1. Missing dependencies: If you use a variable inside useEffect, include it in the dependency array or you'll get stale values.
  2. Infinite loops: Setting state inside an effect without a dependency array causes endless re-renders.
  3. Mutating state directly: Always use the setter function — never do count++ directly.

Summary

Together, useState and useEffect cover the majority of what class-based components used to require. Master these two hooks and you'll have a solid foundation for learning more advanced hooks like useContext, useReducer, and useMemo.