Week 10

Refs

Custom hooks

React 19

React Server Components

Practice

Assignment

Front end Track

What are refs?

React's job is to keep your UI in sync with your data. You describe what the UI should look like, React figures out what to create, update, or remove in the DOM, and you never have to touch the DOM directly. Most of the time, this is exactly what you want.

But occasionally you need to reach into the DOM directly: to focus an input after a modal opens, to measure an element's dimensions, to trigger a video to play, or to integrate with a third-party library that expects a real DOM node. React doesn't have a declarative API for any of these things, because they're inherently imperative. That's where refs come in.

https://www.youtube.com/watch?v=AltU-XcGvuo

What is a Ref?

A ref is a plain JavaScript object with a single .current property. You create one with useRef, attach it to a JSX element using the ref attribute, and React sets .current to the underlying DOM node after the component mounts. When the component unmounts, React sets it back to null.

import { useRef } from 'react';

function SearchBar() {
  // Create the ref — starts as null before the component mounts
  const inputRef = useRef<HTMLInputElement>(null);

  return <input ref={inputRef} type="search" placeholder="Search..." />;
}

After this component mounts, inputRef.current is the actual <input> DOM element — the same object you'd get from document.querySelector('input'). You can call any DOM method on it directly.

Refs Don't Trigger Re-renders

This is the key difference between refs and state. When you update ref.current, nothing re-renders. When you call setState, the component re-renders. This makes refs the right tool for two things:

  1. Accessing DOM nodes directly (focus, scroll, measure)
  2. Storing values that need to persist across renders but don't affect what's displayed

If a value needs to be shown in the UI, it belongs in state. If it's just something your code needs to remember — an interval ID, a previous value, a flag — a ref is the right choice.

<aside> 💡

Info

You've been using state since week 5. The mental model for refs is simpler: think of useRef as a box you can put anything in. The contents of the box don't affect rendering, and the box itself persists for the entire lifetime of the component.

</aside>

Focusing Elements

The most common use of refs is focus management. Here's a simple example — clicking a button focuses a text input:

import { useRef } from 'react';

function CommentBox() {
  const textareaRef = useRef<HTMLTextAreaElement>(null);

  const handleClick = () => {
    // .current is the DOM element — call focus() directly on it
    textareaRef.current?.focus();
  };

  return (
    <div>
      <textarea ref={textareaRef} placeholder="Write a comment..." />
      <button onClick={handleClick}>Write a comment</button>
    </div>
  );
}

The ?. before .focus() is optional chaining: it handles the case where textareaRef.current is null, which can happen if the component hasn't mounted yet or has already unmounted.

Focus Management and Accessibility

Focus management isn't just a convenience feature — it's an accessibility requirement. When a modal opens, focus should move inside it so keyboard users don't have to Tab through the entire page to get there. When it closes, focus should return to the element that triggered it, so users don't lose their place.

import { useRef, useState } from 'react';

function DeleteAccountDialog() {
  const [isOpen, setIsOpen] = useState(false);
  // Keep a ref to the button that opens the dialog
  const triggerRef = useRef<HTMLButtonElement>(null);
  // Keep a ref to the first focusable element inside the dialog
  const confirmRef = useRef<HTMLButtonElement>(null);

  const open = () => {
    setIsOpen(true);
    // Move focus inside the dialog after it renders
    // setTimeout gives React time to render the dialog first
    setTimeout(() => confirmRef.current?.focus(), 0);
  };

  const close = () => {
    setIsOpen(false);
    // Return focus to the trigger when the dialog closes
    triggerRef.current?.focus();
  };

  return (
    <>
      <button ref={triggerRef} onClick={open}>
        Delete account
      </button>

      {isOpen && (
        <div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
          <h2 id="dialog-title">Are you sure?</h2>
          <p>This cannot be undone.</p>
          <button ref={confirmRef} onClick={close}>
            Yes, delete my account
          </button>
          <button onClick={close}>Cancel</button>
        </div>
      )}
    </>
  );
}

Without the focus management here, a keyboard user who clicks "Delete account" would find their focus disappearing into the void when the dialog opens, and would have no obvious way to interact with the dialog or dismiss it.

<aside> ⚠️

Warning

Don't use refs to read or write values that React should manage. If you find yourself doing inputRef.current.value = '' to clear a form field, that's a sign you should be using a controlled input with state instead. Refs are for interacting with the DOM when React's declarative model can't reach — not for working around it.

</aside>

<aside> ⚠️

Hands On

Build a search page with a text input and a "Search" button. When the page first loads, the input should automatically be focused so the user can start typing immediately. Use useRef and useEffect with an empty dependency array to focus the input on mount. Then extend it: add a "Clear" button that clears the input value (using state) and returns focus to the input.

</aside>

Storing Values Between Renders

Refs aren't only for DOM nodes. You can store any value in a ref and unlike state, changing it won't cause a re-render.

A practical example is storing a timer ID so you can clear it later. If you used state for this, stopping the timer would cause a re-render, which is unnecessary:

import { useRef, useState } from 'react';

function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  // Store the interval ID — we need it to clear the interval, but
  // changing it doesn't need to trigger a re-render
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);

  const start = () => {
    if (isRunning) return;
    setIsRunning(true);
    intervalRef.current = setInterval(() => {
      setSeconds((s) => s + 1);
    }, 1000);
  };

  const stop = () => {
    if (intervalRef.current !== null) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
    setIsRunning(false);
  };

  const reset = () => {
    stop();
    setSeconds(0);
  };

  return (
    <div>
      <p>{seconds}s</p>
      <button onClick={start} disabled={isRunning}>Start</button>
      <button onClick={stop} disabled={!isRunning}>Stop</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

The displayed time (seconds) is state because it needs to update the UI. The interval ID is a ref because it's just an internal implementation detail: nothing in the UI depends on it.

Scrolling and Measuring

Refs also give you access to dimensions and scroll position, things React has no declarative equivalent for.

import { useRef } from 'react';

function MessageThread({ messages }: { messages: Message[] }) {
  const bottomRef = useRef<HTMLDivElement>(null);

  const scrollToBottom = () => {
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
  };

  return (
    <div className="message-thread">
      {messages.map((message) => (
        <div key={message.id} className="message">
          <strong>{message.author}</strong>
          <p>{message.text}</p>
        </div>
      ))}
      {/* An invisible div at the bottom — we scroll to this */}
      <div ref={bottomRef} />
      <button onClick={scrollToBottom}>Jump to latest</button>
    </div>
  );
}

And measuring an element's dimensions:

import { useRef, useState } from 'react';

function ResizablePanel({ children }: { children: React.ReactNode }) {
  const panelRef = useRef<HTMLDivElement>(null);
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

  const measure = () => {
    if (panelRef.current) {
      const { width, height } = panelRef.current.getBoundingClientRect();
      setDimensions({ width: Math.round(width), height: Math.round(height) });
    }
  };

  return (
    <div>
      <div ref={panelRef} className="panel">
        {children}
      </div>
      <button onClick={measure}>Measure panel</button>
      <p>
        {dimensions.width} × {dimensions.height}px
      </p>
    </div>
  );
}

<aside> ⌨️

Hands On

Build a chat interface with a list of messages and an input field at the bottom. Use a ref to automatically scroll to the bottom of the message list whenever a new message is added. Trigger this inside a useEffect that runs whenever the messages array changes.

</aside>

Passing Refs to Custom Components

The ref attribute works automatically on built-in HTML elements like <input>, <div>, and <button>. When you want to pass a ref through your own component to the underlying DOM element, accept ref alongside your other props and pass it down:

import { useRef } from 'react';

interface TextInputProps {
  placeholder: string;
  ref?: React.Ref<HTMLInputElement>;
}

function TextInput({ placeholder, ref }: TextInputProps) {
  return <input ref={ref} type="text" placeholder={placeholder} />;
}

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

  return (
    <div>
      <TextInput ref={inputRef} placeholder="Name" />
      <button onClick={() => inputRef.current?.focus()}>
        Focus the input
      </button>
    </div>
  );
}

ref is just a prop like any other — you accept it, pass it to the element, and everything works as expected.

forwardRef in Legacy Codebases

Before React 19, refs could not be passed as regular props. The required pattern was forwardRef, a wrapper function that provided the ref as a second argument to the component:

import { forwardRef, useRef } from 'react';

interface TextInputProps {
  placeholder: string;
}

const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
  ({ placeholder }, ref) => {
    return <input ref={ref} type="text" placeholder={placeholder} />;
  }
);

The behaviour is identical to the React 19 approach — forwardRef is just the older API for doing the same thing. You won't need to write it in new projects, but you'll encounter it often in React 17 and 18 codebases, open source component libraries, and documentation written before React 19.

Refs vs State: Choosing the Right Tool

The rule is straightforward: if a value needs to appear in the UI, use state. If your code just needs to remember something without affecting what's displayed, use a ref.

The clearest way to see the difference is the same component written both ways:

// ✅ State — the count updates on screen when you click
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      You clicked {count} times
    </button>
  );
}

// 🔴 Ref — clicking does nothing visible, because changing a ref
// doesn't trigger a re-render
function Counter() {
  const countRef = useRef(0);

  return (
    <button onClick={() => countRef.current++}>
      You clicked {countRef.current} times
    </button>
  );
}

The ref version silently increments the value — but the button text never updates, because React doesn't know anything changed.

One other difference worth knowing: when you update a ref, the change is immediate.

ref.current = 5;
console.log(ref.current); // 5 — right away

State doesn't work this way. State updates are queued and applied on the next render, which is why you see the "snapshot" behaviour from week 5. Refs have no such mechanism — they're plain JavaScript objects, so mutations take effect instantly.

Refs State
Triggers re-render No Yes
Updated Immediately On next render
Read during render Unreliable Safe
Use for DOM access, internal values Anything shown in the UI

When Not to Use Refs

Refs are genuinely useful, but they're an escape hatch — something you reach for when React's model isn't enough, not something you use by default.

A few patterns to avoid:

Don't read from a ref to compute what to render. If you need a value in your JSX, it belongs in state. Refs are for code that runs outside of rendering (event handlers, effects, callbacks).

// 🔴 Don't do this — the component won't re-render when the ref changes
function Counter() {
  const countRef = useRef(0);
  return (
    <div>
      <p>{countRef.current}</p>
      <button onClick={() => countRef.current++}>Increment</button>
    </div>
  );
}

// ✅ If it needs to show in the UI, use state
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
}

Don't manipulate DOM elements that React is managing. If you use a ref to change the text content, style, or structure of an element that React is rendering, React may overwrite your changes on the next render. Refs for DOM manipulation work best on elements whose content React isn't actively controlling — like a chart container where a third-party library is responsible for the contents.

<aside> 🎉

Celebration

Refs are one of those topics that suddenly makes a whole category of problems solvable. Focus management, scroll control, measuring elements, integrating with third-party libraries — all of these are now in your toolkit. And you understand not just how to use refs, but when they're the right choice and when state is the better answer.

</aside>

Best Practices

Additional Resources

Videos

Reading

Tools