Lesson 11 of 12

React useRef and Custom Hooks

State and effects cover most of what components need. This lesson adds two more tools. React useRef gives a component a place to keep a value that should survive re-renders but should not cause one, and it lets you reach real DOM elements. Custom hooks let you package stateful logic into a function of your own and reuse it across components.

What React useRef gives you

Calling useRef(initialValue) returns a plain object with one property, current. React gives you the same object on every render, so whatever you store in ref.current is still there next time. Unlike state, changing ref.current does not make React render again. That makes refs right for information the screen does not show, such as a timer id.

State (useState)Ref (useRef)
Survives re-rendersYesYes
Changing it re-rendersYesNo
How you change itCall the setterAssign to ref.current
Read during renderingYesNo – only in handlers and effects
Typical useAnything shown on screenDOM elements, timer ids, previous values

Refs to DOM elements

React manages the DOM for you, but sometimes you need the real element – to move focus, scroll to it, or measure it. Create a ref with useRef(null) and pass it to a JSX element with the ref attribute. After React creates the element, it puts the DOM node in ref.current, and you can call browser methods on it in an event handler.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Focusing an input with a ref</title>
  <script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
    input { padding: 6px; width: 220px; margin-right: 6px; }
  </style>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel" data-type="module">
import React, { useRef, useState } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";

function App() {
  const inputRef = useRef(null);
  const [cities, setCities] = useState([]);

  function handleAdd() {
    const value = inputRef.current.value.trim();
    if (value) {
      setCities([...cities, { id: cities.length + 1, name: value }]);
      inputRef.current.value = "";
    }
    inputRef.current.focus();   // keep typing without clicking the field
  }

  return (
    <div>
      <label htmlFor="city">City to visit</label><br />
      <input id="city" ref={inputRef} defaultValue="Hampi" />
      <button type="button" onClick={handleAdd}>Add</button>
      <ul>
        {cities.map(c => <li key={c.id}>{c.name}</li>)}
      </ul>
    </div>
  );
}

createRoot(document.getElementById("root")).render(<App />);
  </script>
</body>
</html>

Here the input is uncontrolled (it uses defaultValue), and the ref reads it only when the button is clicked. Moving focus back into the field after an action is a small detail that keyboard users appreciate.

Tip: New in React 19: a function component can receive ref as a normal prop and pass it on, e.g. function TextField({ ref, ...rest }) { return <input ref={ref} {...rest} />; }. Older code wraps such components in forwardRef, which is no longer needed.

Refs to remember values

A stopwatch needs the id returned by setInterval so that the Stop button can clear it. The id is never shown, so storing it in state would only cause pointless renders, and a normal variable would be forgotten on the next render. A ref is the right home for it.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>A stopwatch that keeps its interval id in a ref</title>
  <script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
    .time { font-size: 32px; font-variant-numeric: tabular-nums; margin: 8px 0; }
    button { margin-right: 6px; }
  </style>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel" data-type="module">
import React, { useRef, useState, useEffect } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";

function App() {
  const [ms, setMs] = useState(0);
  const [running, setRunning] = useState(false);
  const intervalRef = useRef(null);

  function start() {
    if (running) return;
    setRunning(true);
    intervalRef.current = setInterval(() => setMs(t => t + 100), 100);
  }

  function stop() {
    clearInterval(intervalRef.current);
    setRunning(false);
  }

  function reset() {
    stop();
    setMs(0);
  }

  // stop the timer if the component is removed
  useEffect(() => () => clearInterval(intervalRef.current), []);

  return (
    <div>
      <p className="time">{(ms / 1000).toFixed(1)} s</p>
      <button type="button" onClick={start} disabled={running}>Start</button>
      <button type="button" onClick={stop} disabled={!running}>Stop</button>
      <button type="button" onClick={reset}>Reset</button>
    </div>
  );
}

createRoot(document.getElementById("root")).render(<App />);
  </script>
</body>
</html>

Watch out: Do not read or write ref.current while rendering (in the body of the component that builds the JSX). React does not know when it changes, so the screen can show out-of-date values. Use refs inside event handlers and effects; if something must appear on screen, it belongs in state.

Writing a custom hook

Imagine two components that both need the current window width. You could copy the same useState and useEffect code into both, but then you must fix bugs twice. Instead, move the code into a function whose name starts with use. That function is a custom hook. It can call other hooks, and it returns whatever the component needs.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>A useWindowWidth custom hook</title>
  <script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
    .box { padding: 10px; border-radius: 8px; background: #eef6fb; margin-top: 8px; }
  </style>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel" data-type="module">
import React, { useState, useEffect } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";

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 LayoutInfo() {
  const width = useWindowWidth();
  return <p>Width: <strong>{width}px</strong></p>;
}

function MenuHint() {
  const width = useWindowWidth();
  return (
    <p className="box">
      {width < 600 ? "Small screen: show a menu button" : "Wide screen: show the full menu"}
    </p>
  );
}

function App() {
  return (
    <div>
      <LayoutInfo />
      <MenuHint />
      <p>Resize the window (or the editor panel) to see both update.</p>
    </div>
  );
}

createRoot(document.getElementById("root")).render(<App />);
  </script>
</body>
</html>

Rules for custom hooks

  • The name must start with use followed by a capital letter, such as useWindowWidth or useOnlineStatus. This tells React and the linter that the rules of hooks apply.
  • Call it only at the top level of a component or another hook – the same rules as for useState.
  • Custom hooks share logic, not state. Each component that calls useWindowWidth() gets its own independent state.
  • Only make a function a hook if it calls other hooks. A plain helper such as formatRupees() should stay a normal function.

Good candidates for custom hooks are pieces of logic you find yourself repeating: loading data from a URL, tracking whether the browser is online, toggling a boolean, or debouncing a search input. You have now seen every core hook a beginner needs; the last lesson, React Project with Vite, moves all of this into a real project.

Further reading

Key takeaways

  • useRef(initial) returns an object whose current value survives re-renders.
  • Changing ref.current does not cause a re-render; changing state does.
  • Pass a ref to a JSX element (ref={inputRef}) to reach the DOM node, e.g. to call focus().
  • Use refs in event handlers and effects, not during rendering.
  • In React 19, ref can be passed as a normal prop; forwardRef is no longer needed.
  • A custom hook is a function starting with use that calls other hooks to share logic.

Quick quiz

Check what you learned – pick an answer.

1. What happens when you change myRef.current?

2. Which is a valid name for a custom hook?

3. Two components call the same custom hook useCounter(). What about their state?

Practice exercise

Write a custom hook useToggle(initial) that returns [value, toggle], where toggle flips the boolean. Use it in two components: one that shows or hides a FAQ answer, and one that switches a "Notifications: On/Off" label.

Show a hint

Inside the hook, call useState(initial) and define const toggle = () => setValue(v => !v).

Show the solution
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>useToggle hook</title>
  <script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
    button { margin-bottom: 8px; }
  </style>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel" data-type="module">
import React, { useState } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";

function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = () => setValue(v => !v);
  return [value, toggle];
}

function Faq() {
  const [open, toggleOpen] = useToggle(false);
  return (
    <div>
      <button type="button" aria-expanded={open} onClick={toggleOpen}>Is this course free?</button>
      {open && <p>Yes, all lessons on this site are free to read.</p>}
    </div>
  );
}

function Notifications() {
  const [on, toggleOn] = useToggle(true);
  return (
    <button type="button" onClick={toggleOn}>
      Notifications: {on ? "On" : "Off"}
    </button>
  );
}

function App() {
  return (
    <div>
      <Faq />
      <Notifications />
    </div>
  );
}

createRoot(document.getElementById("root")).render(<App />);
  </script>
</body>
</html>

Progress is saved in this browser only.