paint-brush
使用 useImperativeHandle Hook 设计有效的 API 来掌握 React经过@socialdiscoverygroup
新歷史

使用 useImperativeHandle Hook 设计有效的 API 来掌握 React

经过 Social Discovery Group11m2024/12/23
Read on Terminal Reader

太長; 讀書

React 中的 useImperativeHandle 钩子允许开发人员自定义组件公开的方法和属性,从而提高灵活性和可维护性。它与 forwardRef 配合使用,为子组件提供编程接口,从而可以直接控制其行为。最佳实践包括隔离子逻辑、简化与第三方库的集成以及避免常见的陷阱(例如不正确的依赖项数组)。通过有效使用此钩子,开发人员可以创建更高效的组件并提高整体应用性能。
featured image - 使用 useImperativeHandle Hook 设计有效的 API 来掌握 React
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 允许您自定义传递给组件的 ref 将返回的内容。它与 forwardRef 协同工作,后者允许将 ref 传递给子组件。


 useImperativeHandle(ref, createHandle, [deps]);
  • ref — 传递给组件的引用。
  • createHandle — 一个返回可通过 ref 访问的对象的函数。
  • 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 时,子组件向父组件提供了一组我们自己定义的方法。


与简单使用 ref 相比,useImperativeHandle 的优势


  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

直接修改 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. 在初始化之前使用方法


从 useEffect 或事件处理程序调用通过 useImperativeHandle 提供的方法(假设 ref 已经可用)可能会导致错误 - 在调用其方法之前务必检查当前情况。


错误示例:


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 是合理的:


  • 控制子组件行为:例如,为复杂的输入组件提供焦点或重置方法。

  • 隐藏实现细节:父组件只接收所需的方法,而不是整个 ref 对象。


在使用 useImperativeHandle 之前,请问自己这些问题:

  • 是否可以使用 state、props 或 context 以声明方式解决该任务?如果是,那么这是首选方法。
  • 如果没有,并且组件需要提供外部接口,则使用useImperativeHandle。


通过掌握 useImperativeHandle 钩子,React 开发人员可以通过选择性地公开方法来创建更高效、更易于维护的组件。Social Discovery Group团队提出的技术可以帮助开发人员提高灵活性、简化组件 API 并提高整体应用性能。


作者:Sergey Levkovich,Social Discovery Group 高级软件工程师