关于 React Hooks 你需要知道的一切 更高级的 hooks 很少使用的 hooks 总结

2025-05-28

关于 React Hooks 你需要知道的一切

更高级的钩子

很少使用的钩子

综上所述

React 刚刚宣布了一项新功能:Hooks。它是一组全新的 API,它支持以强大的新方式在组件之间共享状态逻辑,无需大量重写即可优化性能,并获得 Redux 式关注点分离的一些好处等等。它们还兑现了 React 团队多年前做出的承诺——状态函数组件。早在 2016 年 4 月,Dan Abramov 在 Reactiflux 的问答中就曾提出过使用函数组件状态的可能性。

Dan Abramov 问答记录截图。高亮部分内容如下:

等了好久,它们终于来了!不仅仅是状态,总共还有 11 个新函数,它们应该能够实现我们今天使用的类和生命周期的全部功能。

  • useState
  • useEffect
  • useContext
  • useCallback
  • useMemo
  • React.memo(不是钩子,而是新的)
  • useReducer
  • useRef
  • useLayoutEffect
  • useImperativeMethods
  • useMutationEffect

让我们看看它们各自的用途。

useState

有状态函数组件通过新功能启用useState

import { useState } from "react";

const SomeComponent = props => {
  const [state, setState] = useState(initialState);
  return (
    <div>
      {state}
      <input onChange={e => setState(e.target.value)} />
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

如果您曾经使用过库recompose,那么这个 API 可能看起来很熟悉。useState它接受初始状态作为参数,并返回当前状态和一个更新函数。setState它返回的函数与类组件使用的函数几乎相同——它可以接受一个回调函数,该回调函数将当前状态作为参数,但不会自动合并顶级对象键。

每次调用useState都会与一个组件配对,其状态在渲染过程中保持不变。这意味着您可以useState在单个函数组件中多次调用以获取多个独立的状态值。由于setState返回值不局限于单个组件,我们可以定义独立于组件的状态行为。这为抽象状态逻辑提供了强大的新方法。

让我们看一个我在几个项目中遇到的例子:在多个组件中管理排序状态。我发现表格组件暴露的 API 不够灵活,所以我倾向于一次性编写数据表。我目前的项目中有一些用于管理按哪个键排序以及按哪个方向排序的代码,这些代码被复制粘贴到几个不同的组件中。有了useState,我们就能够将其定义为单独的模块。

const useSort = (someArray, initialSortKey) => {
  const [state, setState] = useState({
    isAscending: false,
    sortKey: initialSortKey
  });

  // Let's pretend `makeSortComparator` exists for simplicity
  const comparator = makeSortComparator(state);
  const sortedData = someArray.slice().sort(comparator);

  return {
    ...state,
    sortedData,
    toggleAscending: () =>
      setState(state => ({
        ...state,
        isAscending: !state.isAscending
      })),
    setSortKey: sortKey =>
      setState(state => ({ ...state, sortKey }))
  };
};

Enter fullscreen mode Exit fullscreen mode

现在,我们在数据表组件中有了可复用的方法。我们拥有了一个简单的 API,可以跨多个不同的表使用,每个组件都拥有各自独立的状态。

const SomeTable = ({ data }) => {
  const { sortedData, ...sortControls } = useSort(
    data,
    "id"
  );
  return (
    <table>
      <TableHeading {...sortControls} />
      <tbody>
        {sortedData.map(datum => <TableRow {...datum} />)}
      </tbody>
    </table>
  );
};
Enter fullscreen mode Exit fullscreen mode

请注意:React 团队强烈建议在这些类型的模块名称前添加前缀,use以便清晰地表明其提供的行为类型。有关编写自定义 hooks 的更多信息,请参阅完整文档。

我对这种共享功能的新方式感到非常兴奋。它在各方面都比 HOC 轻量级得多;需要编写的代码更少,需要挂载的组件更少,而且注意事项也更少。查看API 文档了解所有详细信息。

useEffect

许多组件在挂载或重新渲染时需要触发不同类型的效果。获取数据、订阅事件以及与页面其他部分进行命令式交互都是常见的例子。但处理这些效果的代码最终分散在componentDidMountcomponentDidUpdate和 中componentWillUnmount

如果你想在 props 发生变化时运行相同的效果,你要么添加一堆比较操作,componentDidUpdate要么在key组件上设置一个。使用一个key可以简化代码,但它会将效果的控制权分散到另一个文件中——完全不受组件控制!

useEffect简化了所有这些情况。命令式交互是每次渲染后运行的简单函数。

const PageTemplate = ({ title, children }) => {
  useEffect(() => {
    document.title = title;
  });
  return (
    <div>
      <h1>{title}</h1>
      {children}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

对于数据获取和其他你不想不必要发生的交互,你可以传递一个值数组。只有当其中一个值发生变化时,才会执行相应的效果。

const ThingWithExternalData = ({ id, sort }) => {
  const [state, setState] = useState({});
  useEffect(() => {
    axios
      .get(`/our/api/${id}?sortBy=${sort}`)
      .then(({ data }) => setState(data));
  }, [id, sort]);
  return <pre>{JSON.stringify(state, null, 2)}</pre>;
};
Enter fullscreen mode Exit fullscreen mode

订阅和其他需要在组件卸载时进行某种清理的效果可以返回一个函数来运行。

const ThingWithASubscription = () => {
  const [state, setState] = useState({});
  useEffect(() => {
    someEventSource.subscribe(data => setState(data));
    return () => {
      someEventSource.unsubscribe();
    };
  });
  return <pre>{JSON.stringify(state, null, 2)}</pre>;
};
Enter fullscreen mode Exit fullscreen mode

这太强大了。就像 一样useState,它们可以定义为单独的模块——这不仅将这些复杂效果所需的所有代码放在一个位置,还可以在多个组件之间共享。与 结合使用useState,这是一种优雅的方式,可以跨组件概括诸如加载状态或订阅之类的逻辑。

useContext

context API 非常棒,与之前的版本相比,可用性有了显著提升。它使 context 从文档中“你可能不应该使用它”的警告提升到了 API 的可接受范围。然而,context 的使用可能比较繁琐。它必须作为渲染 prop 来使用,而这种模式组合起来并不优雅。如果你需要从多个不同的渲染 prop 中获取值,你很快就会发现自己的缩进变得非常繁琐。

useContext向前迈出了一大步。它接受现有React.createContext函数(也就是你用来.Consumer作为渲染 prop 的那个函数)创建的值,并从上下文提供程序返回当前值。每当上下文值发生变化时,组件就会重新渲染,就像 state 或 props 一样。

// An exported instance of `React.createContext()`
import SomeContext from "./SomeContext";

const ThingWithContext = () => {
  const ourData = useContext(SomeContext);
  return <pre>{JSON.stringify(ourData, null, 2)}</pre>;
};
Enter fullscreen mode Exit fullscreen mode

这消除了我对 context 的最后一个抱怨。这个 API 极其简单直观,将成为在应用程序中传递状态的强大方法。

更高级的钩子

上面提到的 3 个钩子被认为是基础钩子。你可以只使用useStateuseEffect和 来编写整个应用程序useContext——实际上,你甚至可以只用前两个。后面的钩子提供了一些优化,以及一些你可能在应用程序中从未遇到过的小众实用功能。

useCallback

React 有许多优化措施,这些措施都依赖于 props 在渲染过程中保持不变。打破这一现状最简单的方法之一就是将回调函数定义成内联函数。这并不是说内联函数会导致性能问题——在很多情况下,它并不会造成影响。然而,当你开始优化并找出导致频繁重新渲染的原因时,你可能会发现内联函数定义是许多不必要的 props 更改的根源。

在当前 API 中,将内联函数更改为在渲染过程中不会更改的函数可能是一项重大更改。对于函数组件,这意味着将其重写为一个类(包含所有必要的更改),并将该函数定义为类方法。useCallback通过记忆传递给它的函数,可以提供一种简单的方法来优化这些函数,同时最大程度地减少对代码的影响。就像 一样useEffect,我们可以告诉它它所依赖的值,这样它就不会进行不必要的更改。

import doSomething from "./doSomething";

const FrequentlyRerenders = ({ id }) => {
  return (
    <ExpensiveComponent
      onEvent={useCallback(() => doSomething(id), [id])}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

这是可用性方面又一次令人兴奋的改进。过去需要对组件进行大规模重写的工作,现在可以通过 React 中的函数直接完成。

useMemo

说到优化,还有另一个让我兴奋的钩子。很多时候,我需要根据提供给组件的 props 计算派生数据。它可能是将对象数组映射到略有不同的形式,将数据数组合并为单个值,或者进行排序或过滤。通常情况下,render这种处理在逻辑上应该发生,但每当其他 props 或状态发生变化时,它就会不必要地运行。

输入useMemo。它与 密切相关useCallback,但用于优化数据处理。它具有相同的 API,用于定义它所依赖的值,例如useEffectuseCallback

const ExpensiveComputation = ({
  data,
  sortComparator,
  filterPredicate
}) => {
  const transformedData = useMemo(
    () => {
      return data
        .filter(filterPredicate)
        .sort(sortComparator);
    },
    [data, sortComparator, filterPredicate]
  );
  return <Table data={data} />;
};
Enter fullscreen mode Exit fullscreen mode

我对此感到兴奋,原因与……有很多相似之处useCallback。以前,优化这类处理通常意味着将逻辑提取到一个单独的函数中并对其进行记忆。由于记忆工具通常依赖于函数参数来使记忆无效,这意味着创建一个纯函数。这种重构最终可能会过于繁琐,因此最终只会解决最极端的性能问题。这个钩子应该有助于避免“千刀万剐”式的性能问题。

React.memo

这不是一个钩子,而是一个新 API 和一项重要的优化。记忆计算并确保 props 不会不必要地更改对性能有益,但两者结合使用时会更有效——这两者都不适用于函数组件shouldComponentUpdatePureComponent

React.memo是一个新函数,其行为类似于PureComponentfor 函数。它会比较 props 值,并且仅在 props 发生变化时重新渲染。它不会像 PureComponent 一样比较 state 或 context。它可以接受第二个参数,以便您可以自定义 props 的比较,但与 有一个重要的区别shouldComponentUpdate:它只接收 props。由于useState它不提供单个 state 对象,因此无法用于此比较。

useReducer

这个钩子对生态系统有着有趣的影响。Reducer/Action 模式是 Redux 最强大的优势之一。它鼓励将 UI 建模为状态机,并具有清晰定义的状态和转换。然而,使用 Redux 的挑战之一是将所有内容粘合在一起。Action 创建器、将哪些组件添加到connect()mapStateToProps使用选择器、协调异步行为……Redux 之上还有一大堆相关的代码和库,可能会让人不知所措。

useReducer结合上下文可用性改进、新的计算记忆技术以及用于运行 effect 的钩子,它能够以更低的概念开销实现许多与 Redux 相同的优势。我个人从未被 Redux 所谓的样板问题所困扰,但考虑到这些钩子将如何组合,我对如何在应用程序中定义和限定功能范围感到兴奋。

useRef

有时在编写组件时,我们会得到一些需要跟踪的信息,但又不想在信息发生变化时重新渲染。最常见的例子是对我们创建的 DOM 节点的引用,例如,input我们需要跟踪光标位置或强制获取焦点的节点。对于类组件,我们会直接将它们分配给 上的属性this,但函数组件没有上下文,我们无法通过这种方式引用它们。

useRef为这些情况提供了一种机制。它会创建一个对象,该对象在组件挂载期间一直存在,并将分配的值公开为.current属性。

直接来自文档(和常见问题解答):

// DOM node ref example
function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

// An arbitrary instance property
function Timer() {
  const intervalRef = useRef();

  useEffect(() => {
    const id = setInterval(() => {
      // ...
    });
    intervalRef.current = id;
    return () => {
      clearInterval(intervalRef.current);
    };
  });


}
Enter fullscreen mode Exit fullscreen mode

与在类组件中使用实例属性相比,此代码更为冗长,但以这种方式存储值的情况相对较少。

很少使用的钩子

上面提到的钩子涵盖了我在编写应用程序时遇到的所有用例。阅读其余钩子的文档后,我理解了它们存在的原因,并且我确信我正在使用可以实现它们的库,但我并不打算在应用程序代码中使用它们。

useLayoutEffect

如果我使用这三个中的任何一个,我预计它会是useLayoutEffect。当你需要在 DOM 发生变异之后,但在浏览器绘制新布局之前读取计算样式时,推荐使用这个钩子。

至关重要的是,这让你有机会在应用动画时,最大限度地减少视觉伪影或浏览器渲染性能问题。这是react-flip-move目前使用的方法,它是一个在项目改变位置时非常出色的过渡库,但在某些情况下,我可能需要自己使用它。

useImperativeMethods

据我所知,这个钩子与 相对应forwardRef,这是一种让库能够传递ref原本会被吞噬的属性的机制。对于像 Material UI、React Bootstrap 这样的组件库,或者像 styled-components 这样的 CSS-in-JS 工具来说,这是一个问题,但我还没有遇到过需要自己解决这个问题的情况。

useMutationEffect

这是让我最费解的一个钩子。它会在 React 使用 的结果修改 DOM 之前立即运行render,但当useLayoutEffect你需要读取计算样式时,它是更好的选择。文档指出它会在兄弟组件更新之前运行,并且应该使用它来执行自定义 DOM 修改。这是我唯一一个想不出具体用例的钩子,但它可能在某些情况下很有用,比如当你想用其他工具(比如 D3,或者可能是 Canvas 或 WebGL 渲染器)来接管实际的输出渲染时。不过,别把我逼到这个地步。

综上所述

Hooks 让我再次对 React 的未来充满期待。我从 2014 年开始使用这款工具,它不断推出新的变化,让我相信它是 Web 开发的未来。这些 Hooks 也不例外,它再次显著提升了开发者体验,让我能够编写出高可靠性的代码,并通过提取复用功能来提高我的生产力。

我原本以为 Suspense 是 2018 年唯一让我兴奋的功能,但很高兴事实证明我错了!总而言之,我预计 React 应用将为最终用户体验和代码稳定性树立新的标杆。


感谢阅读!我的推特账号是@cvitullo(不过在大多数其他平台上我的名字是 vcarl)。我负责Reactiflux(一个面向 React 开发者的聊天室)和Nodeiflux(一个面向 Node.JS 开发者的聊天室)。如果您有任何问题或建议,欢迎联系我们!封面图片来自Unsplash 上的 rawpixel。

文章来源:https://dev.to/vcarl/everything-you-need-to-know-about-react-hooks-doh
PREV
轻松解释 7 个重要的 AWS 概念
NEXT
树和二叉搜索树 — BaseCS 视频系列