Skip to content

Preact Refs and DOM Access — Direct Element Manipulation in 3kB

DodaTech Updated 2026-06-28 5 min read

Learn how to use refs in Preact for direct DOM access, integrating with third-party libraries, managing focus, and measuring elements in the 3kB framework.

In this lesson, you'll understand the ref attribute, useRef hook, callback refs, and forwardRef for passing refs through component boundaries.

What You'll Learn

How to use useRef for DOM references, callback refs for dynamic refs, forwardRef to expose child DOM nodes, and useImperativeHandle for custom ref APIs.

Why It Matters

Refs provide an escape hatch when declarative rendering isn't enough: managing focus, measuring layout, animating elements, and integrating with imperative libraries like Chart.js or D3.

Real-World Use

Durga Antivirus Pro's scan result chart uses useRef to access a canvas element and pass it to Chart.js for rendering performance graphs that Preact's Virtual Dom can't draw.

flowchart LR
    A[Component] -->|useRef| B[DOM Element]
    A -->|forwardRef| C[Child Component]
    C -->|useImperativeHandle| D[Custom Ref API]
    B --> E[Focus, Measure, Animate]
    D --> F[Parent Calls Methods]
    style A fill:#673ab8,color:#fff
    style E fill:#4a148c,color:#fff

useRef for DOM Elements

Access DOM nodes directly:

import { useRef, useEffect } from 'preact/hooks';

function VideoPlayer({ src }) {
  const videoRef = useRef(null);

  const play = () => videoRef.current?.play();
  const pause = () => videoRef.current?.pause();
  const jumpToStart = () => {
    if (videoRef.current) videoRef.current.currentTime = 0;
  };

  return (
    <div>
      <video ref={videoRef} src={src} width={400} />
      <div>
        <button onClick={play}>Play</button>
        <button onClick={pause}>Pause</button>
        <button onClick={jumpToStart}>Restart</button>
      </div>
    </div>
  );
}

Output: A video player with play, pause, and restart buttons. Each button calls the native HTMLMediaElement API through the ref, controlling the video imperatively.

Callback Refs

Use a callback function for more control over ref attachment:

import { useState } from 'preact/hooks';

function MeasureExample() {
  const [height, setHeight] = useState(0);

  const measureRef = (node) => {
    if (node) {
      const rect = node.getBoundingClientRect();
      setHeight(rect.height);
    }
  };

  return (
    <div>
      <p>The box below is {height}px tall</p>
      <div ref={measureRef}
        style={{ padding: 20, background: '#673ab8', color: '#fff' }}>
        This is a resizable box. Its height is measured after mount.
      </div>
    </div>
  );
}

Output: The component measures the box height after mounting and displays it. Callback refs run when the ref attaches and when it detaches (with null).

forwardRef

Pass refs through component boundaries:

import { forwardRef } from 'preact';

const FancyInput = forwardRef((props, ref) => {
  return (
    <div style={{ border: '2px solid #673ab8', borderRadius: 4 }}>
      <input ref={ref} {...props} style={{ border: 'none', padding: 8 }} />
    </div>
  );
});

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

  const focusInput = () => {
    inputRef.current?.focus();
  };

  return (
    <div>
      <FancyInput ref={inputRef} placeholder="Type here..." />
      <button onClick={focusInput}>Focus the input</button>
    </div>
  );
}

Output: Clicking "Focus the input" focuses the input inside the FancyInput wrapper. forwardRef lets the parent access the underlying DOM input through the component boundary.

useImperativeHandle

Expose a custom API from a child component:

import { forwardRef, useImperativeHandle, useRef } from 'preact';

const CustomPlayer = forwardRef(({ src }, ref) => {
  const videoRef = useRef(null);

  useImperativeHandle(ref, () => ({
    play: () => videoRef.current?.play(),
    pause: () => videoRef.current?.pause(),
    restart: () => {
      if (videoRef.current) {
        videoRef.current.currentTime = 0;
        videoRef.current.play();
      }
    },
    getDuration: () => videoRef.current?.duration || 0,
    isPlaying: () => videoRef.current ? !videoRef.current.paused : false
  }));

  return <video ref={videoRef} src={src} width={400} />;
});

function App() {
  const playerRef = useRef(null);

  return (
    <div>
      <CustomPlayer ref={playerRef} src="/video.mp4" />
      <button onClick={() => console.log('Duration:', playerRef.current?.getDuration())}>
        Log Duration
      </button>
    </div>
  );
}

Output: The parent calls playerRef.current.getDuration() through the imperative API exposed by useImperativeHandle, without direct access to the video DOM element.

Refs and Third-Party Libraries

Integrate Preact with imperative libraries:

import { useRef, useEffect } from 'preact/hooks';

function Chart({ data, labels }) {
  const canvasRef = useRef(null);

  useEffect(() => {
    // Imagine a Chart.js-like library
    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');

    // Draw bar chart
    const max = Math.max(...data);
    const barWidth = canvas.width / data.length - 4;

    ctx.clearRect(0, 0, canvas.width, canvas.height);
    data.forEach((value, i) => {
      const barHeight = (value / max) * (canvas.height - 20);
      ctx.fillStyle = '#673ab8';
      ctx.fillRect(i * (barWidth + 4) + 2, canvas.height - barHeight, barWidth, barHeight);
      ctx.fillStyle = '#000';
      ctx.fillText(labels[i], i * (barWidth + 4) + 2, canvas.height - 5);
    });
  }, [data, labels]);

  return <canvas ref={canvasRef} width={400} height={200} />;
}

Output: A bar chart drawn imperatively on a canvas. The ref provides direct access to the canvas element for custom drawing logic.

Common Mistakes

  1. Using refs when state would work: Refs don't trigger re-renders. If changing a value should update the UI, use useState not useRef.
  2. Accessing ref.current before the component mounts: In useEffect, the DOM is available. Outside effects, ref.current may be null.
  3. Forgetting forwardRef for wrapped components: If a component wraps a DOM element in another element, the parent needs forwardRef to pass the ref through.
  4. Overusing useImperativeHandle: Imperative APIs break the declarative model. Only expose imperative methods when declarative patterns don't work.
  5. Not checking ref.current before calling methods: Always guard with ref.current?.method() or if (ref.current) to avoid runtime errors when the ref hasn't attached yet.

Practice Questions

  1. What does useRef return? Answer: A mutable object with a .current property. On initial render, ref.current is the initial value passed to useRef(null).

  2. Why do you need forwardRef? Answer: Components don't natively pass refs to their children. forwardRef lets a component accept a ref from its parent and attach it to a child DOM element.

  3. What is the difference between useRef and useState? Answer: Changing ref.current doesn't trigger a re-render. Changing state with setState or the useState updater triggers a re-render.

  4. When does a callback ref run? Answer: It runs when the component mounts (with the DOM node) and when it unmounts (with null). It also runs when the ref changes.

Challenge

Build a drag-and-drop zone using refs. Track onMouseDown, onMouseMove, and onMouseUp on a div ref to implement a custom drag interaction. Display the current drag position.

Mini Project

Create an image gallery with a lightbox viewer. Use refs for the lightbox overlay (direct DOM manipulation for animations) and forwardRef for reusable image thumbnails.

FAQ

Does Preact support `createRef`?

: Yes. createRef is available from preact for class components. It returns { current: null }, same as React's createRef.

Can refs be used with class components?

: Yes. Class components can use createRef in the constructor or callback refs in the render method.

Do refs work with Preact signals?

: Yes. Refs and signals are independent concepts. Use useRef for DOM access and signals for reactive state.

What happens to a ref when the component unmounts?

: Preact sets ref.current to null when the component unmounts. Cleanup effects should still run before this happens.

What's Next

Learn about Preact Portals to understand how to render components outside the parent DOM hierarchy.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro