Ich baue gerade ein Karussell in React. Um zu den einzelnen Folien zu scrollen, verwende ich document.querySelector
wie so:
useEffect(() => {
document.querySelector(`#slide-${activeSlide}`).scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest'
});
}, [activeSlide]);
Ist das eine schlechte Praxis? Immerhin greife ich hier direkt auf das DOM zu? Was wäre der React Weg, dies zu tun?
edit: vollständige return
Methode
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>
</>
);
Ich kann den Teil "Sollten Sie" nicht beantworten, ob Sie stattdessen Refs verwenden sollen, außer wenn Sie dies tun. Sie benötigen diese id
-Werte nur, wenn Sie sie für etwas anderes verwenden.
Aber so würden Sie:
Verwenden Sie useRef(null)
, um die Referenz zu erstellen.
const activeSlideRef = useRef(null);
Setzen Sie es auf das Slide
, das gerade aktiv ist
<Slide ref={i === activeSlide ? activeSlideRef : null} ...>
Verwenden Sie in Ihrem useEffect
die Eigenschaft current
des Ref
useEffect(() => {
if (activeSlideRef.current) {
activeSlideRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest'
});
}
}, [activeSlide]);
(Ich denke, activeSlide
ist eine vernünftige Abhängigkeit für diesen Effekt. Sie können den Ref nicht verwenden, der Ref selbst variiert nicht ...)
Live-Beispiel: Ich habe einige Ihrer Komponenten der Einfachheit halber in div
s umgewandelt:
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 einem Kommentar haben Sie gefragt:
Wissen Sie, ob es möglich ist,
useEffect
hier für das erste Rendern zu deaktivieren?
Interessanterweise verwenden Sie useRef
, um nicht zustandsbezogene Informationen pro Komponente zu erhalten. Die docs for useRef
weisen darauf hin, dass dies nicht nur für DOM-Elementreferenzen gilt, sondern auch für Nicht-Statusdaten pro Komponente. Also könntest du haben
const firstRenderRef = useRef(true);
aktivieren Sie dann in Ihrem useEffect
Rückruf firstRenderRef.current
& Mndash; Wenn es true
ist, setzen Sie es false
, andernfalls führen Sie den Bildlauf durch:
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>
Als Gedankenexperiment habe ich einen Haken geschrieben, um die Ergonomie etwas zu vereinfachen:
function useInstance(instance = {}) {
// assertion: instance && typeof instance === "object"
const ref = useRef(instance);
return ref.current;
}
Verwendung:
const inst = useInstance({first: true});
In useEffect
, wenn inst.first
ist wahr, mach inst.first = false;
; Andernfalls führen Sie den Bildlauf durch.
Leben:
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>