paint-brush
Освойте React, разрабатывая эффективные API с помощью хука useImperativeHandleк@socialdiscoverygroup
Новая история

Освойте React, разрабатывая эффективные API с помощью хука useImperativeHandle

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

Слишком долго; Читать

Хук useImperativeHandle в React позволяет разработчикам настраивать методы и свойства, предоставляемые компонентом, что повышает гибкость и удобство обслуживания. Он работает с forwardRef, чтобы предоставить программный интерфейс для дочерних компонентов, позволяя напрямую контролировать их поведение. Лучшие практики включают изоляцию дочерней логики, упрощение интеграции со сторонними библиотеками и избежание распространенных ошибок, таких как неправильные массивы зависимостей. Эффективно используя этот хук, разработчики могут создавать более эффективные компоненты и повышать общую производительность приложения.
featured image - Освойте React, разрабатывая эффективные API с помощью хука useImperativeHandle
Social Discovery Group HackerNoon profile picture
0-item
1-item

В современной разработке React хук useImperativeHandle является мощным способом персонализации открытого значения компонента и обеспечивает больший контроль над его внутренними методами и свойствами. В результате более эффективные API компонентов могут улучшить гибкость и удобство обслуживания продукта.


В этой статье команда Social Discovery Group рассматривает лучшие практики эффективного использования useImperativeHandle для улучшения компонентов React.


React предоставляет множество хуков (на момент написания этой статьи в официальной документации описано 17 хуков) для управления состоянием, эффектами и взаимодействиями между компонентами.


Среди них useImperativeHandle — полезный инструмент для создания программного интерфейса (API) для дочерних компонентов, который был добавлен в React, начиная с версии 16.8.0.


useImperativeHandle позволяет вам настраивать то, что будет возвращено ссылкой, переданной компоненту. Он работает в тандеме с forwardRef, который позволяет передавать ссылку дочернему компоненту.


 useImperativeHandle(ref, createHandle, [deps]);
  • ref — ссылка, переданная компоненту.
  • createHandle — функция, возвращающая объект, который станет доступен через ссылку.
  • deps — массив зависимостей.


Этот хук позволяет осуществлять внешний контроль поведения компонента, что может быть полезно в определенных ситуациях, например, при работе со сторонними библиотеками, сложными анимациями или компонентами, требующими прямого доступа к методам. Однако его следует использовать осторожно, поскольку он нарушает декларативный подход React.

Базовый пример


Давайте представим, что нам нужно манипулировать DOM дочернего компонента. Вот пример того, как это сделать с помощью 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> ); }

И вот как этого можно добиться с помощью 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> ); }


Как видно из приведенных выше примеров, при использовании useImperativeHandle дочерний компонент предоставляет родительскому набор методов, которые мы определяем сами.


Преимущества использованияImperativeHandle по сравнению с простым использованием ref


  1. Изолирует логику дочерних компонентов: позволяет предоставлять родительским компонентам только необходимые методы.
  2. Упрощает интеграцию: упрощает интеграцию компонентов React с библиотеками, требующими прямого доступа к DOM, такими как Lottie или Three.js.

Расширенные сценарии

Использование useImperativeHandle в расширенных сценариях, например, в примерах с анимацией, позволяет изолировать сложное поведение внутри компонента. Это делает родительский компонент более простым и читабельным, особенно при работе с библиотеками анимации или звука.



 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> ); }


В этом примере дочерний компонент возвращает методы startAnimation и stopAnimation, которые инкапсулируют в себе сложную логику.


Распространенные ошибки и подводные камни

1. Неправильно заполненный массив зависимостей

Ошибка не всегда заметна сразу. Например, родительский компонент может часто менять props, и вы можете столкнуться с ситуацией, когда устаревший метод (с устаревшими данными) продолжает использоваться.


Пример ошибки:

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 );


Правильный подход:

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. Отсутствует массив зависимостей


Если массив зависимостей не указан, React будет считать, что объект в useImperativeHandle должен быть пересоздан при каждом рендере. Это может вызвать значительные проблемы с производительностью, особенно если внутри хука выполняются «тяжелые» вычисления.


Пример ошибки:

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


Правильный подход:

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. Изменение ссылки внутри useImperativeHandle

Прямая модификация ref.current нарушает поведение React. Если React попытается обновить ref, это может привести к конфликтам или неожиданным ошибкам.


Пример ошибки:

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


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

Правильный подход:

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


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


  1. Использование методов до их инициализации


Вызов методов, предоставляемых через useImperativeHandle из useEffect или обработчиков событий, при условии, что ссылка уже доступна, может привести к ошибкам — всегда проверяйте current перед вызовом его методов.


Пример ошибки:


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

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


Правильный подход:

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


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


  1. Проблемы синхронизации между анимацией и состоянием

Если useImperativeHandle возвращает методы, которые синхронно изменяют состояние (например, запуская анимацию и одновременно изменяя стили), это может привести к «разрыву» между визуальным состоянием и внутренней логикой. Обеспечьте согласованность между состоянием и визуальным поведением, например, с помощью эффектов (useEffect).


Пример ошибки:

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(); }, }));


Правильный подход:


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

Заключение

Использование useImperativeHandle оправдано в следующих ситуациях:


  • Управление поведением дочерних компонентов: например, для предоставления метода фокусировки или сброса для сложного компонента ввода.

  • Скрытие деталей реализации: родительский компонент получает только необходимые ему методы, а не весь объект ссылки.


Перед использованием useImperativeHandle задайте себе следующие вопросы:

  • Можно ли решить задачу декларативно, используя состояние, реквизиты или контекст? Если да, то это предпочтительный подход.
  • Если нет, а компонент должен предоставлять внешний интерфейс, используйте useImperativeHandle.


Освоив хук useImperativeHandle, разработчики React могут создавать более эффективные и поддерживаемые компоненты, выборочно раскрывая методы. Методы, изложенные командой Social Discovery Group, могут помочь разработчикам повысить гибкость, оптимизировать API компонентов и повысить общую производительность приложений.


Автор: Сергей Левкович, старший инженер-программист в Social Discovery Group