paint-brush
Opanuj React, projektując efektywne interfejsy API za pomocą hooka useImperativeHandleprzez@socialdiscoverygroup
Nowa historia

Opanuj React, projektując efektywne interfejsy API za pomocą hooka useImperativeHandle

przez Social Discovery Group11m2024/12/23
Read on Terminal Reader

Za długo; Czytać

Hook useImperativeHandle w React pozwala programistom dostosowywać metody i właściwości udostępniane przez komponent, zwiększając elastyczność i łatwość utrzymania. Działa z forwardRef, aby zapewnić programowy interfejs dla komponentów podrzędnych, umożliwiając bezpośrednią kontrolę nad ich zachowaniem. Najlepsze praktyki obejmują izolowanie logiki podrzędnej, uproszczenie integracji z bibliotekami innych firm i unikanie typowych pułapek, takich jak nieprawidłowe tablice zależności. Dzięki efektywnemu wykorzystaniu tego hooka programiści mogą tworzyć wydajniejsze komponenty i poprawiać ogólną wydajność aplikacji.
featured image - Opanuj React, projektując efektywne interfejsy API za pomocą hooka useImperativeHandle
Social Discovery Group HackerNoon profile picture
0-item
1-item

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]);
  • ref — referencja przekazana do komponentu.
  • createHandle — funkcja zwracająca obiekt, który stanie się dostępny poprzez odwołanie.
  • deps — tablica zależności.


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.

Podstawowy przykład


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.


Zalety korzystania z ImperativeHandle w porównaniu z prostym użyciem ref


  1. Izoluje logikę komponentów podrzędnych: umożliwia dostarczanie komponentom nadrzędnym wyłącznie wymaganych metod.
  2. Ułatwia integrację: ułatwia integrację komponentów React z bibliotekami wymagającymi bezpośredniego dostępu do DOM, takimi jak Lottie lub Three.js.

Zaawansowane scenariusze

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ę.


Typowe błędy i pułapki

1. Nieprawidłowo wypełniona tablica zależności

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:

https://use-imperative-handle.levkovich.dev/deps-is-not-correct/correct

 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:

https://use-imperative-handle.levkovich.dev/deps-empty/correct

 useImperativeHandle(ref, () => { // There is might be a difficult task console.log("useImperativeHandle calculated again"); return { focus: () => {} } }, []); // Array of dependencies is correct


  1. Modyfikowanie ref wewnątrz useImperativeHandle

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:

https://use-imperative-handle.levkovich.dev/ref-modification/wrong


 useImperativeHandle(ref, () => { // ref is mutated directly ref.current = { customMethod: () => console.log("Error") }; });

Właściwe podejście:

https://use-imperative-handle.levkovich.dev/ref-modification/correct


 useImperativeHandle(ref, () => ({ customMethod: () => console.log("Correct"), }));


  1. Korzystanie z metod przed ich zainicjowaniem


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:


https://use-imperative-handle.levkovich.dev/before-init/wrong

 const increment = useCallback(() => { childRef.current.increment(); }, [])


Właściwe podejście:

https://use-imperative-handle.levkovich.dev/before-init/correct


 const increment = useCallback(() => { if (childRef.current?.increment) { childRef.current.increment() } }, [])


  1. Problemy z synchronizacją między animacjami i stanem

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:

https://use-imperative-handle.levkovich.dev/state-animation-sync/wrong


 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:


https://use-imperative-handle.levkovich.dev/state-animation-sync/correct

 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"); }, }), [] );

Wniosek

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:

  • Czy zadanie można rozwiązać deklaratywnie, używając stanu, rekwizytów lub kontekstu? Jeśli tak, to jest to preferowane podejście.
  • W przeciwnym razie, a komponent musi zapewnić interfejs zewnętrzny, należy użyć useImperativeHandle.


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