Keywords: React | useRef | DOM manipulation | useEffect | carousel
Abstract: This article explores the pitfalls of using document.querySelector for direct DOM manipulation in React applications and details the best practices of replacing it with useRef, focusing on a carousel component case study. It delves into creating refs, assigning references, implementing scrolling logic in useEffect, avoiding side effects on first render, and summarizes the advantages of refs, such as lifecycle awareness and platform agnosticism.
Background and Challenges of Direct DOM Access
In React development, manipulating DOM elements is often necessary, such as for implementing smooth scrolling in a carousel component. A common approach is to use document.querySelector to select specific elements and invoke the scrollIntoView method, as shown in the following example:
useEffect(() => {
document.querySelector(`#slide-${activeSlide}`).scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest'
});
}, [activeSlide]);While functional, directly accessing the DOM may be considered bad practice as it bypasses React's declarative data flow, potentially leading to maintenance issues like duplicate element IDs or performance overhead from DOM queries. React recommends using the useRef hook to manage DOM references for safer and more efficient interactions.
The React Way: Utilizing useRef
useRef is a React hook that creates mutable reference objects, primarily used for accessing DOM elements or storing non-state data across component lifecycles. By using useRef, developers can avoid direct DOM queries and better integrate with React's virtual DOM system.
The implementation steps are as follows: first, create a reference using useRef(null), then assign it to the currently active Slide component, and finally use the reference's current property in useEffect for scrolling operations.
Here is a core code example:
const activeSlideRef = useRef(null);
return (
<>
{children.map((child, i) => {
const active = i === activeSlide;
return (
<Slide ref={active ? activeSlideRef : null} key={`slide-${i}`}>
{child}
</Slide>
);
})}
</>
);In useEffect, use the optional chaining operator ?. to handle cases where the reference might be null, ensuring robustness:
useEffect(() => {
activeSlideRef.current?.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "nearest"
});
}, [activeSlideRef.current]);Preventing Scrolling on First Render
In some scenarios, it may be necessary to prevent the useEffect from triggering scrolling on the first render of the component. This can be achieved by using useRef to store non-state data, such as creating a firstRenderRef to mark the initial render.
const firstRenderRef = useRef(true);
useEffect(() => {
if (firstRenderRef.current) {
firstRenderRef.current = false;
return;
}
activeSlideRef.current?.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "nearest"
});
}, [activeSlide]);As an extension, custom hooks like useInstance can be encapsulated to simplify this pattern:
function useInstance(instance = {}) {
const ref = useRef(instance);
return ref.current;
}Advantages of Using Refs
Compared to document.querySelector, useRef offers several benefits: it allows unique element identification in O(1) time, avoiding duplicate ID errors; it automatically synchronizes with React component lifecycle, such as setting references to null on unmount; it supports cross-platform usage, e.g., in React Native; and for server-side rendering (SSR), refs can safely point to React elements rather than DOM. While document.querySelector is not incorrect, adhering to React's built-in mechanisms generally leads to better performance and maintainability.
Conclusion
In React, it is recommended to use useRef instead of document.querySelector for DOM manipulation, as it aligns better with React's declarative paradigm and enhances code predictability and efficiency. By properly assigning references and managing side effects, developers can build more robust interactive components.