Skip to main content
React Essentials·Lesson 4 of 5

Essential Hooks

You already know useState. React provides several other hooks that solve common problems: running side effects, accessing DOM elements, sharing data across the tree, and extracting reusable logic.

useEffect

useEffect runs a function after the component renders. It's how you perform side effects — data fetching, subscriptions, timers, or manual DOM manipulation.

import { useState, useEffect } from "react";

interface User {
  id: number;
  name: string;
  email: string;
}

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

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

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

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

The dependency array [userId] tells React to re-run the effect only when userId changes. Key rules:

  • No array — runs after every render.
  • Empty array [] — runs once on mount.
  • With dependencies [a, b] — runs when a or b change.

Cleanup

If your effect creates a subscription or timer, return a cleanup function:

import { useState, useEffect } from "react";

function Clock() {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    const interval = setInterval(() => setTime(new Date()), 1000);
    return () => clearInterval(interval); // cleanup on unmount
  }, []);

  return <p>{time.toLocaleTimeString()}</p>;
}

React calls the cleanup before the effect re-runs and when the component unmounts.

useRef

useRef gives you a mutable container that persists across renders without causing re-renders when changed. Its most common use is accessing DOM elements:

import { useRef, useEffect } from "react";

function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null);

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

  return <input ref={inputRef} type="text" placeholder="I focus on mount" />;
}

You can also use useRef to store any mutable value that shouldn't trigger a re-render, such as a previous value or a timer ID:

import { useState, useRef, useEffect } from "react";

function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T | undefined>(undefined);

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
}

function PriceDisplay({ price }: { price: number }) {
  const prevPrice = usePrevious(price);
  const direction =
    prevPrice !== undefined
      ? price > prevPrice
        ? "up"
        : price < prevPrice
        ? "down"
        : "same"
      : "same";

  return (
    <span className={direction= "up" ? "text-green-600" : direction= "down" ? "text-red-600" : ""}>
      ${price.toFixed(2)} {direction === "up" ? "" : direction === "down" ? "" : ""}
    </span>
  );
}

useContext

Context lets you pass data through the component tree without prop drilling. Create a context, wrap a portion of the tree with a provider, and consume it with useContext:

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

interface ThemeContext {
  theme: "light" | "dark";
  toggle: () => void;
}

const ThemeContext = createContext<ThemeContext | null>(null);

function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error("useTheme must be used within a ThemeProvider");
  }
  return context;
}

function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<"light" | "dark">("light");

  function toggle() {
    setTheme((prev) => (prev === "light" ? "dark" : "light"));
  }

  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
}

function ThemeToggle() {
  const { theme, toggle } = useTheme();
  return (
    <button onClick={toggle}>
      Current theme: {theme}. Click to switch.
    </button>
  );
}

// Usage in your app
function App() {
  return (
    <ThemeProvider>
      <ThemeToggle />
    </ThemeProvider>
  );
}

Context is ideal for global concerns like themes, authentication, or locale. Avoid overusing it for every piece of state — it re-renders all consumers when the value changes.

Custom Hooks

A custom hook is a function that starts with use and calls other hooks. It lets you extract and reuse stateful logic:

import { useState, useEffect } from "react";

function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    if (typeof window === "undefined") return initialValue;
    const stored = localStorage.getItem(key);
    return stored ? (JSON.parse(stored) as T) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue] as const;
}

// Usage
function Settings() {
  const [name, setName] = useLocalStorage("username", "");

  return (
    <input
      value={name}
      onChange={(e)=> setName(e.target.value)}
      placeholder="Your name (persisted)"
    />
  );
}

Another practical example — a hook that tracks window dimensions:

import { useState, useEffect } from "react";

function useWindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEffect(() => {
    function handleResize() {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    }

    handleResize(); // set initial size
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return size;
}

function ResponsiveLayout() {
  const { width } = useWindowSize();
  return <p>Window width: {width}px — {width < 768 ? "Mobile" : "Desktop"}</p>;
}

Custom hooks follow the same rules as built-in hooks: call them at the top level, never inside conditions or loops.

Summary

  • useEffect handles side effects and supports cleanup functions.
  • useRef gives you a mutable container that doesn't cause re-renders — useful for DOM access and persistent values.
  • useContext provides tree-wide data sharing without prop drilling.
  • Custom hooks (use* functions) let you extract and share stateful logic across components.