Chapter 14 of 14 · browse all
Chapter 14

Refs and Custom Hooks

Reaching the DOM directly, keeping values between renders, and packaging logic for reuse.

useRef

useRef returns an object with a single property, current. That object stays the same across renders, and changing current does not trigger a re-render. Two uses follow from that.

Reaching a DOM element

Pass a ref to the ref attribute and React puts the real DOM node in current after rendering.

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

  function focusInput() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} type="search" />
      <button onClick={focusInput}>Focus the field</button>
    </>
  );
}

This is the right tool for focus, text selection, scrolling, measuring an element, and driving media playback or a canvas — things the DOM does that React has no declarative equivalent for.

function VideoPlayer({ src }) {
  const videoRef = useRef(null);
  const [playing, setPlaying] = useState(false);

  function toggle() {
    if (playing) {
      videoRef.current.pause();
    } else {
      videoRef.current.play();
    }
    setPlaying(!playing);
  }

  return (
    <>
      <video ref={videoRef} src={src} />
      <button onClick={toggle}>{playing ? "Pause" : "Play"}</button>
    </>
  );
}

current is null during the first render, because the element does not exist yet. Read it in an event handler or an effect, never during render.

Holding a value between renders

A ref is also a box for anything that must survive re-renders but should not cause one: a timer id, a previous value, a mutable counter.

function Stopwatch() {
  const [elapsed, setElapsed] = useState(0);
  const intervalRef = useRef(null);

  function start() {
    if (intervalRef.current) return;
    intervalRef.current = setInterval(() => setElapsed(e => e + 1), 1000);
  }

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

  useEffect(() => stop, []);

  return (
    <>
      <p>{elapsed}s</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </>
  );
}

The rule of thumb: if changing a value should update the screen, it is state. If it is bookkeeping the screen does not care about, it is a ref.

Avoid using refs to fight React

Do not add, remove, or restyle DOM nodes that React manages. React will overwrite the change on its next render, or crash trying to update a node that is no longer there. Refs are for reading and for calling browser methods, not for rewriting the page.

Writing a custom hook

A custom hook is a function whose name begins with use and which calls other hooks. It exists to share logic, not state — each component that calls it gets its own independent copy of whatever state is inside.

import { useState, useEffect } from "react";

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

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth);
    }
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return width;
}
function Layout() {
  const width = useWindowWidth();
  return width < 768 ? <MobileNav /> : <DesktopNav />;
}

A hook with arguments and a richer return

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

  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);
    setError(null);

    fetch(url, { signal: controller.signal })
      .then(res => {
        if (!res.ok) throw new Error("Request failed: " + res.status);
        return res.json();
      })
      .then(json => { setData(json); setLoading(false); })
      .catch(err => {
        if (err.name === "AbortError") return;
        setError(err.message);
        setLoading(false);
      });

    return () => controller.abort();
  }, [url]);

  return { data, error, loading };
}
function Posts() {
  const { data, error, loading } = useFetch("/api/posts");

  if (loading) return <p>Loading…</p>;
  if (error) return <p>Could not load posts. {error}</p>;

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

Return an object when there are several values, so callers can destructure the ones they want in any order. Return a single value, or a tuple like useState does, when there are only one or two.

Where to go next

You now have the whole core of React: components, props, state, events, lists, forms, and the hooks that tie them together. The usual next steps are a router for multiple pages, a data-fetching library, and eventually a framework such as Next.js or Remix that bundles routing, data loading, and server rendering together.

The official documentation at react.dev is the best reference once the basics are in place — particularly its pages on preserving and resetting state, and on when an effect is not the answer.