Hooks 常见问题

Hooks 是一项新功能提案,可让您在不编写类的情况下使用 state(状态) 和其他 React 功能。它们目前处于 React v16.7.0-alpha 中,并在 一个开放RFC 中进行讨论。

此页面回答了一些有关 Hooks 的常见问题。

采用策略

我是否需要重写所有类组件?

不需要。我们 没有计划 从 React 中删除类 - 我们会保持 React 中类,你无需重写代码。但是我们建议在新代码中尝试 Hooks 。

我的 React 知识还有多少是相关的?

Hooks 使用的是您已经知道的 React 功能,只是采用了更直接的方式 - 例如 state(状态),lifecycle(生命周期),context(上下文) 和 refs 。 它们并没有从根本上改变 React 的工作方式,而且您对组件,props(属性) 和自上而下数据流的了解也同样重要。

钩子确实有自己的学习曲线。 如果本文档中缺少某些内容,请提交 issue,我们会尽力提供帮助。

我应该使用 Hooks 、classes 还是两者兼而有之?

当您准备好之后,我们将鼓励您开始在您编写的新组件中尝试 Hooks。确保您的团队中的每个人都使用它们,并且熟悉这个文档。我们不建议将现有的类重写为 Hook ,除非您打算重写它们(例如修复 bug )。

您不能在类组件中使用 Hooks ,但是您绝对可以在组件树中,将类和函数式组件混合在一起使用。组件是类还是使用Hook的函数是该组件的实现细节。从长远来看,我们期望 Hooks 成为大家编写响应式组件的主要方式。

Hooks 是否涵盖了 classes 的所有用例?

我们的目标是让 Hooks 尽快涵盖类的所有用例。对于不常见的 getSnapshotBeforeUpdatecomponentDidCatch 生命周期,目前还没有 Hook 等价物,但我们计划很快添加它们。

对于 Hook 来说,现在是一个非常早的时期,因此 DevTools 支持或 Flow/TypeScript typings等一些集成可能还没有准备好。 目前,某些第三方库也可能与 Hook 不兼容。

Hooks 会替换 render props(渲染属性) 和 higher-order components(高阶组件) 吗?

通常,render props(渲染属性) 和 higher-order components(高阶组件) 只渲染单个子组件。 我们认为 Hooks 是服务于这个用例的一种更简单的方法。这两种模式仍然有一席之地(例如,虚拟滚动器组件可能具有 renderItem prop(属性),或者可视化容器组件可能具有自己的 DOM 结构)。但是在大多数情况下,Hooks 就足够了,可以帮助减少组件树中的嵌套。

Hooks 对 Redux connect() 和 React Router 等流行 API 意味着什么?

您可以与以往一样继续使用完全相同的 APIs; 他们会继续工作。

将来,这些库的新版本也可能导出自定义 Hook ,例如 useRedux()useRouter() ,它们允许您使用相同的功能而无需包装成组件。

Hooks 可以使用静态类型吗?

Hooks 的设计考虑了静态类型。 因为它们是函数,所以它们比高阶组件之类的模式更容易正确检测。 我们已提前与 Flow 和 TypeScript 团队联系,他们计划在未来包含 React Hooks 的定义。

重要的是,如果您想以某种方式更严格地检测 React API ,则自定义 Hook 可以让您有权限制 React API 。 React 为您提供了原语,但您可以以不同于我们提供的方式将它们组合在一起。

如何测试使用 Hooks 的组件?

从 React 的角度来看,使用 Hooks 的组件只是一个常规组件。 如果您的测试解决方案不依赖于 React 内部,则测试使用 Hooks 的组件不应与您通常测试组件的方式没有什么不同。

如果您需要测试自定义 Hook ,可以通过在测试中创建一个组件并使用它的 Hook 来实现。 然后,您可以测试您编写的组件。

lint 规则 到底实施了什么?

我们提供了一个 ESLint 插件 ,它强制执行 Hooks 规则 以避免错误。 它假设任何以 ”use” 开头的函数,并且在 ”use” 之后紧跟大写字母都是 Hook 。 我们认识到这种启发式方法并不完美,并且可能存在一些误报,但如果没有整个生态系统的约定,就没有办法让 Hooks 良好工作 - 更长的名字会阻止人们采用 Hooks 或遵循惯例。

特别是,该规则强制执行:

  • 对 Hooks 的调用要么在 PascalCase 函数内(假设是一个组件),要么是另一个 useSomething 函数(假定为自定义Hook)。
  • 在每个渲染上以相同的顺序调用 Hooks 。

还有一些启发式方法,它们可能会随着时间的推移而改变,因为我们需要调整规则,以便在发现bug和避免误报之间取得平衡。

从 Classes 到 Hooks

生命周期方法如何对应于 Hooks ?

  • constructor:函数组件不需要构造函数。 您可以在 useState 调用中初始化状态。 如果计算它开销都很大,您可以将函数传递给 useState

  • getDerivedStateFromProps :在 渲染时 安排更新。

  • shouldComponentUpdate:请参阅 下面React.memo

  • render:这是函数式组件主体本身。

  • componentDidMountcomponentDidUpdatecomponentWillUnmountuseEffect Hook 可以表达这些的所有组合(包括 不太 常见 的情况)。

  • componentDidCatchgetDerivedStateFromError :这些方法还没有 Hook 等价物,但很快就会添加它们。

是否有类似实例变量的东西?

是的! useRef() Hook 不仅适用于 DOM 引用。 “ref” 对象是一个通用容器,其 current 属性是可变的,可以保存任何值,类似于类上的实例属性。

你可以在 useEffect 里面这么写:

function Timer() {
  const intervalRef = useRef();

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

  // ...
}

如果我们只想设置一个间隔,我们就不需要ref(id 可能是 effect 的本地引用),但如果我们想要从事件处理程序中清除间隔,它会很有用:

  // ...
  function handleCancelClick() {
    clearInterval(intervalRef.current);
  }
  // ...

从概念上讲,您可以将 refs 视为类中的实例变量。避免在渲染过程中设置引用 - 这可能会导致令人惊讶的行为。相反,只在事件处理程序和 effects 中修改引用。

我应该使用一个还是多个 state(状态) 变量?

如果你以前都是使用 classes 编写组件 ,你可能希望调用 useState() 一次并将所有 state(状态) 放入一个对象中。 如果你愿意,你可以这样做。 以下是鼠标移动的组件示例。 我们在本地 state(状态) 中保持其 position 和 size :

function Box() {
  const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 });
  // ...
}

现在我们想写一些在用户移动鼠标时 lefttop 发生变化的逻辑。请注意我们必须手动将这些字段合并到以前的状态对象中:

  // ...
  useEffect(() => {
    function handleWindowMouseMove(e) {
      // 展开 "...state" 确保我们不会“失去”宽度和高度 
      setState(state => ({ ...state, left: e.pageX, top: e.pageY }));
    }
    // 注意:这个实现很简化
    window.addEventListener('mousemove', handleWindowMouseMove);
    return () => window.removeEventListener('mousemove', handleWindowMouseMove);
  }, []);
  // ...

这是因为当我们更新状态变量时,我们会替换它的值。 这与类中的 this.setState 不同,后者将更新的字段 合并 到对象中。

如果您希望自动合并,则可以编写一个自定义的 useLegacyState Hook来合并对象 state(状态) 更新。 但是,我们建议将 state(状态) 分割为多个 state(状态) 变量,根据这些 state(状态) 变量的值可能一起更改。

例如,我们可以将组件 state(状态) 拆分为 positionsize 对象,并始终替换 position 而无需合并:

function Box() {
  const [position, setPosition] = useState({ left: 0, top: 0 });
  const [size, setSize] = useState({ width: 100, height: 100 });

  useEffect(() => {
    function handleWindowMouseMove(e) {
      setPosition({ left: e.pageX, top: e.pageY });
    }
    // ...

分离独立 state(状态) 变量还有另一个好处。以后可以轻松地将一些相关逻辑提取到自定义 Hook 中,例如:

function Box() {
  const position = useWindowPosition();
  const [size, setSize] = useState({ width: 100, height: 100 });
  // ...
}

function useWindowPosition() {
  const [position, setPosition] = useState({ left: 0, top: 0 });
  useEffect(() => {
    // ...
  }, []);
  return position;
}

请注意,我们如何能够在不更改其代码的情况下将 position 状态变量的 useState 调用和相关 effect 移动到自定义 Hook 中。如果所有的状态都在一个对象中,那么提取它将更加困难。

将所有状态放在单个 useState 调用中,并且每个字段都有一个 useState 调用都可以工作。 当您在这两个极端之间找到平衡时,组件往往最具可读性,并且将相关状态组分成几个独立的状态变量。 如果状态逻辑变得复杂,我们建议 使用 reducer 或自定义Hook管理它。

我可以仅针对更新是运行 effect 吗?

这是一个罕见的用例。 如果需要,可以使用 mutable ref 手动存储一个布尔值,该值对应于您是第一次渲染还是后续渲染,然后在 effect 中检查该标志。 (如果你发现自己经常这样做,你可以为它创建一个自定义Hook。)

如何获得以前的 props(属性) 或 state(状态)?

目前,您可以 使用 ref 手动执行此操作:

function Counter() {
  const [count, setCount] = useState(0);

  const prevCountRef = useRef();
  useEffect(() => {
    prevCountRef.current = count;
  });
  const prevCount = prevCountRef.current;

  return <h1>Now: {count}, before: {prevCount}</h1>;
}

这可能有点复杂,但您可以将其提取到自定义 Hook 中:

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return <h1>Now: {count}, before: {prevCount}</h1>;
}

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

请注意这对于 props ,state 或任何其他计算值是如何工作的。

function Counter() {
  const [count, setCount] = useState(0);

  const calculation = count * 100;
  const prevCalculation = usePrevious(calculation);
  // ...

未来 React 可能会提供一个 usePrevious Hook 开箱即用,因为它是一个相对常见的用例。

另请参见 派生状态的推荐模式

如何实现 getDerivedStateFromProps

虽然您可能 不需要它,但在极少数情况下(例如实现<Transition> 组件),您可以在渲染期间更新状态。 React 将在退出第一个渲染后立即重新运行具有更新状态的组件,因此它的开销不会很大。

在这里,我们将 row prop 的先前值存储在状态变量中,以便我们可以比较:

function ScrollView({row}) {
  let [isScrollingDown, setIsScrollingDown] = useState(false);
  let [prevRow, setPrevRow] = useState(null);

  if (row !== prevRow) {
    // Row changed since last render. Update isScrollingDown.
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }

  return `Scrolling down: ${isScrollingDown}`;
}

这可能看起来很奇怪,但渲染过程中的更新正是 getDerivedStateFromProps 在概念上一直如此。

我可以对函数式组件进行 ref 吗?

虽然您不应经常需要这样做,但您可以使用 useImperativeMethods Hook 向父组件公开一些强制性方法。

const [thing, setThing] = useState() 是什么意思?

如果您不熟悉此语法,请查看 State Hook 文档中的说明

性能优化

我可以在更新时跳过 effect 吗?

是。请参阅条件控制的 effect。请注意,忘记处理更新通常会 引起错误,这就是为什么这不是默认行为的原因。

我如何实现 shouldComponentUpdate

您可以使用 React.memo 包装一个函数组件来浅比较它的 props :

const Button = React.memo((props) => {
  // your component
});

它不是一个 Hook ,因为它不像 Hooks 那样的组合。 React.memo 相当于 PureComponent ,但它只比较 props(属性)。 (您还可以添加第二个参数来指定采用旧 props(属性) 和 新props(属性) 的自定义比较函数。如果返回 true ,则跳过更新。)

React.memo 不比较 state(状态) ,因为没有单个状态对象可以比较。但你也可以让子组件变变成纯函数,甚至 useMemo 优化个别子组件

如何记忆(memoize)计算结果?

useMemo Hook允许您通过 “记住” 以前的计算来缓存多个渲染之间的计算:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

此代码调用 computeExpensiveValue(a, b) 。 但是如果输入 [a, b] 自上一个值以来没有改变,则 useMemo 会跳过第二次调用它并简单地重用它返回的最后一个值。

方便的是,这也可以让你跳过一个开销巨大的子组件的重新渲染:

function Parent({ a, b }) {
  // Only re-rendered if `a` changes:
  const child1 = useMemo(() => <Child1 a={a} />, [a]);
  // Only re-rendered if `b` changes:
  const child2 = useMemo(() => <Child2 b={b} />, [b]);
  return (
    <>
      {child1}
      {child2}
    </>
  )
}

请注意,这种方法在循环中不起作用,因为Hook调用 不能 放在循环中。但是您可以为列表项提取单独的组件,并在那里调用 useMemo

在渲染过程中创建函数,Hooks 是否会变慢?

不会!在现代浏览器中,除了极端情况之外,闭包与类的原始性能没有显著差异。

此外,考虑到 Hooks 的设计在以下几个方面更有效:

  • Hooks 避免了类所需的大量开销,比如在构造函数中创建类实例和绑定事件处理程序的成本。

  • 使用 Hooks 的惯用代码不需要深层组件树嵌套,这在使用 higher-order components(高阶组件),render props(渲染属性)和context 的代码库中很常见。使用较小的组件树,React 的工作量较少。

传统上,React 中内联函数的性能问题,与每个渲染上的新回调如何在子组件中断开 shouldComponentUpdate 优化有关。Hooks 从三个方面解决了这个问题。

  • useCallback Hook 允许您在重新渲染之间保持相同的回调引用,以便 shouldComponentUpdate 继续工作:

    // Will not change unless `a` or `b` changes
    const memoizedCallback = useCallback(() => {
      doSomething(a, b);
    }, [a, b]);
  • useMemo Hook 可以更轻松地控制个别子组件何时更新,从而减少对纯组件的需求。

  • 最后,useReducer Hook 减少了深度传递回调的需要,如下所述。

如何避免传递回调?

我们发现,大多数人不喜欢通过组件树的每个级别手动传递回调。尽管它更显式的,但感觉像是很多 “plumbing(管道)” 。

在大型组件树中,我们建议的另一种方法是通过上下文从 useReducer 传递 dispatch 函数:

const TodosDispatch = React.createContext(null);

function TodosApp() {
  // Tip: `dispatch` won't change between re-renders
  const [todos, dispatch] = useReducer(todosReducer);

  return (
    <TodosDispatch.Provider value={dispatch}>
      <DeepTree todos={todos} />
    </TodosDispatch.Provider>
  );
}

TodosApp 中树中的任何子节点都可以使用 dispatch 函数将操作传递到 TodosApp

function DeepChild(props) {
  // If we want to perform an action, we can get dispatch from context.
  const dispatch = useContext(TodosDispatch);

  function handleClick() {
    dispatch({ type: 'add', text: 'hello' });
  }

  return (
    <button onClick={handleClick}>Add todo</button>
  );
}

从维护的角度来看,这更方便(不需要一直转发回调),而且完全避免了回调问题。像这样向下传递 dispatch 是深度更新的推荐模式。

请注意,您仍然可以选择将应用程序 state(状态) 作为 props(更明确),或是作为 context(对于非常深的更新更方便)传递下去。 如果您也使用 context 传递 state(状态) ,请使用两种不同的上下文类型 - dispatch context 永远不会更改,因此读取它的组件不需要重新渲染,除非它们还需要应用程序 state(状态)。

如何从 useCallback 中读取经常变化的值?

注意

我们建议 在 context 中传递 dispatch ,而不是在 props(属性) 中单独回调。为了完整起见,并作为 escape hatch(逃生舱),此处仅提及以下方法。

还要注意,此模式可能会在 并发模式 中导致问题。我们计划在将来提供更符合人们习惯的替代方案,但是目前最安全的解决方案是,如果某个值依赖于更改,则总是使回调无效。

在极少数情况下,您可能需要使用 useCallback 来 memoize(记住) 回调,但是memoization 不能很好地工作,因为内部函数必须经常重新创建。 如果您要记住的函数是事件处理程序并且在渲染期间未使用,则可以使用 ref 作为实例变量,并将最后提交的值手动保存到其中:

function Form() {
  const [text, updateText] = useState('');
  const textRef = useRef();

  useLayoutEffect(() => {
    textRef.current = text; // Write it to the ref
  });

  const handleSubmit = useCallback(() => {
    const currentText = textRef.current; // Read it from the ref
    alert(currentText);
  }, [textRef]); // Don't recreate handleSubmit like [text] would do

  return (
    <>
      <input value={text} onChange={e => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}

这是一个相当复杂的模式,但它表明如果需要,您可以执行此 escape hatch(逃生舱) 优化。如果将其提取到自定义 Hook,则更可让人接受一些:

function Form() {
  const [text, updateText] = useState('');
  // Will be memoized even if `text` changes:
  const handleSubmit = useEventCallback(() => {
    alert(text);
  }, [text]);

  return (
    <>
      <input value={text} onChange={e => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}

function useEventCallback(fn, dependencies) {
  const ref = useRef(() => {
    throw new Error('Cannot call an event handler while rendering.');
  });

  useLayoutEffect(() => {
    ref.current = fn;
  }, [fn, ...dependencies]);

  return useCallback(() => {
    const fn = ref.current;
    return fn();
  }, [ref]);
}

在任何一种情况下,我们都 不建议使用此模式 ,为了完整起见,我们只在这里显示它。相反,最好避免将回调传递到深层组件树种

底层实现

React 如何将 Hook 调用与组件相关联?

React 跟踪当前渲染组件。 由于 Hooks 规则,我们知道 Hook 只能从 React 组件调用(或自定义 Hooks 也只能从 React 组件中调用)。

每个组件都有一个 “内存单元” 的内部列表。它们只是 JavaScript 对象,我们可以在其中放置一些数据。当调用 useState() 这样的Hook 时,它读取当前单元格(或在第一次呈现时初始化它),然后将指针移动到下一个单元格。这就是多个 useState() 调用各自获取独立本地状态的方式。

Hooks 的先前技术是什么?

Hooks 综合了来自几个不同来源的观点:

Sebastian Markbåge 提出了 Hooks 的原创设计,后来由Andrew ClarkSophie AlpertDominic Gannaway 以及 React 团队的其他成员完善。