不要调用React函数组件

44 阅读3分钟

我在AMA上收到了Taranveer Bains的一个好问题:

我遇到了一个问题,如果我提供了一个在其实现中使用钩子的函数,并返回一些JSX到回调,Array.prototype.map 。我收到的错误是React Error: Rendered fewer hooks than expected

下面是该错误的一个简单再现

import * as React from 'react'

function Counter() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return {count}
}

function App() {
  const [items, setItems] = React.useState([])
  const addItem = () => setItems(i => [...i, {id: i.length}])
  return (
    
      Add Item
      {items.map(Counter)}
    
  )
}

这是渲染时的表现(在它周围有一个错误边界,这样我们就不会让这个页面崩溃)。

在控制台中,像这样的信息有更多细节。

Warning: React has detected a change in the order of Hooks
called by BadCounterList. This will lead to bugs and
errors if not fixed. For more information, read the
Rules of Hooks: https://fb.me/rules-of-hooks

   Previous render            Next render
   ------------------------------------------------------
1. useState                   useState
2. undefined                  useState
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

那么,这里发生了什么?让我们深入了解一下。

首先,我只告诉你解决方案:

{items.map(Counter)}
{items.map(i => )}

在你开始认为这与key 道具有关之前,让我告诉你,这不是。但一般来说,关键道具是很重要的,你可以从我的另一篇博文中了解到这一点。了解React的关键道具

这里有另一种方法可以使这种相同的错误发生:

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 {count}
}

关键是我们的Example 组件在有条件地调用一个钩子,这违背了钩子的规则,这也是eslint-plugin-react-hooks包有一个rules-of-hooks 规则的原因。你可以从React文档中读到更多关于这个限制的内容,但只需要说,你需要确保钩子对于一个特定的组件总是被调用相同的次数。

好吧,但在我们的第一个例子中,我们并没有有条件地调用钩子,对吗?那么,在这种情况下,为什么会给我们带来问题呢?

好吧,让我们稍微重写一下我们原来的例子:

function Counter() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return {count}
}

function App() {
  const [items, setItems] = React.useState([])
  const addItem = () => setItems(i => [...i, {id: i.length}])
  return (
    
      Add Item
      
        {items.map(() => {
          return Counter()
        })}
      
    
  )
}

你会注意到我们正在做一个只是调用另一个函数的函数,所以让我们内联它:

function App() {
  const [items, setItems] = React.useState([])
  const addItem = () => setItems(i => [...i, {id: i.length}])
  return (
    
      Add Item
      
        {items.map(() => {
          const [count, setCount] = React.useState(0)
          const increment = () => setCount(c => c + 1)
          return {count}
        })}
      
    
  )
}

开始觉得有问题了吗?你会注意到,我们实际上并没有改变任何行为。这只是一个重构。但你现在注意到问题所在了吗?让我重复一下我之前说的:你需要确保钩子对于一个特定的组件总是被调用相同的次数**。**

基于我们的重构,我们已经认识到,我们所有的useState 调用的 "给定组件 "不是AppCounter ,而是单独的App 。这是由于我们调用我们的Counter 函数组件的方式造成的。它根本就不是一个组件,而是一个函数。React不知道我们在JSX中调用一个函数和内联函数之间的区别。所以它不能将任何东西与Counter 函数联系起来,因为它没有像一个组件那样被渲染。

这就是为什么你在渲染组件时需要使用JSX(或React.createElement而不是简单地调用函数。这样一来,任何使用的钩子都可以在React创建的组件实例中注册。

所以不要调用函数组件。渲染它们。

哦,值得一提的是,有时调用函数组件会 "有效"像这样:

function Counter() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return {count}
}

function App() {
  return (
    
      Here is a counter:
      {Counter()}
    
  )
}

但是Counter 中的钩子将与App 组件实例相关,因为没有Counter 组件实例。所以它会 "工作",但不是你所期望的方式,而且在你进行修改时,它可能会有意想不到的表现。所以,只要正常渲染即可。

祝您好运

你可以在 codesandbox 中玩玩这个。

Edit Don't call function components