一文讲清楚为啥React Hook不能写在条件语句里

4,845 阅读4分钟

Hook 使用规则

在官方文档Hook 概览 – React (docschina.org)中,对于 Hook 使用规则叙述如下:

Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中,我们稍后会学习到。)

可以看到,官方文档中明确说明了,不要在循环、条件判断或者子函数中调用。

我看了很多解释,但是一直都模模糊糊,一知半解的,直到最近看了一本书:Learn React Hooks.

里面通手动实现一个 useState 让我明白了,useState 的基本实现思路,从而明白了,为什么要顺序调用 Hook.

手动实现一个 useState

实现单个 Hook

首先捋一下,useState 有啥用?

  1. 从里边能够导出一个数组,useState[0] 是一个变量,useState[1] 是一个功能函数。
  2. 能够传一个参数,作为它的初始值。
  3. setState 之后,会把新的值赋值给value,然后,会重新渲染整个函数组件。这时候,组件取到的 value, 就是全新的值。

那我们大概实现一下代码 , 假设,我们在 一个 <App /> 的组件里使用 useState.

function useState(initialValue){
    let value = initialvalue;
    function setState(nextvalue)=>{
        value = nextValue;
        ReactDOM.render(<App />, document.getElementById('root'))
    }
    
    return [value,setState]
}
//.....
const [value,setState] = useState('initialValue');

我们捋一下流程: 第一次,使用 useState ,拿到初始值,正常 render, 取到 value 值。

当我们使用 setState 的时候,此时,按照流程,会重新触发渲染整个组件.

而重新渲染组件的时候,会重新执行一遍 useState, 那这个时候会发生什么呢? 返回的 value 值是初始值。

很明显,按照现有的逻辑,每次都会重新渲染 useState, 我们把 value 放在 useState 里面不对,我们需要每次 setState value 的值为新的后,返回的 [value,setState] 里面的值是最新的。

所以,我们需要把 value 的值放到 useState 的外面,每次 setState 后,重新返回的 value 值,取最外层最新值,这样 render 读取到的数值就是正确的。

代码如下:

import React from 'react';
import ReactDOM from 'react-dom';
import './App.css';

let value;

function useState(initialState) {
  if (typeof value === 'undefined') value = initialState;
  function setState(nextValue) {
    value = nextValue;
    ReactDOM.render(<App />, document.getElementById('root'))
  }
  return [value, setState];
}

function App() {

  const [name, setName] = useState('initialName');
  const handleInputChange = (e) => {
    setName(e.target.value)
  }

  return (
    <div className="App">
      <div>
        <h1>My name is:{name} </h1>
        <input onChange={handleInputChange}></input>
      </div>
    </div>
  );
}

export default App;

实现多个 Hook

按照上文的思路, 每次 setState 之后,都会改变 value 的值。那如果用了 两个 useState,就无法存储两个 value 对应的值了,value 只会存储最新的 setState 改变之后的值。

那怎么办呢? 我们得让不同的 useState 对应不同的 value 的值。

为什么不能将 value 设置成一个数组呢?

第一声明的 useState 的值,放到 value[0], 第二次声明的 useState 的值,放到 value[1]。 这样它们就互不干涉,能够在一个组件函数中多次使用 useState 了。

为了将 useState 的声明和 value 数组对应起来,我们需要引入一个新的变量:

currentHook

现在我们思路如下:

默认 currentHook = 0.

声明第一个 useState 的时候,对 value[0] 的值进行操作。然后 将 currentHook++, 当再次声明 useState 的时候,就对 value[1] 进行操作,以此类推。

要注意的是,setState 以后,会重初始化 useState. 而如果不对 currentHook 重新赋值,它会无限增加,这样就会导致返回的结果不对了。

所以,每次 useState 开始之前,都要对 currenHook 重新赋值为 0.

这样才能保证上一次的 新初始化的 useState 和现有的 value 数组值能够一一对应。

代码如下:

import React from 'react';
import ReactDOM from 'react-dom';
import './App.css';

let values = [];
let currentHook = 0;

function useState(initialState) {
  if (typeof values[currentHook] === 'undefined') values[currentHook] = initialState;
  let hookIndex = currentHook;
  function setState(nextValue) {
    values[hookIndex] = nextValue;
    ReactDOM.render(<App />, document.getElementById('root'))
  }
  return [values[currentHook++], setState];
}

function App() {

  currentHook = 0;

  const [name, setName] = useState('');
  const [age, setAge] = useState();

  console.log('values', values);

  return (
    <div className="App">
      <div>
        <h1>My name is:{name} </h1>
        <input onChange={(e) => setName(e.target.value)}></input>
        <br />
        <h1>My age is:{age} </h1>
        <input onChange={(e) => setAge(e.target.value)}></input>
      </div>
    </div>
  );
}

export default App;

注意

实际的 Hook 实现复杂的多,用起来也更方便。但是它的基础实现原理是和文中的代码思路一致的。

为什么不能放在条件语句里?

从上文的实现过程中,我们可以发现,每次渲染值的时候,useState 的初始值其实是和 value 数组中一一对应的。

当 setState 以后,重新渲染整个组件时:

第一个 useState 取 value[0] 的值,第二个取 value[1] 的值,以此类推。

如果我们将 useState 放在条件语句里,如果这个条件在 N 次执行了,声明了 useState 语句,而在 N + 1 次没有执行,那么 useState 顺序取值的时候,取到的值就是上一个值,以此类推,接下来声明的 useState 全部会出现取值错误。

基于此,我们尽量避免将 Hooks 放在条件语句中。

结语

以上就是我的总结内容,该书的读书笔记可以参照本人违章 :juejin.cn/post/711671…

也可以直接阅读原书。

如果以上内容对您有帮助,记得给我点个赞噢~它会给予我继续更文的动力!