I am building a carousel right now, in React. To scroll to the individual slides I am using document.querySelector
like so :
useEffect(() => {
document.querySelector(`#slide-${activeSlide}`).scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest'
});
}, [activeSlide]);
Is this bad practice? After all, I am accessing the DOM directly here? What would be the React way of doing this?
edit: full return
method
return (
<>
<button onClick={() => setActiveSlide(moveLeft)}>PREV</button>
<Wrapper id="test">
{children.map((child, i) => {
return (
<Slide id={`slide-${i}`} key={`slide-${i}`}>
{child}
</Slide>
);
})}
</Wrapper>
<button onClick={() => setActiveSlide(moveRight)}>NEXT</button>
</>
);
It's still possible to use querySelector in React, however, React encourages developers not to use it. Instead, we should aim to use refs if possible.
In react, there is another way to use refs that is called "callback refs" and it gives more control when the refs are set and unset. Instead of creating refs by createRef() method, React allows a way to create refs by passing a callback function to the ref attribute of a component.
The equivalent of the document. querySelector() method in React is using refs. To select an element, set the ref prop on it to the return value of calling the useRef() hook and access the dom element using the current property on the ref , e.g. ref. current .
We should not use ref attribute on function components because they do not have instances. React will assign the current property with Dom element when component mount and assign null to it when component unmount. ref updates happen before componentDidMount or componentDidUpdate methods.
I can't answer the "should you" part of whether to use refs for this instead other than if you do, you don't need those id
values unless you use them for something else.
But here's how you would:
Use useRef(null)
to create the ref.
const activeSlideRef = useRef(null);
Put it on the Slide
that's currently active
<Slide ref={i === activeSlide ? activeSlideRef : null} ...>
In your useEffect
, use the ref's current
property
useEffect(() => {
if (activeSlideRef.current) {
activeSlideRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest'
});
}
}, [activeSlide]);
(I think activeSlide
is a reasonable dependency for that effect. You can't use the ref, the ref itself doesn't vary...)
Live example, I've turned some of your components into div
s for convenience:
const {useEffect, useRef, useState} = React;
function Deck({children}) {
const [activeSlide, setActiveSlide] = useState(0);
const activeSlideRef = useRef(null);
useEffect(() => {
if (activeSlideRef.current) {
activeSlideRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest'
});
}
}, [activeSlide]);
const moveLeft = Math.max(0, activeSlide - 1);
const moveRight = Math.min(children.length - 1, activeSlide + 1);
return (
<React.Fragment>
<button onClick={() => setActiveSlide(moveLeft)}>PREV</button>
<div id="test">
{children.map((child, i) => {
const active = i === activeSlide;
return (
<div className={`slide ${active ? "active" : ""}`} ref={active ? activeSlideRef : null} id={`slide-${i}`} key={`slide-${i}`}>
{child}
</div>
);
})}
</div>
<button onClick={() => setActiveSlide(moveRight)}>NEXT</button>
</React.Fragment>
);
}
ReactDOM.render(
<Deck>
<div>slide 0 </div>
<div>slide 1 </div>
<div>slide 2 </div>
<div>slide 3 </div>
<div>slide 4 </div>
<div>slide 5 </div>
<div>slide 6 </div>
<div>slide 7 </div>
<div>slide 8 </div>
<div>slide 9 </div>
</Deck>,
document.getElementById("root")
);
.slide {
height: 4em;
vertical-align: middle;
text-align: center;
}
#test {
overflow: scroll;
max-height: 20em;
}
.active {
font-weight: bold;
color: blue;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
In a comment you've asked:
Do you know whether it's possible to disable
useEffect
here for the first render?
To keep non-state per-component info around, interestingly you use useRef
. The docs for useRef
point out that it's not just for DOM element references, it's also for per-component non-state data. So you could have
const firstRenderRef = useRef(true);
then in your useEffect
callback, check firstRenderRef.current
&mndash; if it's true
, set it false
, otherwise do the scrolling:
const {useEffect, useRef, useState} = React;
function Deck({children}) {
const [activeSlide, setActiveSlide] = useState(0);
const activeSlideRef = useRef(null);
// *** Use a ref with the initial value `true`
const firstRenderRef = useRef(true);
console.log("render");
useEffect(() => {
// *** After render, don't do anything, just remember we've seen the render
if (firstRenderRef.current) {
console.log("set false");
firstRenderRef.current = false;
} else if (activeSlideRef.current) {
console.log("scroll");
activeSlideRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest'
});
}
}, [activeSlide]);
const moveLeft = Math.max(0, activeSlide - 1);
const moveRight = Math.min(children.length - 1, activeSlide + 1);
return (
<React.Fragment>
<button onClick={() => setActiveSlide(moveLeft)}>PREV</button>
<div id="test">
{children.map((child, i) => {
const active = i === activeSlide;
return (
<div className={`slide ${active ? "active" : ""}`} ref={active ? activeSlideRef : null} id={`slide-${i}`} key={`slide-${i}`}>
{child}
</div>
);
})}
</div>
<button onClick={() => setActiveSlide(moveRight)}>NEXT</button>
</React.Fragment>
);
}
ReactDOM.render(
<Deck>
<div>slide 0 </div>
<div>slide 1 </div>
<div>slide 2 </div>
<div>slide 3 </div>
<div>slide 4 </div>
<div>slide 5 </div>
<div>slide 6 </div>
<div>slide 7 </div>
<div>slide 8 </div>
<div>slide 9 </div>
</Deck>,
document.getElementById("root")
);
.slide {
height: 4em;
vertical-align: middle;
text-align: center;
}
#test {
overflow: scroll;
max-height: 10em;
}
.active {
font-weight: bold;
color: blue;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
As a thought experiment, I wrote a hook to make the ergonomics a bit easier:
function useInstance(instance = {}) {
// assertion: instance && typeof instance === "object"
const ref = useRef(instance);
return ref.current;
}
Usage:
const inst = useInstance({first: true});
In useEffect
, if inst.first
is true, do inst.first = false;
; otherwise, do the scrolling.
Live:
const {useEffect, useRef, useState} = React;
function useInstance(instance = {}) {
// assertion: instance && typeof instance === "object"
const ref = useRef(instance);
return ref.current;
}
function Deck({children}) {
const [activeSlide, setActiveSlide] = useState(0);
const activeSlideRef = useRef(null);
const inst = useInstance({first: true});
console.log("render");
useEffect(() => {
// *** After render, don't do anything, just remember we've seen the render
if (inst.first) {
console.log("set false");
inst.first = false;
} else if (activeSlideRef.current) {
console.log("scroll");
activeSlideRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest'
});
}
}, [activeSlide]);
const moveLeft = Math.max(0, activeSlide - 1);
const moveRight = Math.min(children.length - 1, activeSlide + 1);
return (
<React.Fragment>
<button onClick={() => setActiveSlide(moveLeft)}>PREV</button>
<div id="test">
{children.map((child, i) => {
const active = i === activeSlide;
return (
<div className={`slide ${active ? "active" : ""}`} ref={active ? activeSlideRef : null} id={`slide-${i}`} key={`slide-${i}`}>
{child}
</div>
);
})}
</div>
<button onClick={() => setActiveSlide(moveRight)}>NEXT</button>
</React.Fragment>
);
}
ReactDOM.render(
<Deck>
<div>slide 0 </div>
<div>slide 1 </div>
<div>slide 2 </div>
<div>slide 3 </div>
<div>slide 4 </div>
<div>slide 5 </div>
<div>slide 6 </div>
<div>slide 7 </div>
<div>slide 8 </div>
<div>slide 9 </div>
</Deck>,
document.getElementById("root")
);
.slide {
height: 4em;
vertical-align: middle;
text-align: center;
}
#test {
overflow: scroll;
max-height: 10em;
}
.active {
font-weight: bold;
color: blue;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
Adding to the accepted answer and trying to answer the 'should' part of the question, wrt using refs for DOM manipulation:
ofcourse, using query selector is not incorrect and it wouldn't break your functionality if you use it in the react world generally, but it is better to use something provided by the framework as it comes with some default benefits in most cases.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With