是因为我在数组的 map 中使用了 hooks 触发了 eslint 检查,然后蓝教授扔了这篇文章给我。

太长不看版:有 hooks 的函数组件使用 JSX 或者React.createElement来渲染组件,而不是直接函数调用。

标题: Don’t call a React function component

原文链接: https://kentcdodds.com/blog/dont-call-a-react-function-component

作者说他看到 Taranveer Bains 在他的 AMA 提出的很棒的 issue:

我遇到了个问题,当我在函数组件中使用了 hooks,并且将 JSX 传递给Array.prototype.map的回调函数并返回,触发了这样一个报错:React Error: Rendered fewer hooks than expected

大概这样写能复现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import React from "react";
function Counter() {
  const [count, setCount] = React.useState(0);
  const increment = () => setCount((c) => c + 1);
  return <button onClick={increment}>{count}</button>;
}
function App() {
  const [items, setItems] = React.useState([]);
  const addItem = () => setItems((i) => [...i, { id: i.length }]);
  return (
    <div>
      <button onClick={addItem}>Add Item</button>
      <div>{items.map(Counter)}</div>
    </div>
  );
}

这是怎么肥事呢,首先,解决方法是这样写:

1
2
- <div>{items.map(Counter)}</div>
+ <div>{items.map(i => <Counter key={i.id} />)}</div>

如果你认为这个跟key props 有关的话,那先告诉你是没有的。不过key还是很重要的,可以通过作者的这篇文章了解一下key的作用:Understanding React’s key prop

下面这样的写法也会触发这个错误:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function Example() {
  const [count, setCount] = React.useState(0);
  let otherState;
  if (count > 0) {
    React.useEffect(() => {
      console.log("count", count);
    });
  }
  const increment = () => setCount((c) => c + 1);
  return <button onClick={increment}>{count}</button>;
}

关键是因为Example组件在条件语句中调用了 hook ,违反了 hooks 规则,这也是 eslint-plugin-react-hooks 包有 rules-of-hooks 规则的原因。也可以在 React 文档中知道更多关于这个规则的内容,但是其实一句话就可以解决这个问题:保证对于给定的组件,hooks 的调用次数始终保持一致。

但是在刚开始的例子里,我们并没有在条件语句中调用 hooks 不是吗?那为什么这样调用会有问题呢?

让我们稍微重写下我们的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function Counter() {
  const [count, setCount] = React.useState(0);
  const increment = () => setCount((c) => c + 1);
  return <button onClick={increment}>{count}</button>;
}
function App() {
  const [items, setItems] = React.useState([]);
  const addItem = () => setItems((i) => [...i, { id: i.length }]);
  return (
    <div>
      <button onClick={addItem}>Add Item</button>
      <div>
        {/** 就是这里跟上面不一样 */}
        {items.map(() => {
          return Counter();
        })}
      </div>
    </div>
  );
}

你会注意到其实我们就是在一个函数中调用了另一个函数,所以可以直接改写成内联的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function App() {
  const [items, setItems] = React.useState([]);
  const addItem = () => setItems((i) => [...i, { id: i.length }]);
  return (
    <div>
      <button onClick={addItem}>Add Item</button>
      <div>
        {/** 还是这里跟上面不一样 */}
        {items.map(() => {
          const [count, setCount] = React.useState(0);
          const increment = () => setCount((c) => c + 1);
          return <button onClick={increment}>{count}</button>;
        })}
      </div>
    </div>
  );
}

我们只是简单的重构了下,并没有改变任何逻辑,注意到问题在哪里了吗?让我们复习一下上面说过的:我们需要保证对于一个给定的组件,hooks 调用的次数总是一致的。

基于我们的重构,这里调用了useState这个 hook 的“给定的组件”不是AppCounter,而是仅有App。这依赖我们调用Counter函数组件的方式。当下这种写法它只是个函数,而不是个组件。React 并不知道我们在 JSX 中调用函数和直接内联的区别,因此它无法将任何内容与Counter函数关联,因为它并未像一个组件一样被调用。

这就是为什么我们有的时候需要用 JSX (或者React.createElement)而不是简单调用函数来渲染组件,因为通过 JSX (或者React.createElement)可以使所有的 hooks 注册到 React 创建的实例上。

所以在这种情况下不要直接调用函数,而是应该使用 JSX (或者React.createElement)渲染组件

顺便需要注意一下,有的时候直接调用函数也是阔以的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function Counter() {
  const [count, setCount] = React.useState(0);
  const increment = () => setCount((c) => c + 1);
  return <button onClick={increment}>{count}</button>;
}
function App() {
  return (
    <div>
      <div>Here is a counter:</div>
      {Counter()}
    </div>
  );
}

但是这个时候Counter内的 hooks 是与App组件实例相关联的,因为并没有Counter的组件实例。所以虽然这样写可以 work,但是不是按照我们期待的方式 work 的。所以还是正常走“渲染”流程吧。