首页/文章/八股文

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

2025-02-26
19387 分钟
...

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

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

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

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

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

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

实现单个 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 放在条件语句中。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
八股文2025-03-24
OPTIONS请求条件与浏览器安全策略CORS
1\. 什么时候会发送OPTIONS请求 在 Web 开发中, 请求是一种 HTTP 请求方法,通常与 CORS(跨源资源共享,Cross Origin Resource Sharing) 机制相关。它主要用于预检(preflight)请求,以检查服务器是否允许特定的跨域请求。以下是详细说明: 1.1. 发送OPTIONS请求条件 1.1.1. 跨域请求且不是简单请求 当发起的请求是跨域的(即请求的目标域名与当前页面域名不同),并且不满...
面试面试官
八股文2025-03-24
前端项目中,强缓存和协商缓存的配置
前端缓存分为HTTP缓存和 1. HTTP缓存(本文重点) 强缓存 协商缓存 2. 浏览器缓存 比较熟悉的 cookie,localstorage sessionstorage indexDB… 或者cacheStorage 请求的缓存,如果本地有取本地的 先说总结的内容 webpack配置(主要是文件名上有个hash值,便于服务器校验文件有没有修改) 使用hash的好处就是,使版本发布之后,使之前文件的强缓存失效 nginx配置 (主...
面试面试官
八股文2025-03-24
搞定前端性能优化问题
一、首屏加载优化 首屏加载是指 用户打开页面到第一个可视内容呈现出来之间的时间 。它直接影响用户对网站的第一印象,因此在前端性能优化中具有最高的优先级。 优化首屏加载的目标是 尽可能快地把关键内容展示给用户,减少白屏时间,提升首屏体验。 常见的优化方式包括资源按需加载、预加载关键资源、使用服务端渲染或静态生成等。 1\. 资源按需加载(Lazy Load) 在前端应用中,JS、CSS、图片等资源如果全部打包在一起加载,会极大拖慢页面的加...
面试面试官
八股文2025-03-19
JS宏任务,微任务,DOM渲染,requestAnimationFrame执行顺序比较
本文小结 1. 宏任务和微任务都是异步任务。 2. 优先级: 3. 慢于同步任务。本次实践中,第一次执行的时间比微任务慢,比宏任务和DOM渲染快。 来自MDN的理论部分 每个代理都是由 事件循环 驱动的,事件循环负责收集用事件(包括用户事件以及其他非用户事件等)、对任务进行排队以便在合适的时候执行回调。然后它执行所有处于等待中的 JavaScript 任务(宏任务),然后是微任务,然后在开始下一次循环之前执行一些必要的渲染和绘制操作。...
面试面试官
八股文2025-03-18
一文总结vue和react的区别
Vue.js 和 React.js 都是流行的 JavaScript 前端框架,用于构建用户界面。它们在设计理念、语法和使用方式等方面有一些区别,以下是一些比较详细的对比: 设计理念: Vue.js :Vue.js 是一款轻量级、渐进式的框架,它专注于视图层的渲染和状态管理。Vue 的核心思想是通过数据驱动视图的变化,提供了一套简洁的模板语法和响应式数据绑定机制,使得开发者可以更便捷地构建交互式的用户界面。 React.js :Reac...
面试面试官
八股文2025-03-10
最全的Map 和 WeakMap的区别
和 的区别及面试追问知识点 一、两者区别总结 (一)基本概念 和 均为 JavaScript 中用于存储键值对的数据结构,但在多个关键特性上有显著差异。 (二)键的类型限制 1. 键类型无严格限制,可接受基本数据类型(如字符串、数字、布尔值)以及引用数据类型(如对象、数组、函数)。 示例代码: 2. 键必须为对象,使用基本数据类型作为键会抛出错误。 示例代码: (三)垃圾回收机制 1. 对键和值采用强引用,只要 对象存在,其引用的键值对...
面试面试官

评论

请登录后发表评论

去登录
加载评论中...

目录