Poznajemy drugą część hooka useRef, czyli zastosowanie do manipulowania drzewem DOM w React oraz forwardRef. Do dzieła.
Tworzymy ref z nullem:
import { useRef } from 'react';
function MyComponent() {
const inputRef = useRef(null);
// ...
Gdzieś w JSX mamy element z atrybutem ref równym naszej zmiennej:
// ...
return <input ref={inputRef} />;
W event handlerach wykorzystujemy ref.current, aby manipulować elementem:
function handleClick() {
inputRef.current.focus();
}
Przykład pierwszy, focus elementu na klik:
import { useRef } from 'react';
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
Przykład drugi ze scrollem, pokażemy od końca, czyli najpierw JSX:
return (
<>
<nav>
<button onClick={() => scrollToIndex(0)}>
Tom
</button>
<button onClick={() => scrollToIndex(1)}>
Maru
</button>
<button onClick={() => scrollToIndex(2)}>
Jellylorum
</button>
</nav>
<div>
<ul ref={listRef}>
<li>
<img
src="https://placekitten.com/g/200/200"
alt="Tom"
/>
</li>
<li>
<img
src="https://placekitten.com/g/300/200"
alt="Maru"
/>
</li>
<li>
<img
src="https://placekitten.com/g/250/200"
alt="Jellylorum"
/>
</li>
</ul>
</div>
</>
);
}
Mamy tutaj delegację (swego rodzaju) refów, ref jest na ul ustawione. Dobra, teraz JS:
import { useRef } from 'react';
export default function CatFriends() {
const listRef = useRef(null);
function scrollToIndex(index) {
const listNode = listRef.current;
// This line assumes a particular DOM structure:
const imgNode = listNode.querySelectorAll('li > img')[index];
imgNode.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}
Jak widać normalny querySelectorAll na tym wywołujemy, indeks odpowiedni mamy z onClick, inaczej musielibyśmy tworzyć ref do każdego jednego img, tego raczej nie chcemy.
Odtwarzacz video też pokażemy sobie najpierw od strony JSX:
return (
<>
<button onClick={handleClick}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<video
width="250"
ref={ref}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
>
<source
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
type="video/mp4"
/>
</video>
</>
);
}
A teraz JS:
import { useState, useRef } from 'react';
export default function VideoPlayer() {
const [isPlaying, setIsPlaying] = useState(false);
const ref = useRef(null);
function handleClick() {
const nextIsPlaying = !isPlaying;
setIsPlaying(nextIsPlaying);
if (nextIsPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}
Co tu się dzieje na klik? Zakładam, że mamy stan isPlaying na false:
- Tworzymy zmienną nextIsPlaying i ustawiamy na przeciwieństwo, czyli true
- Wywołujemy zmianę stanu na tę zmienną, czyli ustawiamy stan na true
- Jeszcze nie mamy re-rendera. Właśnie po to nam nextIsPlaying, stan się zmieni jak skończy się funkcja handleClick
- Jeszcze stan jest na false, choć true ustawiliśmy, ale mamy nextIsPlaying
- W zależności od jego wartości używamy refa aby odpalić albo zatrzymać odtwarzanie
No i jeszcze forwardRef, czyli jak tworzyć własne komponenty, do których komponenty-rodzice mogą robić ref:
import { forwardRef, useRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
Dokumentacja ostrzega – choć initial state jest używany tylko do pierwszego rendera, każdy inny jest wyrzucany, to jednak funkcje się wywołują:
function Video() {
const playerRef = useRef(new VideoPlayer());
// ...
Po prostu odrzucane są, ale wywołują się co render. Swoją drogą nie wiem, dlaczego nie można tam przekazać funkcji strzałkowej i czy by to rozwiązało problem, ale dokumentacja pokazuje taki sposób:
function Video() {
const playerRef = useRef(null);
if (playerRef.current === null) {
playerRef.current = new VideoPlayer();
}
Zawsze mi się wydawało, że w React unikamy takich rzeczy poprzez podawanie funkcji strzałkowej zwracającej np. initial state zamiast tego newVideoPlayer, ale trzymamy się standardów nakreślonych w oficjalnej dokumentacji, oni się powinni znać na rzeczy.
Podają też ciekawy przykład funkcji na potem:
function Video() {
const playerRef = useRef(null);
function getPlayer() {
if (playerRef.current !== null) {
return playerRef.current;
}
const player = new VideoPlayer();
playerRef.current = player;
return player;
}
Więcej Reacta niedługo…