I sometimes find myself googling useRef
vs useState
and found it helpful to talk to ChatGPT about it. Specifically I wanted some concrete examples of when useRef
is more beneficial. ChatGPT did a pretty good job at this. I cleaned up the conversation and packaged into a post so that it may be helpful for others in the future!
useState
and useRef
are both hooks provided by React, but they serve different purposes and are used in different scenarios:
const [state, setState] = useState(initialValue);
const ref = useRef(initialValue);
.current
property does not cause the component to re-render.useState
when you need a value that, when updated, should cause the component to re-render. It's typically used for data that is tied to the UI.useRef
when you need a mutable reference to a DOM element or you need to store a value persistently across renders without causing re-renders. It's not suitable for triggering UI updates.In summary, useState
is for state management that is tied to the render cycle, while useRef
is for accessing DOM elements or keeping track of data across renders without influencing the render cycle.
Here are ten examples that I thought were neat.
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
Explanation: This helps in managing DOM state without causing unnecessary re-renders
setTimeout
or setInterval
function TimerComponent() {
const timerId = useRef(null);
useEffect(() => {
timerId.current = setInterval(() => console.log('Tick'), 1000);
return () => clearInterval(timerId.current);
}, []);
// ...
}
Explanation: Store a timer ID for setInterval
or setTimeout
. This allows you to clear the timer in the cleanup function of useEffect
without re-rendering the component every time the timer ID changes.
function RenderCounterComponent() {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
// ...
}
Explanation: Keep track of how many times the component has rendered. This count is for internal use and doesn't trigger additional renders.
function ComponentWithPreviousProps(props) {
const prevProps = useRef();
useEffect(() => {
prevProps.current = props;
}); // No dependency array, runs after every render
// Compare props.current with prevProps.current for changes
// ...
}
Explanation: Useful for comparing the previous and current props or state values, for instance, to detect specific changes or manage transitions.
function ComponentWithSkipFirstEffect() {
const isFirstRun = useRef(true);
useEffect(() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
// Effect logic to run from the second render onwards
});
// ...
}
Explanation: This pattern is useful when you want an effect to run on updates only, not on the initial mount.
function ThrottledComponent() {
const lastCalled = useRef(Date.now());
const throttledFunction = () => {
if (Date.now() - lastCalled.current >= 1000) {
lastCalled.current = Date.now();
// Execute some action
}
};
// ...
}
Explanation: useRef
is used to store the timestamp of the last function call, ensuring the function is called no more than once per second.
function MouseTrackerComponent() {
const mousePosition = useRef({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
mousePosition.current = { x: e.clientX, y: e.clientY };
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
// ...
}
Explanation: Store the latest mouse position without re-rendering the component each time the mouse moves.
function EventListenerComponent() {
const eventListener = useRef(null);
useEffect(() => {
eventListener.current = (e) => {
// Handle event
};
window.addEventListener('customEvent', eventListener.current);
return () => window.removeEventListener('customEvent', eventListener.current);
}, []);
// ...
}
Explanation: Store a reference to an event listener function, making it easy to add and remove the same instance of the listener for proper cleanup.
function AnimationComponent() {
const animationFrameId = useRef(null);
const animate = () => {
// Animation logic here
animationFrameId.current = requestAnimationFrame(animate);
};
useEffect(() => {
animationFrameId.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(animationFrameId.current);
}, []);
// ...
}
Explanation: Manage the ID of an animation frame when using requestAnimationFrame
. This is useful for animations that are independent of React's rendering logic.
function ComponentWithLatestState() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);
useEffect(() => {
latestCount.current = count;
}, [count]);
const someDelayedAction = useCallback(() => {
console.log(latestCount.current); // Always logs the latest count
}, []);
// ...
}
Explanation: Here, useRef
is used to keep a reference to the latest state value for use in callbacks or event handlers, ensuring they always have access to the current state.