В современной разработке React хук useImperativeHandle является мощным способом персонализации открытого значения компонента и обеспечивает больший контроль над его внутренними методами и свойствами. В результате более эффективные API компонентов могут улучшить гибкость и удобство обслуживания продукта.
В этой статье команда Social Discovery Group рассматривает лучшие практики эффективного использования useImperativeHandle для улучшения компонентов React.
React предоставляет множество хуков (на момент написания этой статьи в официальной документации описано 17 хуков) для управления состоянием, эффектами и взаимодействиями между компонентами.
Среди них useImperativeHandle — полезный инструмент для создания программного интерфейса (API) для дочерних компонентов, который был добавлен в React, начиная с версии 16.8.0.
useImperativeHandle позволяет вам настраивать то, что будет возвращено ссылкой, переданной компоненту. Он работает в тандеме с forwardRef, который позволяет передавать ссылку дочернему компоненту.
useImperativeHandle(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 дочерний компонент предоставляет родительскому набор методов, которые мы определяем сами.
Использование 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, которые инкапсулируют в себе сложную логику.
Ошибка не всегда заметна сразу. Например, родительский компонент может часто менять 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 );
Правильный подход:
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
Правильный подход:
useImperativeHandle(ref, () => { // There is might be a difficult task console.log("useImperativeHandle calculated again"); return { focus: () => {} } }, []); // Array of dependencies is correct
Прямая модификация ref.current нарушает поведение React. Если React попытается обновить ref, это может привести к конфликтам или неожиданным ошибкам.
Пример ошибки:
useImperativeHandle(ref, () => { // ref is mutated directly ref.current = { customMethod: () => console.log("Error") }; });
Правильный подход:
useImperativeHandle(ref, () => ({ customMethod: () => console.log("Correct"), }));
Вызов методов, предоставляемых через useImperativeHandle из useEffect или обработчиков событий, при условии, что ссылка уже доступна, может привести к ошибкам — всегда проверяйте current перед вызовом его методов.
Пример ошибки:
const increment = useCallback(() => { childRef.current.increment(); }, [])
Правильный подход:
const increment = useCallback(() => { if (childRef.current?.increment) { childRef.current.increment() } }, [])
Если useImperativeHandle возвращает методы, которые синхронно изменяют состояние (например, запуская анимацию и одновременно изменяя стили), это может привести к «разрыву» между визуальным состоянием и внутренней логикой. Обеспечьте согласованность между состоянием и визуальным поведением, например, с помощью эффектов (useEffect).
Пример ошибки:
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(); }, }));
Правильный подход:
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, разработчики React могут создавать более эффективные и поддерживаемые компоненты, выборочно раскрывая методы. Методы, изложенные командой Social Discovery Group, могут помочь разработчикам повысить гибкость, оптимизировать API компонентов и повысить общую производительность приложений.
Автор: Сергей Левкович, старший инженер-программист в Social Discovery Group