useImperativeHandle customizes the instance value that is exposed to parent components when using ref. This hook should be used with forwardRef.
Signature
function useImperativeHandle<T, R extends T>(
ref: Ref<T>,
create: () => R,
inputs?: ReadonlyArray<unknown>
): void
Parameters
The ref that will be mutated. This is typically the ref forwarded from a parent component.
A function that returns the value to be attached to ref.current. This should return an object with the methods/properties you want to expose.
An array of dependencies. The effect will only activate if the values in this array change (compared using ===). If omitted, the handle is recreated on every render.
Returns
void
Basic Usage
import { forwardRef, useImperativeHandle, useRef } from 'preact/compat';
import { useRef as useHookRef } from 'preact/hooks';
const FancyInput = forwardRef((props, ref) => {
const inputRef = useHookRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
clear: () => {
inputRef.current.value = '';
}
}), []);
return <input ref={inputRef} {...props} />;
});
function Parent() {
const inputRef = useHookRef();
return (
<div>
<FancyInput ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>Focus</button>
<button onClick={() => inputRef.current.clear()}>Clear</button>
</div>
);
}
Video Player Control
const VideoPlayer = forwardRef(({ src }, ref) => {
const videoRef = useRef();
useImperativeHandle(ref, () => ({
play: () => {
videoRef.current.play();
},
pause: () => {
videoRef.current.pause();
},
seek: (time) => {
videoRef.current.currentTime = time;
},
setVolume: (volume) => {
videoRef.current.volume = Math.max(0, Math.min(1, volume));
},
getCurrentTime: () => {
return videoRef.current.currentTime;
}
}), []);
return <video ref={videoRef} src={src} />;
});
function VideoController() {
const playerRef = useRef();
return (
<div>
<VideoPlayer ref={playerRef} src="video.mp4" />
<button onClick={() => playerRef.current.play()}>Play</button>
<button onClick={() => playerRef.current.pause()}>Pause</button>
<button onClick={() => playerRef.current.seek(0)}>Restart</button>
<button onClick={() => playerRef.current.setVolume(0.5)}>50% Volume</button>
</div>
);
}
const Form = forwardRef(({ children }, ref) => {
const formRef = useRef();
const [errors, setErrors] = useState({});
useImperativeHandle(ref, () => ({
submit: () => {
formRef.current.dispatchEvent(
new Event('submit', { cancelable: true, bubbles: true })
);
},
reset: () => {
formRef.current.reset();
setErrors({});
},
validate: () => {
const isValid = formRef.current.checkValidity();
if (!isValid) {
// Collect validation errors
const newErrors = {};
const elements = formRef.current.elements;
for (let i = 0; i < elements.length; i++) {
const el = elements[i];
if (!el.validity.valid) {
newErrors[el.name] = el.validationMessage;
}
}
setErrors(newErrors);
}
return isValid;
},
getValues: () => {
const formData = new FormData(formRef.current);
return Object.fromEntries(formData.entries());
}
}), []);
return <form ref={formRef}>{children}</form>;
});
function App() {
const formRef = useRef();
const handleExternalSubmit = () => {
if (formRef.current.validate()) {
const values = formRef.current.getValues();
console.log('Form values:', values);
formRef.current.submit();
}
};
return (
<div>
<Form ref={formRef}>
<input name="email" type="email" required />
<input name="password" type="password" required />
</Form>
<button onClick={handleExternalSubmit}>Submit</button>
<button onClick={() => formRef.current.reset()}>Reset</button>
</div>
);
}
Modal Control
const Modal = forwardRef(({ title, children }, ref) => {
const [isOpen, setIsOpen] = useState(false);
useImperativeHandle(ref, () => ({
open: () => setIsOpen(true),
close: () => setIsOpen(false),
toggle: () => setIsOpen(prev => !prev),
isOpen: () => isOpen
}), [isOpen]);
if (!isOpen) return null;
return (
<div className="modal-overlay">
<div className="modal">
<h2>{title}</h2>
<div>{children}</div>
<button onClick={() => setIsOpen(false)}>Close</button>
</div>
</div>
);
});
function App() {
const modalRef = useRef();
return (
<div>
<button onClick={() => modalRef.current.open()}>Open Modal</button>
<Modal ref={modalRef} title="My Modal">
<p>Modal content here</p>
</Modal>
</div>
);
}
Canvas Drawing API
const Canvas = forwardRef(({ width, height }, ref) => {
const canvasRef = useRef();
useImperativeHandle(ref, () => {
const ctx = canvasRef.current?.getContext('2d');
return {
clear: () => {
ctx.clearRect(0, 0, width, height);
},
drawCircle: (x, y, radius, color = 'black') => {
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
},
drawRect: (x, y, w, h, color = 'black') => {
ctx.fillStyle = color;
ctx.fillRect(x, y, w, h);
},
drawLine: (x1, y1, x2, y2, color = 'black', width = 1) => {
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
},
getImageData: () => {
return canvasRef.current.toDataURL();
}
};
}, [width, height]);
return <canvas ref={canvasRef} width={width} height={height} />;
});
function DrawingApp() {
const canvasRef = useRef();
return (
<div>
<Canvas ref={canvasRef} width={800} height={600} />
<button onClick={() => canvasRef.current.drawCircle(100, 100, 50, 'red')}>
Draw Circle
</button>
<button onClick={() => canvasRef.current.clear()}>Clear</button>
</div>
);
}
With Dependencies
const Counter = forwardRef(({ step = 1 }, ref) => {
const [count, setCount] = useState(0);
useImperativeHandle(ref, () => ({
increment: () => setCount(c => c + step),
decrement: () => setCount(c => c - step),
reset: () => setCount(0),
getValue: () => count
}), [step, count]); // Recreate when step or count changes
return <div>Count: {count}</div>;
});
useImperativeHandle should be used with forwardRef. It allows child components to expose a custom API to parent components through refs.
This hook runs during the layout phase (like useLayoutEffect), so the ref value is updated synchronously before the browser paints.
Avoid overusing imperative APIs. Most interactions between components should be done declaratively through props. Use useImperativeHandle sparingly for cases where you need to expose imperative methods like focus(), play(), or reset().