简单易懂的 React useState() Hook 指南

2024-01-16 10:08

本文主要是介绍简单易懂的 React useState() Hook 指南,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

来源 | https://github.com/qq449245884/xiaozhi/issues/147

原文:https://dmitripavlutin.com/react-usestate-hook-guide/

状态是隐藏在组件中的信息,组件可以在父组件不知道的情况下修改其状态。我更偏爱函数组件,因为它们足够简单,要使函数组件具有状态管理,可以useState() Hook。

本文会逐步讲解如何使用useState() Hook。此外,还会介绍一些常见useState() 坑。

1.使用 useState() 进行状态管理


无状态的函数组件没有状态,如下所示(部分代码):

import React from 'react';
function Bulbs() { return <div className="bulb-off" />;}

可以找 codesandbox 尝试一下。

运行效果:

这时,要如何添加一个按钮来打开/关闭灯泡呢?为此,咱们需要具有状态的函数组件,也就是有状态函数组件。

useState()是实现灯泡开关状态的 Hoook,将状态添加到函数组件需要4个步骤:启用状态、初始化、读取和更新。

1.1 启用状态

要将<Bulbs> 转换为有状态组件,需要告诉 React:从'react'包中导入useState钩子,然后在组件函数的顶部调用useState()

大致如下所示:

import React, { useState } from 'react';function Bulbs() { ... = useState(...); return <div className="bulb-off" />;}

Bulbs函数的第一行调用useState()(暂时不要考Hook的参数和返回值)。重要的是,在组件内部调用 Hook 会使该函数成为有状态的函数组件。

启用状态后,下一步是初始化它。

1.2初始化状态

始时,灯泡关闭,对应到状态应使用false初始化 Hook:

import React, { useState } from 'react';
function Bulbs() { ... = useState(false); return <div className="bulb-off" />;}

useState(false)false初始化状态。

启用和初始化状态之后,如何读取它?来看看useState(false)返回什么。

1.3 读取状态

当 hook useState(initialState)被调用时,它返回一个数组,该数组的第一项是状态值

const stateArray = useState(false);stateArray[0]; // => 状态值

咱们读取组件的状态

function Bulbs() { const stateArray = useState(false); return <div className={stateArray[0] ? 'bulb-on' : 'bulb-off'} />;}

<Bulbs>组件状态初始化为false,可以打开 codesandbox 看看效果。

useState(false)返回一个数组,第一项包含状态值,该值当前为false(因为状态已用false初始化)。

咱们可以使用数组解构来将状态值提取到变量on上:

import React, { useState } from 'react';
function Bulbs() { const [on] = useState(false); return <div className={on ? 'bulb-on' : 'bulb-off'} />;}

on状态变量保存状态值。

状态已经启用并初始化,现在可以读取它了。但是如何更新呢?再来看看useState(initialState)返回什么。

1.4 更新状态

用值更新状态

咱们已经知道,useState(initialState)返回一个数组,其中第一项是状态值,第二项是一个更新状态的函数。

const [state, setState] = useState(initialState);// 将状态更改为 'newState' 并触发重新渲染setState(newState);// 重新渲染`state`后的值为`newState`

要更新组件的状态,请使用新状态调用更新器函数setState(newState)。组件重新渲染后,状态接收新值newState

当点击开灯按钮时将灯泡开关状态更新为true,点击关灯时更新为 false

import React, { useState } from 'react';function Bulbs() {  const [on, setOn] = useState(false); const lightOn = () => setOn(true);  const lightOff = () => setOn(false); return ( <> <div className={on ? 'bulb-on' : 'bulb-off'} /> <button onClick={lightOn}>开灯</button> <button onClick={lightOff}>关灯</button> </> );}

打开 codesandbox 自行尝试一下。

单击开灯按钮时,lightOn()函数将on更新为truesetOn(true)。单击关灯时也会发生相同的情况,只是状态更新为false

状态一旦改变,React 就会重新渲染组件,on变量获取新的状态值。

状态更新作为对提供一些新信息的事件的响应。这些事件包括按钮单击、HTTP 请求完成等,确保在事件回调或其他 Hook 回调中调用状态更新函数。

使用回调更新状态

当使用前一个状态计算新状态时,可以使用回调更新该状态:

const [state, setState] = useState(initialState);...setState(prevState => nextState);
...

下面是一些事例:

// Toggle a booleanconst [toggled, setToggled] = useState(false);setToggled(toggled => !toggled);// Increase a counterconst [count, setCount] = useState(0);setCount(count => count + 1);// Add an item to arrayconst [items, setItems] = useState([]);setItems(items => [...items, 'New Item']);

接着,通过这种方式重新实现上面电灯的示例:

import React, { useState } from 'react';
function Bulbs() { const [on, setOn] = useState(false);
const lightSwitch = () => setOn(on => !on);
return ( <> <div className={on ? 'bulb-on' : 'bulb-off'} /> <button onClick={lightSwitch}>开灯/关灯</button> </> );}

打开 codesandbox 自行尝试一下。

setOn(on => !on)使用函数更新状态。

1.5 小结一波
  • 调用useState() Hook 来启用函数组件中的状态。

  • useState(initialValue)的第一个参数initialValue是状态的初始值。

  • [state, setState] = useState(initialValue)返回一个包含2个元素的数组:状态值和状态更新函数。

  • 使用新值调用状态更新器函数setState(newState)更新状态。或者,可以使用一个回调setState(prev => next)来调用状态更新器,该回调将返回基于先前状态的新状态。

  • 调用状态更新器后,React 确保重新渲染组件,以使新状态变为当前状态。

2. 多种状态

通过多次调用useState(),一个函数组件可以拥有多个状态。

function MyComponent() { const [state1, setState1] = useState(initial1); const [state2, setState2] = useState(initial2); const [state3, setState3] = useState(initial3); // ...}

需要注意的,要确保对useState()的多次调用在渲染之间始终保持相同的顺序(后面会讲)。

我们添加一个按钮添加灯泡,并添加一个新状态来保存灯泡数量,单击该按钮时,将添加一个新灯泡。

新的状态count 包含灯泡的数量,初始值为1

import React, { useState } from 'react';function Bulbs() { const [on, setOn] = useState(false);  const [count, setCount] = useState(1); const lightSwitch = () => setOn(on => !on);  const addBulbs = () => setCount(count => count + 1); const bulb = <div className={on ? 'bulb-on' : 'bulb-off'} />;  const bulbs = Array(count).fill(bulb); return ( <> <div className="bulbs">{bulbs}</div> <button onClick={lightSwitch}>开/关</button> <button onClick={addBulbs}>添加灯泡</button> </> );}

打开演示,然后单击添加灯泡按钮:灯泡数量增加,单击开/关按钮可打开/关闭灯泡。

[on, setOn] = useState(false) 管理开/关状态

  • [count, setCount] = useState(1)管理灯泡数量。

多个状态可以在一个组件中正确工作。

3.状态的延迟初始化

每当 React 重新渲染组件时,都会执行useState(initialState)。如果初始状态是原始值(数字,布尔值等),则不会有性能问题。

当初始状态需要昂贵的性能方面的操作时,可以通过为useState(computeInitialState)提供一个函数来使用状态的延迟初始化,如下所示:

function MyComponent({ bigJsonData }) { const [value, setValue] = useState(function getInitialState() { const object = JSON.parse(bigJsonData); // expensive operation return object.initialValue;  }); // ...}

getInitialState()仅在初始渲染时执行一次,以获得初始状态。在以后的组件渲染中,不会再调用getInitialState(),从而跳过昂贵的操作。

4. useState() 中的坑

现在咱们基本已经初步掌握了如何使用useState(),尽管如此,咱们必须注意在使用useState()时可能遇到的常见问题。

4.1 在哪里调用 useState()

在使用useState() Hook 时,必须遵循 Hook 的规则

  1. 仅顶层调用 Hook :不能在循环,条件,嵌套函数等中调用useState()。在多个useState()调用中,渲染之间的调用顺序必须相同。

  2. 仅从React 函数调用 Hook:必须仅在函数组件或自定义钩子内部调用useState()

来看看useState()的正确用法和错误用法的例子。

有效调用useState()

useState()在函数组件的顶层被正确调用

function Bulbs() { // Good const [on, setOn] = useState(false); // ...}

以相同的顺序正确地调用多个useState()调用:

function Bulbs() { // Good const [on, setOn] = useState(false); const [count, setCount] = useState(1); // ...

useState()在自定义钩子的顶层被正确调用

function toggleHook(initial) { // Good const [on, setOn] = useState(initial); return [on, () => setOn(!on)];}function Bulbs() { const [on, toggle] = toggleHook(false); // ...}

useState() 的无效调用

在条件中调用useState()是不正确的:

function Switch({ isSwitchEnabled }) { if (isSwitchEnabled) { // Bad const [on, setOn] = useState(false); } // ...}

在嵌套函数中调用useState()也是不对的

function Switch() { let on = false;  let setOn = () => {}; function enableSwitch() { // Bad [on, setOn] = useState(false);  } return ( <button onClick={enableSwitch}> Enable light switch state </button> );}

4.2 过时状态

闭包是一个从外部作用域捕获变量的函数。

闭包(例如事件处理程序,回调)可能会从函数组件作用域中捕获状态变量。由于状态变量在渲染之间变化,因此闭包应捕获具有最新状态值的变量。否则,如果闭包捕获了过时的状态值,则可能会遇到过时的状态问题。

来看看一个过时的状态是如何表现出来的。组件<DelayedCount>延迟3秒计数按钮点击的次数。

function DelayedCount() { const [count, setCount] = useState(0);
const handleClickAsync = () => { setTimeout(function delay() { setCount(count + 1); }, 3000);  } return ( <div> {count} <button onClick={handleClickAsync}>Increase async</button> </div> );}

打开演示,快速多次点击按钮。count 变量不能正确记录实际点击次数,有些点击被吃掉。

delay() 是一个过时的闭包,它从初始渲染(使用0初始化时)中捕获了过时的count变量。

为了解决这个问题,使用函数方法来更新count状态:

function DelayedCount() { const [count, setCount] = useState(0);
const handleClickAsync = () => { setTimeout(function delay() { setCount(count => count + 1); }, 3000); }
return ( <div> {count} <button onClick={handleClickAsync}>Increase async</button> </div> );}

现在etCount(count => count + 1)delay()中正确更新计数状态。React 确保将最新状态值作为参数提供给更新状态函数,过时闭包的问题解决了。

打开演示,快速单击按钮。延迟过去后,count 能正确表示点击次数。

4.3 复杂状态管理

useState()用于管理简单状态。对于复杂的状态管理,可以使用useReducer() hook。它为需要多个状态操作的状态提供了更好的支持。

假设需要编写一个最喜欢的电影列表。用户可以添加电影,也可以删除已有的电影,实现方式大致如下:

import React, { useState } from 'react';function FavoriteMovies() {  const [movies, setMovies] = useState([{ name: 'Heat' }]);  const add = movie => setMovies([...movies, movie]); const remove = index => { setMovies([ ...movies.slice(0, index), ...movies.slice(index + 1) ]);  } return ( // Use add(movie) and remove(index)... );}

尝试演示:添加和删除自己喜欢的电影。

状态列表需要几个操作:添加和删除电影,状态管理细节使组件混乱。

更好的解决方案是将复杂的状态管理提取到reducer中:

import React, { useReducer } from 'react';function reducer(state, action) { switch (action.type) { case 'add': return [...state, action.item]; case 'remove': return [ ...state.slice(0, action.index), ...state.slice(action.index + 1) ]; default: throw new Error(); }}function FavoriteMovies() { const [state, dispatch] = useReducer(reducer, [{ name: 'Heat' }]);
return ( // Use dispatch({ type: 'add', item: movie }) // and dispatch({ type: 'remove', index })... );}

reducer管理电影的状态,有两种操作类型:

  • "add"将新电影插入列表

  • "remove"从列表中按索引删除电影

尝试演示并注意组件功能没有改变。但是这个版本的<FavoriteMovies>更容易理解,因为状态管理已经被提取到reducer中。

还有一个好处:可以将reducer 提取到一个单独的模块中,并在其他组件中重用它。另外,即使没有组件,也可以对reducer 进行单元测试。

这就是关注点分离的威力:组件渲染UI并响应事件,而reducer 执行状态操作。

4.4 状态 vs 引用

考虑这样一个场景:咱们想要计算组件渲染的次数。

一种简单的实现方法是初始化countRender状态,并在每次渲染时更新它(使用useEffect() hook)

import React, { useState, useEffect } from 'react';function CountMyRenders() {  const [countRender, setCountRender] = useState(0); useEffect(function afterRender() { setCountRender(countRender => countRender + 1);  }); return ( <div>I've rendered {countRender} times</div> );}

useEffect()在每次渲染后调用afterRender()回调。但是一旦countRender状态更新,组件就会重新渲染。这将触发另一个状态更新和另一个重新渲染,依此类推。

可变引用useRef()保存可变数据,这些数据在更改时不会触发重新渲染,使用可变的引用改造一下<CountMyRenders> :

import React, { useRef, useEffect } from 'react';function CountMyRenders() {  const countRenderRef = useRef(1); useEffect(function afterRender() { countRenderRef.current++;  }); return ( <div>I've rendered {countRenderRef.current} times</div> );}

打开演示并单击几次按钮来触发重新渲染。

每次渲染组件时,countRenderRef可变引用的值都会使countRenderRef.current ++递增。重要的是,更改不会触发组件重新渲染。

5. 总结

要使函数组件有状态,请在组件的函数体中调用useState()

useState(initialState)的第一个参数是初始状态。返回的数组有两项:当前状态和状态更新函数。

const [state, setState] = useState(initialState);

使用 setState(newState)来更新状态值。另外,如果需要根据先前的状态更新状态,可以使用回调函数setState(prevState => newState)

在单个组件中可以有多个状态:调用多次useState()

当初始状态开销很大时,延迟初始化很方便。使用计算初始状态的回调调用useState(computeInitialState),并且此回调仅在初始渲染时执行一次。

必须确保使用useState()遵循 Hook 规则。

当闭包捕获过时的状态变量时,就会出现过时状态的问题。可以通过使用一个回调来更新状态来解决这个问题,这个回调会根据先前的状态来计算新的状态。

最后,您将使用useState()来管理一个简单的状态。为了处理更复杂的状态,一个更好的的选择是使用useReducer() hook。

这篇关于简单易懂的 React useState() Hook 指南的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/612211

相关文章

Python正则表达式匹配和替换的操作指南

《Python正则表达式匹配和替换的操作指南》正则表达式是处理文本的强大工具,Python通过re模块提供了完整的正则表达式功能,本文将通过代码示例详细介绍Python中的正则匹配和替换操作,需要的朋... 目录基础语法导入re模块基本元字符常用匹配方法1. re.match() - 从字符串开头匹配2.

Vue和React受控组件的区别小结

《Vue和React受控组件的区别小结》本文主要介绍了Vue和React受控组件的区别小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录背景React 的实现vue3 的实现写法一:直接修改事件参数写法二:通过ref引用 DOMVu

JavaScript中的高级调试方法全攻略指南

《JavaScript中的高级调试方法全攻略指南》什么是高级JavaScript调试技巧,它比console.log有何优势,如何使用断点调试定位问题,通过本文,我们将深入解答这些问题,带您从理论到实... 目录观点与案例结合观点1观点2观点3观点4观点5高级调试技巧详解实战案例断点调试:定位变量错误性能分

Java实现将HTML文件与字符串转换为图片

《Java实现将HTML文件与字符串转换为图片》在Java开发中,我们经常会遇到将HTML内容转换为图片的需求,本文小编就来和大家详细讲讲如何使用FreeSpire.DocforJava库来实现这一功... 目录前言核心实现:html 转图片完整代码场景 1:转换本地 HTML 文件为图片场景 2:转换 H

Java使用jar命令配置服务器端口的完整指南

《Java使用jar命令配置服务器端口的完整指南》本文将详细介绍如何使用java-jar命令启动应用,并重点讲解如何配置服务器端口,同时提供一个实用的Web工具来简化这一过程,希望对大家有所帮助... 目录1. Java Jar文件简介1.1 什么是Jar文件1.2 创建可执行Jar文件2. 使用java

C#使用Spire.Doc for .NET实现HTML转Word的高效方案

《C#使用Spire.Docfor.NET实现HTML转Word的高效方案》在Web开发中,HTML内容的生成与处理是高频需求,然而,当用户需要将HTML页面或动态生成的HTML字符串转换为Wor... 目录引言一、html转Word的典型场景与挑战二、用 Spire.Doc 实现 HTML 转 Word1

Vue3绑定props默认值问题

《Vue3绑定props默认值问题》使用Vue3的defineProps配合TypeScript的interface定义props类型,并通过withDefaults设置默认值,使组件能安全访问传入的... 目录前言步骤步骤1:使用 defineProps 定义 Props步骤2:设置默认值总结前言使用T

Python实现精确小数计算的完全指南

《Python实现精确小数计算的完全指南》在金融计算、科学实验和工程领域,浮点数精度问题一直是开发者面临的重大挑战,本文将深入解析Python精确小数计算技术体系,感兴趣的小伙伴可以了解一下... 目录引言:小数精度问题的核心挑战一、浮点数精度问题分析1.1 浮点数精度陷阱1.2 浮点数误差来源二、基础解决

Java实现在Word文档中添加文本水印和图片水印的操作指南

《Java实现在Word文档中添加文本水印和图片水印的操作指南》在当今数字时代,文档的自动化处理与安全防护变得尤为重要,无论是为了保护版权、推广品牌,还是为了在文档中加入特定的标识,为Word文档添加... 目录引言Spire.Doc for Java:高效Word文档处理的利器代码实战:使用Java为Wo

从入门到精通详解Python虚拟环境完全指南

《从入门到精通详解Python虚拟环境完全指南》Python虚拟环境是一个独立的Python运行环境,它允许你为不同的项目创建隔离的Python环境,下面小编就来和大家详细介绍一下吧... 目录什么是python虚拟环境一、使用venv创建和管理虚拟环境1.1 创建虚拟环境1.2 激活虚拟环境1.3 验证虚