Preact Refs and DOM Access — Direct Element Manipulation in 3kB
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
- Using refs when state would work: Refs don't trigger re-renders. If changing a value should update the UI, use
useStatenotuseRef. - Accessing
ref.currentbefore the component mounts: InuseEffect, the DOM is available. Outside effects,ref.currentmay be null. - Forgetting
forwardReffor wrapped components: If a component wraps a DOM element in another element, the parent needsforwardRefto pass the ref through. - Overusing
useImperativeHandle: Imperative APIs break the declarative model. Only expose imperative methods when declarative patterns don't work. - Not checking
ref.currentbefore calling methods: Always guard withref.current?.method()orif (ref.current)to avoid runtime errors when the ref hasn't attached yet.
Practice Questions
What does
useRefreturn? Answer: A mutable object with a.currentproperty. On initial render,ref.currentis the initial value passed touseRef(null).Why do you need
forwardRef? Answer: Components don't natively pass refs to their children.forwardReflets a component accept a ref from its parent and attach it to a child DOM element.What is the difference between
useRefanduseState? Answer: Changingref.currentdoesn't trigger a re-render. Changing state withsetStateor theuseStateupdater triggers a re-render.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
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