W nowoczesnym rozwoju React hook useImperativeHandle jest potężnym sposobem personalizacji wartości komponentu i daje większą kontrolę nad jego wewnętrznymi metodami i właściwościami. W rezultacie bardziej wydajne API komponentów mogą poprawić elastyczność i łatwość utrzymania produktu.
W tym artykule zespół Social Discovery Group zagłębia się w najlepsze praktyki efektywnego wykorzystania useImperativeHandle do ulepszania komponentów React.
React udostępnia wiele haków (oficjalna dokumentacja opisuje 17 haków w chwili pisania tego tekstu) umożliwiających zarządzanie stanem, efektami i interakcjami między komponentami.
Wśród nich useImperativeHandle to przydatne narzędzie do tworzenia interfejsu programistycznego (API) dla komponentów podrzędnych, które zostało dodane do Reacta od wersji 16.8.0.
useImperativeHandle pozwala dostosować, co zostanie zwrócone przez ref przekazane do komponentu. Działa w tandemie z forwardRef, który umożliwia przekazanie ref do komponentu podrzędnego.
useImperativeHandle(ref, createHandle, [deps]);
Ten hak umożliwia zewnętrzną kontrolę zachowania komponentu, co może być przydatne w pewnych sytuacjach, takich jak praca z bibliotekami stron trzecich, złożonymi animacjami lub komponentami wymagającymi bezpośredniego dostępu do metod. Należy go jednak używać ostrożnie, ponieważ łamie deklaratywne podejście React.
Wyobraźmy sobie, że musimy manipulować DOM komponentu podrzędnego. Oto przykład, jak to zrobić za pomocą ref.
import React, { forwardRef, useRef } from 'react'; const CustomInput = forwardRef((props, ref) => { // Use forwardRef to pass the ref to the input element return <input ref={ref} {...props} />; }); export default function App() { const inputRef = useRef(); const handleFocus = () => { inputRef.current.focus(); // Directly controlling the input }; const handleClear = () => { inputRef.current.value = ''; // Directly controlling the input value }; return ( <div> <CustomInput ref={inputRef} /> <button onClick={handleFocus}>Focus</button> <button onClick={handleClear}>Clear</button> </div> ); }
A oto jak można to osiągnąć używając useImperativeHandle.
import React, { useImperativeHandle, forwardRef, useRef } from 'react'; const CustomInput = forwardRef((props, ref) => { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); }, clear: () => { inputRef.current.value = ''; }, })); return <input ref={inputRef} {...props} />; }); export default function App() { const inputRef = useRef(); return ( <div> <CustomInput ref={inputRef} /> <button onClick={inputRef.current.focus}>Focus</button> <button onClick={inputRef.current.clear}>Clear</button> </div> ); }
Jak widać w powyższych przykładach, podczas korzystania z useImperativeHandle komponent podrzędny udostępnia komponentowi nadrzędnemu zestaw metod, które sami definiujemy.
Używanie useImperativeHandle w zaawansowanych scenariuszach, takich jak przykłady z animacją, pozwala na izolowanie złożonych zachowań wewnątrz komponentu. Dzięki temu komponent nadrzędny staje się prostszy i bardziej czytelny, zwłaszcza podczas pracy z bibliotekami animacji lub dźwięków.
import React, { useRef, useState, useImperativeHandle, forwardRef, memo } from "react"; import { Player } from '@lottiefiles/react-lottie-player' import animation from "./animation.json"; const AnimationWithSound = memo( forwardRef((props, ref) => { const [isAnimating, setIsAnimating] = useState(false); const animationRef = useRef(null); const targetDivRef = useRef(null); useImperativeHandle( ref, () => ({ startAnimation: () => { setIsAnimating(true); animationRef.current?.play() updateStyles("start"); }, stopAnimation: () => { animationRef.current?.stop() updateStyles("stop"); }, }), [] ); const updateStyles = (action) => { if (typeof window === 'undefined' || !targetDivRef.current) return; if (action === "start") { if (targetDivRef.current.classList.contains(styles.stop)) { targetDivRef.current.classList.remove(styles.stop); } targetDivRef.current.classList.add(styles.start); } else if (action === "stop") { if (targetDivRef.current.classList.contains(styles.start)) { targetDivRef.current.classList.remove(styles.start); } targetDivRef.current.classList.add(styles.stop); } }; return ( <div> <Player src={animation} loop={isAnimating} autoplay={false} style={{width: 200, height: 200}} ref={animationRef} /> <div ref={targetDivRef} className="target-div"> This div changes styles </div> </div> ); }) ); export default function App() { const animationRef = useRef(); const handleStart = () => { animationRef.current.startAnimation(); }; const handleStop = () => { animationRef.current.stopAnimation(); }; return ( <div> <h1>Lottie Animation with Sound</h1> <AnimationWithSound ref={animationRef} /> <button onClick={handleStart}>Start Animation</button> <button onClick={handleStop}>Stop Animation</button> </div> ); }
W tym przykładzie komponent podrzędny zwraca metody startAnimation i stopAnimation, które hermetyzują w sobie złożoną logikę.
Błąd nie zawsze jest zauważalny od razu. Na przykład, komponent nadrzędny może często zmieniać właściwości i możesz napotkać sytuację, w której nadal używana jest przestarzała metoda (z nieaktualnymi danymi).
Przykładowy błąd:
https://use-imperative-handle.levkovich.dev/deps-is-not-correct/wrong
const [count, setCount] = useState(0); const increment = useCallback(() => { console.log("Current count in increment:", count); // Shows old value setCount(count + 1); // Are using the old value of count }, [count]); useImperativeHandle( ref, () => ({ increment, // Link to the old function is used }), [] // Array of dependencies do not include increment function );
Właściwe podejście:
const [count, setCount] = useState(0); useImperativeHandle( ref, () => ({ increment, }), [increment] // Array of dependencies include increment function );
2. Brakująca tablica zależności
Jeśli tablica zależności nie jest podana, React założy, że obiekt w useImperativeHandle powinien być tworzony ponownie przy każdym renderowaniu. Może to powodować znaczne problemy z wydajnością, zwłaszcza jeśli w haku wykonywane są „ciężkie” obliczenia.
Przykładowy błąd:
useImperativeHandle(ref, () => { // There is might be a difficult task console.log("useImperativeHandle calculated again"); return { focus: () => {} } }); // Array of dependencies is missing
Właściwe podejście:
useImperativeHandle(ref, () => { // There is might be a difficult task console.log("useImperativeHandle calculated again"); return { focus: () => {} } }, []); // Array of dependencies is correct
Bezpośrednia modyfikacja ref.current zakłóca zachowanie React. Jeśli React próbuje zaktualizować ref, może to prowadzić do konfliktów lub nieoczekiwanych błędów.
Przykładowy błąd:
useImperativeHandle(ref, () => { // ref is mutated directly ref.current = { customMethod: () => console.log("Error") }; });
Właściwe podejście:
useImperativeHandle(ref, () => ({ customMethod: () => console.log("Correct"), }));
Wywoływanie metod udostępnionych za pośrednictwem useImperativeHandle z useEffect lub obsługi zdarzeń, przy założeniu, że odwołanie jest już dostępne, może prowadzić do błędów — zawsze sprawdzaj current przed wywołaniem jego metod.
Przykładowy błąd:
const increment = useCallback(() => { childRef.current.increment(); }, [])
Właściwe podejście:
const increment = useCallback(() => { if (childRef.current?.increment) { childRef.current.increment() } }, [])
Jeśli useImperativeHandle zwraca metody, które synchronicznie zmieniają stan (np. uruchamiając animację i jednocześnie modyfikując style), może to spowodować „lukę” między stanem wizualnym a logiką wewnętrzną. Zapewnij spójność między stanem a zachowaniem wizualnym, na przykład, używając efektów (useEffect).
Przykładowy błąd:
useImperativeHandle(ref, () => ({ startAnimation: () => { setState("running"); // Animation starts before the state changes lottieRef.current.play(); }, stopAnimation: () => { setState("stopped"); // Animation stops before the state changes lottieRef.current.stop(); }, }));
Właściwe podejście:
useEffect(() => { if (state === "running" && lottieRef.current) { lottieRef.current.play(); } else if (state === "stopped" && lottieRef.current) { lottieRef.current.stop(); } }, [state]); // Triggered when the state changes useImperativeHandle( ref, () => ({ startAnimation: () => { setState("running"); }, stopAnimation: () => { setState("stopped"); }, }), [] );
Użycie useImperativeHandle jest uzasadnione w następujących sytuacjach:
Sterowanie zachowaniem komponentu podrzędnego: Na przykład w celu zapewnienia metody fokusu lub resetowania dla złożonego komponentu wejściowego.
Ukrywanie szczegółów implementacji: Komponent nadrzędny otrzymuje tylko potrzebne mu metody, a nie cały obiekt ref.
Zanim użyjesz useImperativeHandle, zadaj sobie następujące pytania:
Opanowując hook useImperativeHandle, programiści React mogą tworzyć bardziej wydajne i łatwe w utrzymaniu komponenty, selektywnie udostępniając metody. Techniki opracowane przez zespół Social Discovery Group mogą pomóc programistom poprawić elastyczność, usprawnić interfejsy API komponentów i zwiększyć ogólną wydajność aplikacji.
Autor: Sergey Levkovich, starszy inżynier oprogramowania w Social Discovery Group