useRef for Performance
The only way to improve performance is to do less work. Immutable state changes can cause a lot of work by rerendering entire component trees and updating DOM element. Now imagine that happing at 60fps in a mouse move event. A lot of work means low performance. Time to consider mutable.
useRef and useCallback
Callbacks are a prime place to switch to mutable. This is particularly true if a state is tracked across multiple callbacks or if state updates at a faster frequency than needed to render.
Example
In this example a mouse drag in the component will move a child square.
The square position needs to be stored in state because it is used in the component's render, but since the mouse move event handler needs the current position to calculate the distance, the mouse handler gets updated on every mouse move. This is an addition DOM change. It also tracks the last mouse position which is only used in callbacks, but it also causes component renders and changes to the handlers because it is a useState
.
type Point = {
x: number;
y: number;
};
function DragRedSquare() {
// the position of the red square
const [position, setPosition] = useState<Point>({
x: 0,
y: 0,
});
// the last mouse position
const [mouseLast, setMouseLast] = useState<Point>({
x: 0,
y: 0,
});
// mouse down
const handleMouseDown = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
setMouseLast({
x: event.clientX,
y: event.clientY,
});
},
[setMouseLast],
);
// mouse move
const handleMouseMove = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
if (event.buttons == 1) {
// calculate the distance of the mouse move
const dx = event.clientX - mouseLast.x;
const dy = event.clientY - mouseLast.y;
// move the red square the same distance
setPosition({
x: position.x + dx,
y: position.y + dy,
});
// save the mouse position for the next move
setMouseLast({
x: event.clientX,
y: event.clientY,
});
}
},
[mouseLast, setMouseLast, position, setPosition],
);
return (
<div
className={clsx(
'min-h-40', // size
'relative', // layout
'overflow-hidden', // overflow
'border', // border
)}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
>
<div
className={clsx(
'w-8 h-8', // size
'absolute', // layout
'bg-red-500', // color
)}
style={{
left: position.x,
top: position.y,
}}
></div>
</div>
);
}
Optimized Example
This next example accomplishes the same outcome but without any handlers changing.
The mouse position, which is only used in callbacks, is stored only in a ref. The square position is stored in both a ref and a useState
. The handler only reads from the ref and writes to the state. This removes an unnecessary DOM change.
function DragGreenSquare() {
// the position of the green square
const [position, setPosition] = useState<Point>({
x: 0,
y: 0,
});
// the last mouse position
// this value is a ref and only used in callbacks
const mouseLast = useRef<Point>({
x: 0,
y: 0,
});
// the last position of the green square
// this value is a ref and only used in callbacks
const positionLast = useRef<Point>(position);
// mouse down
const handleMouseDown = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
mouseLast.current = {
x: event.clientX,
y: event.clientY,
};
},
[mouseLast],
);
// mouse move
const handleMouseMove = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
if (event.buttons) {
// calculate the distance of the mouse move
const dx = event.clientX - mouseLast.current.x;
const dy = event.clientY - mouseLast.current.y;
// save the mouse position for the next move
mouseLast.current = {
x: event.clientX,
y: event.clientY,
};
// save the new green square position
positionLast.current = {
x: positionLast.current.x + dx,
y: positionLast.current.y + dy,
};
// move the green square
setPosition(positionLast.current);
}
},
[mouseLast, positionLast, setPosition],
);
return (
<div
className={clsx(
'min-h-40', // size
'relative', // layout
'overflow-hidden', // overflow
'border', // border
)}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
>
<div
className={clsx(
'w-8 h-8', // size
'absolute', // layout
'bg-green-500', // color
)}
style={{
left: position.x,
top: position.y,
}}
></div>
</div>
);
}
Optimal Example
This next example uses a canvas for rendering the same effect.
In this example there are no React or DOM changes at all during drag. Since the position is not used in the component render, we don't need to store it in immutable state. If we did, the component would unnecessarily render and we'd be left wondering why canvas is slow.
function DragBlueSquare() {
// canvas ref
const canvas = useRef<HTMLCanvasElement | null>(null);
// the mouse position
const mouse = useRef<Point>({
x: 0,
y: 0,
});
// the position of the blue square
const position = useRef<Point>({
x: 0,
y: 0,
});
// render the canvas
const renderCanvas = useCallback(() => {
if (canvas.current) {
const ctx = canvas.current.getContext('2d');
if (ctx) {
ctx.fillStyle = 'blue';
ctx.clearRect(0, 0, 384, 160);
ctx.fillRect(position.current.x, position.current.y, 32, 32);
}
}
}, [canvas, position]);
// first render
useEffect(() => {
renderCanvas();
}, [renderCanvas]);
// mouse down
const handleMouseDown = useCallback(
(event: React.MouseEvent<HTMLCanvasElement>) => {
mouse.current = {
x: event.clientX,
y: event.clientY,
};
},
[mouse],
);
// mouse move
const handleMouseMove = useCallback(
(event: React.MouseEvent<HTMLCanvasElement>) => {
if (event.buttons) {
// calculate the distance of the mouse move
const dx = event.clientX - mouse.current.x;
const dy = event.clientY - mouse.current.y;
// save the mouse position for the next move
mouse.current = {
x: event.clientX,
y: event.clientY,
};
// save the new blue square position
position.current = {
x: position.current.x + dx,
y: position.current.y + dy,
};
renderCanvas();
}
},
[mouse, position],
);
return (
<canvas
ref={canvas}
className={clsx(
'w-96 h-40', // size
'border', // border
)}
width={384}
height={160}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
></canvas>
);
}
This example shows that useRef
gives us a powerful tool to remove unnecessary work while still embracing React patterns. But what if we had a state library that could share common state across multiple components where each could use the state immutably or mutably as needed?
This is exactly what React Scopes gives us.