在现代 React 开发中,useImperativeHandle 钩子是一种强大的方法来个性化组件的暴露值,并对其内部方法和属性提供更多的控制。因此,更高效的组件 API 可以提高产品的灵活性和可维护性。
在本文中, Social Discovery Group团队深入探讨了有效使用 useImperativeHandle 来增强 React 组件的最佳实践。
React 提供了许多钩子(截至本文撰写时,官方文档描述了 17 个钩子)用于管理状态、效果和组件之间的交互。
其中,useImperativeHandle 是一个用于为子组件创建编程接口(API)的有用工具,从 React 16.8.0 版本开始被添加到其中。
useImperativeHandle 允许您自定义传递给组件的 ref 将返回的内容。它与 forwardRef 协同工作,后者允许将 ref 传递给子组件。
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"), }));
从 useEffect 或事件处理程序调用通过 useImperativeHandle 提供的方法(假设 ref 已经可用)可能会导致错误 - 在调用其方法之前务必检查当前情况。
错误示例:
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 是合理的:
控制子组件行为:例如,为复杂的输入组件提供焦点或重置方法。
隐藏实现细节:父组件只接收所需的方法,而不是整个 ref 对象。
在使用 useImperativeHandle 之前,请问自己这些问题:
通过掌握 useImperativeHandle 钩子,React 开发人员可以通过选择性地公开方法来创建更高效、更易于维护的组件。Social Discovery Group团队提出的技术可以帮助开发人员提高灵活性、简化组件 API 并提高整体应用性能。
作者:Sergey Levkovich,Social Discovery Group 高级软件工程师