React Hooks 的流行模式和反模式

2025-06-07

React Hooks 的流行模式和反模式

React 引入Hooks API已经两年多了。许多项目已经采用了新的 API,并且有足够的时间来观察这些新模式在生产环境中的运作情况。在本文中,我将带您回顾我在维护一个基于 Hooks 的大型代码库后的一些经验教训。

学习 #1. 所有标准规则均适用

钩子要求开发人员学习新的模式并遵循一些钩子规则。这有时会让人觉得新模式会抛弃所有先前的良好实践。然而,钩子只是创建可重用构建块的另一种方式。如果您要创建自定义钩子,仍然需要应用基本的软件开发实践:

  1. 单一职责原则。一个钩子应该封装单一功能。与其创建一个超级钩子,不​​如将其拆分成多个更小、更独立的钩子。
  2. 清晰定义的 API。普通函数/方法类似,如果一个钩子接受过多的参数,则表明该钩子需要重构以更好地封装。建议避免 React 组件包含过多的 props,React 钩子也应尽量减少参数数量。
  3. 可预测的行为。钩子的名称应该与其功能相对应,没有额外的意外行为。

尽管这些建议看起来非常明显,但在创建自定义钩子时遵循这些建议仍然很重要。

学习#2. 处理钩子依赖关系。

一些 React hooks 引入了“依赖项”的概念——一系列应该触发 hook 更新的元素。这在useEffect、 和 中都很useMemo常见useCallback。ESLint 中有一个规则可以帮助你管理代码中的依赖项数组,但是这条规则只能检查代码结构,而不能检查你的意图。管理 hook 依赖项是最棘手的概念,需要开发人员投入大量精力。为了提高代码的可读性和可维护性,可以减少 hook 依赖项的数量。

使用这个简单的技巧,你的基于 hooks 的代码会变得更容易。例如,让我们考虑一个自定义 hook useFocusMove

function Demo({ options }) {
  const [ref, handleKeyDown] = useFocusMove({
    isInteractive: (option) => !option.disabled,
  });
  return (
    <ul onKeyDown={handleKeyDown}>
      {options.map((option) => (
        <Option key={option.id} option={option} />
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

此自定义钩子依赖于isInteractive,可以在钩子实现内部使用:

function useFocusMove({ isInteractive }) {
  const [activeItem, setActiveItem] = useState();

  useEffect(() => {
    if (isInteractive(activeItem)) {
      focusItem(activeItem);
    }
    // update focus whenever active item changes
  }, [activeItem, isInteractive]);

  // ...other implementation details...
}
Enter fullscreen mode Exit fullscreen mode

ESLint 规则要求isInteractive将参数添加到useEffect依赖项中,因为规则不知道这个自定义钩子在哪里使用,也不知道这个参数是否会发生变化。然而,作为开发者,我们知道这个函数一旦定义,其实现就始终相同,将其添加到依赖项数组只会使代码变得混乱。标准的“工厂函数”模式可以解决这个问题:

function createFocusMove({ isInteractive }) {
  return function useFocusMove() {
    const [activeItem, setActiveItem] = useState();

    useEffect(() => {
      if (isInteractive(activeItem)) {
        focusItem(activeItem);
      }
    }, [activeItem]); // no ESLint rule violation here :)

    // ...other implementation details...
  };
}

// usage
const useFocusMove = createFocusMove({
  isInteractive: (option) => !option.disabled,
});
function Demo({ options }) {
  const [ref, handleKeyDown] = useFocusMove();
  // ...other code unchanged...
}
Enter fullscreen mode Exit fullscreen mode

这里的技巧是将运行时参数和开发时参数分开。如果在组件生命周期内某些东西发生了变化,它就是运行时依赖项,并会被添加到依赖项数组中。如果组件的依赖项一旦确定,并且在运行时永远不会改变,那么尝试工厂函数模式是一个好主意,它可以简化 hooks 依赖项的管理。

学习 #3. 重构 useEffect

useEffect钩子函数为我们在 React 组件内部实现命令式 DOM 交互提供了一个平台。有时,组件可能会变得非常复杂,而在其上添加依赖项数组会使代码的阅读和维护更加困难。这可以通过将命令式 DOM 逻辑提取到钩子代码之外来解决。例如,考虑一个钩子useTooltipPlacement

function useTooltipPosition(placement) {
  const tooltipRef = useRef();
  const triggerRef = useRef();
  useEffect(() => {
    if (placement === "left") {
      const triggerPos = triggerRef.current.getBoundingElementRect();
      const tooltipPos = tooltipPos.current.getBoundingElementRect();
      Object.assign(tooltipRef.current.style, {
        top: triggerPos.top,
        left: triggerPos.left - tooltipPos.width,
      });
    } else {
      // ... and so on of other placements ...
    }
  }, [tooltipRef, triggerRef, placement]);
  return [tooltipRef, triggerRef];
}
Enter fullscreen mode Exit fullscreen mode

如果正确使用钩子依赖项,内部代码useEffect会变得非常长,难以跟踪。为了简化操作,我们可以将效果内容提取到一个单独的函数中:

// here is the pure DOM-related logic
function applyPlacement(tooltipEl, triggerEl, placement) {
  if (placement === "left") {
    const triggerPos = tooltipEl.getBoundingElementRect();
    const tooltipPos = triggerEl.getBoundingElementRect();
    Object.assign(tooltipEl.style, {
      top: triggerPos.top,
      left: triggerPos.left - tooltipPos.width,
    });
  } else {
    // ... and so on of other placements ...
  }
}

// here is the hook binding
function useTooltipPosition(placement) {
  const tooltipRef = useRef();
  const triggerRef = useRef();
  useEffect(() => {
    applyPlacement(tooltipRef.current, triggerRef.current, placement);
  }, [tooltipRef, triggerRef, placement]);
  return [tooltipRef, triggerRef];
}
Enter fullscreen mode Exit fullscreen mode

我们的钩子现在只有一行,而且很容易跟踪依赖关系。此外,我们还获得了一个纯 DOM 的定位实现,可以在 React 之外使用和测试 :)

学习 #4. useMemo、useCallback 和过早优化

useMemo钩子文档说

你可以依赖 useMemo 进行性能优化

出于某种原因,开发人员会把这部分理解成“你必须”而不是“你可以”,并试图记住所有内容。乍一看,这似乎是个好主意,但具体细节就比较棘手了。

为了充分利用记忆化机制,需要使用React.memoPureComponent包装器来防止组件被不必要的更新。此外,还需要进行非常精细的调整和验证,以确保所有属性的更改频率都不超过应有的水平。任何一个错误的属性都可能像纸牌屋一样破坏所有记忆化机制:

现在是回顾 YAGNI 方法并将记忆化工作集中在应用程序中几个最常用的位置的好时机。在代码的其余部分,不值得使用useMemo/增加额外的复杂性useCallback。您可以使用普通函数编写更简单易读的代码,并在其优势更加明显时再应用记忆化模式。

在进行记忆化之前,我还建议您查看文章“在使用 memo() 之前”,您可以在其中找到一些记忆化的替代方法。

学习 #5. 其他 React API 仍然存在

如果你有一把锤子,所有东西看起来都像钉子

Hooks 的引入使得一些其他 React 模式变得过时。例如,useContextHook 似乎比Consumer 组件更方便。

然而,其他 React 特性仍然存在,不应被遗忘。例如,让我们来看看这个钩子代码:

function useFocusMove() {
  const ref = useRef();
  useEffect(() => {
    function handleKeyDown(event) {
      // actual implementation is extracted outside as shown in learning #3 above
      moveFocus(ref.current, event.keyCode);
    }
    ref.current.addEventListener("keydown", handleKeyDown);
    return () => ref.current.removeEventListener("keydown", handleKeyDown);
  }, []);
  return ref;
}

// usage
function Demo() {
  const ref = useFocusMove();
  return <ul ref={ref} />;
}
Enter fullscreen mode Exit fullscreen mode

这看起来像是一个合适的 Hooks 用例,但为什么我们不能将实际的事件订阅委托给 React,而不是手动操作呢?这里有一个替代方案:

function useFocusMove() {
  const ref = useRef();
  function handleKeyDown(event) {
    // actual implementation is extracted outside as shown in learning #3 above
    moveFocus(ref.current, event.keyCode);
  }
  return [ref, handleKeyDown];
}

// usage
function Demo() {
  const [ref, handleKeyDown] = useFocusMove();
  return <ul ref={ref} onKeyDown={handleKeyDown} />;
}
Enter fullscreen mode Exit fullscreen mode

新的钩子实现更短,并且具有优势,因为钩子消费者现在可以决定在哪里附加侦听器,以防他们有更复杂的 UI。

这只是一个例子,可能还有许多其他场景,但主要观点仍然是一样的——即使有钩子可用,许多 React 模式(高阶组件、渲染 props 等)仍然存在并且有意义。

结论

基本上,以上所有学习都围绕着一个基本原则:保持代码简短易读。这样,你将来就能扩展和重构它。遵循标准的编程模式,你基于钩子的代码库就能长久繁荣。

文章来源:https://dev.to/justboris/popular-patterns-and-anti-patterns-with-react-hooks-4da2
PREV
使用 Vue Formulate、S3 和 Lambda 实现更好的上传
NEXT
TypeScript 初学者入门概述和设置