A React hook that reliably runs an effect once when an element is mounted. Unlike useEffect, this hook guarantees single execution and proper cleanup, making it ideal for setting up event listeners and DOM manipulations.
function useRefEffect<T>(
callback: (root: T) => (() => void) | void
): (root: T) => void
| Parameter | Type | Description |
|---|---|---|
| callback | (root: T) => (() => void) | void | Function called when the element mounts. Can return a cleanup function. |
A ref callback function that should be passed to the element's ref prop.
React's useEffect with an empty dependency array doesn't reliably run only once due to:
useRefEffect solves this by using a ref callback pattern that guarantees:
import { useRefEffect } from "aidos-ui";
function ResizeObserverComponent() {
const ref = useRefEffect((element) => {
const observer = new ResizeObserver((entries) => {
console.log("Size changed:", entries[0].contentRect);
});
observer.observe(element);
// Return cleanup function
return () => observer.disconnect();
});
return <div ref={ref}>Resize me!</div>;
}
Perfect for attaching event listeners to elements:
function DraggableElement() {
const ref = useRefEffect((element) => {
let isDragging = false;
let startX = 0;
let startY = 0;
const onMouseDown = (e) => {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
};
const onMouseMove = (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
element.style.transform = `translate(${dx}px, ${dy}px)`;
};
const onMouseUp = () => {
isDragging = false;
};
element.addEventListener("mousedown", onMouseDown);
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mouseup", onMouseUp);
return () => {
element.removeEventListener("mousedown", onMouseDown);
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
};
});
return <div ref={ref}>Drag me</div>;
}
Use for operations that need the actual DOM element:
function MeasuredElement() {
const [dimensions, setDimensions] = useState(null);
const ref = useRefEffect((element) => {
const rect = element.getBoundingClientRect();
setDimensions({
width: rect.width,
height: rect.height,
});
});
return (
<div ref={ref}>
{dimensions && (
<span>
{dimensions.width}x{dimensions.height}
</span>
)}
</div>
);
}
Initialize libraries that need a DOM element:
function ChartComponent({ data }) {
const ref = useRefEffect((element) => {
const chart = new SomeChartLibrary(element, {
data,
width: 400,
height: 300,
});
return () => chart.destroy();
});
return <div ref={ref} />;
}
If no cleanup is needed, simply don't return anything:
const ref = useRefEffect((element) => {
element.scrollIntoView({ behavior: "smooth" });
// No cleanup needed
});
useRefEffect is the foundation for other hooks in this library:
// useKeyboard is built on useRefEffect
export function useKeyboard<T extends HTMLElement>(
shortcuts: Array<KeyboardShortcut<T>>
) {
return useRefEffect<T>((root: T) => {
const onKeyDown = (e: KeyboardEvent) => {
// Handle shortcuts...
};
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
});
}
The hook is generic and preserves element types:
// Typed as HTMLInputElement
const inputRef = useRefEffect<HTMLInputElement>((input) => {
input.focus();
input.select();
});
// Typed as HTMLCanvasElement
const canvasRef = useRefEffect<HTMLCanvasElement>((canvas) => {
const ctx = canvas.getContext("2d");
// ctx is properly typed
});
// ❌ useEffect - may run multiple times in dev/strict mode
useEffect(() => {
const element = ref.current;
if (!element) return;
element.addEventListener("click", handler);
return () => element.removeEventListener("click", handler);
}, []);
// ✅ useRefEffect - runs exactly once
const ref = useRefEffect((element) => {
element.addEventListener("click", handler);
return () => element.removeEventListener("click", handler);
});
The hook uses a ref callback combined with a cleanup ref: