【翻译】自定义钩子的陷阱

0 阅读3分钟

原文链接:shramko.dev/blog/react-…

理解 React 中自定义钩子的隐藏陷阱

在之前的文章中,我们探讨了 React 重渲染的原理。但今天,我们将深入剖析 React 中自定义钩子的隐藏陷阱。

在 React 中处理状态、组件重渲染和应用性能时,自定义钩子是保持组件简洁可控的强大工具。然而这种简洁性有时会掩盖性能问题,导致我们无法立即察觉。

以一个用于切换工具提示可见性的基础自定义钩子为例:

const useTooltipToggle = () => {
  const [visible, setVisible] = useState(false);

  return {
    visible,
    show: () => setVisible(true),
    hide: () => setVisible(false),
  };
};

在组件中使用这个钩子可能看起来简洁明了:

const App = () => {
  const {
    visible,
    show,
    hide
  } = useTooltipToggle();

  return (
    <div className="app-container">
      <button onMouseEnter={show} onMouseLeave={hide}>Hover me!</button>
      {visible && <Tooltip text="Hello, Tooltip!" />}
      <HeavyComputationComponent />
      <AnotherIntensiveComponent />
    </div>
  );
};

乍看之下,这种设计显得相当优雅——状态逻辑被整齐地封装在 useTooltipToggle 钩子中。然而这种做法隐藏了一个微妙的性能问题:每当工具提示可见状态发生变化时,React 都会重新渲染整个 App 组件,尽管状态变化仅涉及微小的交互操作。

为什么?你可以在这里阅读更多关于 React 重新渲染的详细说明。

隐藏状态与重新渲染陷阱

让我们扩展自定义钩子以监听滚动事件:

const useTooltipToggle = () => {
  const [scrollPosition, setScrollPosition] = useState(window.scrollY);

  useEffect(() => {
    const handleScroll = () => setScrollPosition(window.scrollY);
    window.addEventListener('scroll', handleScroll);

    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  return {
    // For example, this part of the code was simplified
    visible: false,
    show: () => {},
    hide: () => {},
  };
};

现在,每次用户滚动时,这个钩子都会在内部更新状态。即使滚动位置并未被返回或直接使用,它仍会触发状态变化,导致整个应用程序不必要地重新渲染。

嵌套钩子可能引发连锁问题

当自定义钩子间接依赖其他钩子时,问题会进一步恶化。例如,考虑另一个用于追踪鼠标移动的钩子:

const useMouseTracker = () => {
  const [mousePosition, setMousePosition] = useState({
    x: 0,
    y: 0
  });

  useEffect(() => {
    const updateMousePosition = (e) => setMousePosition({
      x: e.clientX,
      y: e.clientY
    });
    window.addEventListener('mousemove', updateMousePosition);

    return () => window.removeEventListener('mousemove', updateMousePosition);
  }, []);

  return null;
};

const useTooltipToggle = () => {
  useMouseTracker();

  return {
    // For example, this part of the code was simplified
    visible: false,
    show: () => {},
    hide: () => {},
  };
};

即使 useTooltipToggle 未直接使用鼠标位置状态,由于状态在嵌套钩子内部被更新,App 组件仍会在每次鼠标移动时重新渲染。

将状态逻辑隐藏在钩子中并不能消除其性能影响。解决之道不在于隐藏复杂性,而在于智能管理并隔离状态。

如何解决此问题

为避免此类问题,请将状态逻辑封装在更小的组件中:

const Tooltip = () => {
  const {
    visible,
    show,
    hide
  } = useTooltipToggle();

  return (
    <>
      <button onMouseEnter={show} onMouseLeave={hide}>Hover me!</button>
      {visible && <Tooltip text="Hello, Tooltip!" />}
    </>
  );
};

这确保了仅重新渲染所需的最小区域,避免了整个应用程序的不必要更新。

在此方案中,ToolTip 逻辑从应用程序层下移至独立组件。我在本文中详细阐述了这种方法。

核心要点

关键经验:在自定义钩子中谨慎处理状态封装。保持状态管理的局部化与聚焦性,以优化 React 应用的性能与响应速度。

附注:备忘优化及其他性能优化将在后续文章中探讨。您可先阅读关于元素、子元素作为 props以及重新渲染 的相关内容。