# React组件、组件状态、useState、

# 一、 React 组件介绍

组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:

components.7fbb3771

image-20260416153141821

上图左侧的网页,由一个根组件 Root 开始,在根组件 Root 中包含 Header、Main、Aside子组件,在Main 组件包含了两个 Article 子组件;在 Aside 组件中,包含了 3 个 Item 子组件。

我们再来看下面这个网页结构,用红色框出来的部分,都可以抽离出去作成一个单独的组件,从而达到复用。

16818821754465

在React中,组件本质就是一组 DOM 的封装,用来显示具体功能的UI界面。

一个完整的 React 组件包含HTML、CSS、JS 三部分。当我们需要在多个页面实现相同的功能的 UI 时,就可以把这部分功能的(CSS、HTML、JS)抽离出去,做成一个单独的组件,然后实现复用。

区分模块化与组件化

  • 模块化强调的是把一个大的JS文件拆分成许多具有独立功能的小的JS文件,从而实现代码复用
  • 组件化强调的把一个大的UI界面,拆分成许多具有独立功能的小UI,从而实现UI界面的复用

# 二、React 组件核心基础

# 1、如何创建 React 组件

React 中组件是一个特殊的函数,函数名首字母大写,函数返回值为 JSX,最终被 React 渲染成 DOM 插入到页面中。

// src/App.jsx 文件 

// App 函数为创建的组件
function App(){
    // 返回JSX
	return <div>Hello React !! </div>
}

// 通常我们会导出组件,这样其它文件中就可以使用这个组件了
export default App

在其它页面,我们可以使用以下方式导入组件

import App from "./App.jsx";

# 2、创建 MyButton 组件

在 React 中创建一个 MyButton 组件,该组件包含 HTML、CSS、JS 三个部分,最终会被渲染成一个按钮插入到页面中,如下图:

image-20240415123233591

因为按钮还绑定了click 点击事件,所以点击按钮会弹出提示框。

// 导入CSS
import './App.css'

// 创建MyButton组件,组件最终会被渲染出一个如上图所示的按钮插入到页面中
function MyButton(){
    // js代码,用于处理按钮点击事件
    const handleClick=()=>{
        alert('点击提交后操作')
    }
    // 返回JSX
    return <button className='primary' onClick={handleClick}>提交</button>
}

export default MyButton
/* App.css 样式 */
.primary {
    width: 100px;
    height: 50px;
    background-color: tomato;
    color: #fff;
    font-size: 18px;
    border-radius: 5px;
}

# 3、根组件

  • 在 React 中,根组件是挂载到真实 DOM 节点上的顶层组件,负责管理整个应用的顶层状态与结构。
  • 根组件作为顶层组件,直接作为 render 方法的参数传入,最终被渲染成一个真实的 DOM 挂载到 HTML 页面中指定的容器元素(通常是一个具有 id="root"<div> 节点)内部
// src/main.jsx
import { createRoot } from "react-dom/client";
// 导入组件
import App from "./App.jsx";

createRoot(document.getElementById('root')).render(
           <App/>   // APP为根组件
          )

# 3.1、单个根组件:单页面应用

React 框架主要用于构建单页应用,所以在 index.html 页面只有一个根节点<div id='root'></div>,创建一个 App 根组件即可,其它内容均作为根组件 App 的子组件

代码示例

  • index.html页面(项目首页)中,创建一个根节点 <div id="root"></div>
<body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
</body>
  • main.jsx 中创建一个根组件,并将根组件渲染到的根节点<div id="root"></div>中。
// src/main.jsx
import { createRoot } from "react-dom/client";
// 根组件
function App(){
    return <div> 根组件,其它子组件被添加到这里</div>
}
createRoot(document.getElementById('root')).render(
           <App/>   // APP为根组件
          )

# 3.2、多个根组件:旧项目改造

如果我们原先有一非 React 项目,在原有代码不变的基础上,想用 React 添加新的功能,就需要创建多个根组件与多个根节点,来渲染不同内容

代码示例

以下代码展示了在一个页面中创建了两个根节点,然后分别向两个根节点中渲染不同的根组件来展示不同内容。

  • index.html页面中,创建两个根节点
<body>
    <!-- 省略大部分内容,项目原有代码-->
    
    <!-- 在原有项目基础上,新增加两个版块内容 -->
    <div id="news"></div>
    <div id="products"></div>
    <script type="module" src="/src/main.tsx"></script>
</body>
  • main.jsx 中创建两个根组件,并将根组件渲染到对应的根节点。
import { createRoot } from "react-dom/client";

// 创建根组件-展示新闻中心版块
function MyNew() {
    return <div>新闻中心版块</div>;
}

// 创建根组件-展示产品版块
function MyProduct() {
    return <div>产品中心版块</div>;
}

// 渲染新闻中心
createRoot(document.getElementById("news")).render(<MyNew />);

                                                   // 渲染产品中心
                                                   createRoot(document.getElementById("products")).render(<MyProduct />);

最终渲染后的DOM如下:

<body>
    <div id="news"><div>新闻中心版块</div></div>
    <div id="products"><div>产品中心版块</div></div>
</body>

# 4、什么是子组件

  • 子组件本身也是组件,其写法与根组件的写法一模一样。
  • 子组件 与 根组件唯一的不同,在于渲染的位置不同,子组件只能被嵌套在根组件或其它子组件中使用。

代码示例

以下代码中组件说明

  • App 为根组件,也是 Hello 组件的父组件
  • Hello 和 Butto均为子组件,但 Hello 组件被认为是 Button 组件的父组件
import { createRoot } from "react-dom/client";

// 子组件
function Hello() {
  return (
    <>
      <div>Hello React!</div>
      <Button /> {/* 子组件内部嵌套子组件 */}
    </>
  );
}

// 子组件
function Button() {
  return <button>提交</button>;
}

// App 根组件
function App() {
  return (
    <div>
      {/* 子组件嵌套在根组件中 */}
      <Hello></Hello>
    </div>
  );
}

createRoot(document.getElementById("root")).render(<App />);

最终渲染结果如下:

GIF2024-4-1514-47-57

# 5、组件拆分到单独文件

在实际的开发中,我们不会把所有的组件写在一个文件中,而是把每个组件定义在一个单独的 .jsx.tsx 文件中,然后在需要用到该组件的页面导入该组件。

将上面示例中的组件拆分到单独的文件中,拆分后文件结构如下:

src
├─ App.tsx   // App组件
├─ components
│  ├─ Button.tsx  // Butto组件
│  └─ Hello.tsx  // Hello 组件
├─ main.tsx  // 项目JS入口文件

每个文件中内容如下:

  • Button.jsx文件中创建 Button 组件 , 并对外默认导出 Button 组件
// Button 组件
function Button() {
    return <button>提交</button>;
}
// 默认导出组件
export default Button
  • Hello.jsx 文件中创建 Hello 组件,并导入Button组件,将其嵌套在 Hello 组件中。最后对外导出 Hello 组件
import Button from "./Button"
function Hello(){
    return (
        <>
        <div>Hello React!</div>
        <Butto/>
        </>
    )
}

// 默认导出组件
export default Hello
  • App.tsx文件中创建App组件,并导入Hello组件,将其嵌套在App组件中。最后对外导出App组件
import Hello from "./components/Hello"
// 根组件
function App(){
    return <div>
        <Hello></Hello>
    </div>
}
// 默认导出组件
export default App
  • main.tsx文件中,导入App根组件,并将其渲染到页面的根节点#root容器中

import { createRoot } from "react-dom/client";
import App from "./App";

createRoot(document.getElementById("root")).render(<App />);

# 6、实战应用:组件化实现页面布局

本小节,我们将在 React 中利用组件化实现以下布局

components.7fbb3771

实现思路

  • 首先我们在 Root 根组件中实现整体的布局效果。
  • 然后将 Root 根组件中代码拆分到不同的子组件中
  • 最后将所有组件组合在一起,完成最终效果

# 6.1、在 Root 根组件中实现整体的布局效果

  • App.jsx 文件中创建 App 根组件(App 为上图中的 Root 根组件)
import './App.css'
// 根组件
function App(){
    return   (
        <div id="app">
            {/* 头部,相当于子组件Header */}
            <div className="header"> 头部</div>

            {/* content start */}
            <div className="content">
                
                
                {/* 相当于子组件Main*/}
                <div className="main">
                    <div className="article1">article1</div>
                    <div className="article2">article2</div>
                </div>

                {/* aside,相当于子组件Aside */}
                <div className="aside">
                    <div className="item item1">item1</div>
                    <div className="item item2">item2</div>
                    <div className="item item3">item3</div>
                </div>
            </div>
            {/* content end */}
        </div>
    )

}

export default App
  • App.css 文件中,定义了App根组件中所需要的CSS样式
html,
    body {
        margin: 0;
        padding: 0;
        font-size: 30px;
        text-align: center;
        background-color: #ddd;
    }
.header {
    height: 100px;
    background-color: plum;
}

.content {
    display: flex;
}
.main {
    width: 70%;
}
.main .article1,
    .main .article2 {
        height: 310px;
        margin: 20px;
        background-color: skyblue;
    }
.main .article2 {
    background-color: khaki;
}

.aside {
    width: 30%;
}
.aside .item {
    margin: 20px;
    height: 200px;
}
.aside .item1 {
    background-color: orange;
}
.aside .item2 {
    background-color: skyblue;
}
.aside .item3 {
    background-color: tomato;
}
.box {
    width: 100px;
    height: 100px;

    background-color: black;
}

App根组件最终渲染效果如下:

image-20240415160237893

# 6.2、将Root(App)根组件拆分成多个子组件

所有代码都写在一个组件中,肯定是不方便维护,更没法实现复用。所以需要将 App 组件按功能拆分成许多个小组件,这样维护和复用就很方便了。

拆分后的结构如下

将App组件中代码拆分到不同的子组件中去,拆分后整个应用结构如下图

src
├─ App.css
├─ App.jsx  // App根组件(指代上图中Root根组件)
├─ components
│  ├─ aside   // Aside组件
│  │  ├─ aside.css
│  │  ├─ Index.jsx
│  │  └─ Item  // Item组件
│  │     ├─ Index.jsx
│  │     └─ item.css
│  ├─ header  // Header组件
│  │  ├─ header.css
│  │  └─ Index.jsx
│  └─ main  // Main组件
│     ├─ article  // Article组件
│     │  ├─ article.css
│     │  └─ Index.jsx
│     ├─ Index.jsx
│     └─ main.css
└─ main.jsx
  • Header.jsx 文件中创建 Header 组件,并引入 css 文件
// 导入css
import './header.css'
// 创建组件
function Header(){
    return    <div className="header"> 头部</div>
}
// 导出组件
export default Header
/* header.css */
.header {
  height: 100px;
  background-color: plum;
}
  • Main.tsx 文件中创建 Main 组件
import './main.css'
import Article from './article/Index'
function Main(){
    return (
            <div className="main">
                <Article/>
                <Article/>
            </div>
            )  
}

export default Main
.main {
    width: 70%;
}
  • Aside.jsx 文件中创建 Aside 组件
import Item from "./Item/Index"
import './aside.css'
function Aside(){
    return <div className="aside">
        <Item/>
        <Item/>
        <Item/>
    </div>
}

export default Aside
.aside {
  width: 30%;
}
  • Article.jsx 文件中创建 Article 组件
import './article.css'
function Article(){
    return <div className="article1">article1</div>
}

export default Article
.main .article1,
.main .article2 {
    height: 310px;
    margin: 20px;
    background-color: skyblue;
}
.main .article2 {
    background-color: khaki;
}
  • Item.tsx 文件中创建 Item 组件
import './item.css'

function Item(){
    return <div className="item item1">item1</div>
}
export default Item
.aside .item {
    margin: 20px;
    height: 200px;
}
.aside .item1 {
    background-color: orange;
}
.aside .item2 {
    background-color: skyblue;
}
.aside .item3 {
    background-color: tomato;
}

# 6.3、组件嵌套组合

App 组件拆分后,将拆分出去的子组件与根组件嵌套组合在一起,完成最终效果

// App.tsx
import './App.css'
import Aside from './components/aside/Index'
import Header from './components/header/Index'
import Main from './components/main/Index'
// 根组件
function App(){
    return   (
        <div id="app">
            {/* 头部,相当于子组件Header */}
            {/* <div className="header"> 头部</div> */}
            <Header/>

            {/* content start */}
            <div className="content">
                {/* 相当于子组件Main*/}
                <Main/>
                {/* <div className="main">
                <div className="article1">article1</div>
                <div className="article2">article2</div>
            </div> */}

                {/* aside,相当于子组件Aside */}
                <Aside/>
                {/* <div className="aside">
                <div className="item item1">item1</div>
                <div className="item item2">item2</div>
                <div className="item item3">item3</div>
            </div> */}
            </div>
            {/* content end */}
        </div>
    )

}

export default App

最终渲染效果如下:

image-20240415163245622

如何让相同的组件实现不同的背景效果和显示不同的内容,在后面组件间通信时会讲到。在这里大家只需要了解如何拆分组件,及组合组件即可

# 7、组件的点标记写法

在我们使用一些第三方组件库时,通常会看到如下所示的方式来使用一个组件(组件名以点分隔)

<Checkbox.Group/> {/*ant design 组件库的多选框组件*/}

接下来我们讲解两种组件点标记写法

  • 对象写法
  • 函数写法

# 7.1、对象写法

  • 定义一个对象,对象名以大写字母开头,相当于命名空间
  • 在对象中定义的方法就相当于组件,所以方法名要大写字母开头
// src/components/Ibc.jsx 文件
// Ibc相当于命名空间
const Ibc={
    // Hello组件
    Hello(){
        return <div>Ibc.Hello</div>;
    }
}
export default Ibc
  • 在 App 组件中使用上面定义的 Hello 组件
import Ibc from "./components/Ibc";
export default function App() {
  return (
    <div>
      <Ibc.Hello></Ibc.Hello>
    </div>
  );
}

渲染效果

image-20260410172654749

# 7.2、函数写法

  • 定义一个函数式组件
  • 给函数添加静态方法
// 函数式组件,Ibc 本身是一个组件,同时也相当于命名空间
const Ibc = () => {
  return <div>Ibc 组件 </div>;
};

// 给Ibc函数添加静态方法Hello,该静态方法Hello就是相当于一个函数式组件
Ibc.Hello = function () {
  return <div>Ibc.Hello</div>;
};

// 导出 
export default Ibc;
  • 在 App 组件中使用上面定义的 Ibc 和 Hello 组件
import Ibc from "./components/Ibc";
export default function App() {
  return (
    <div>
      <Ibc></Ibc>
      <Ibc.Hello></Ibc.Hello>
    </div>
  );
}

image-20260410172549511

# 三、组件的状态 state

# 1、什么是组件的状态 state

在编程中,状态(state)指的是一个可以随着时间变化的数据,并且数据变化后会驱动视图的变化。

为了帮助你更好的理解状态,我将拿状态与普通变量作对比来学习。

**组件的状态(state):**指的是组件内部的可变数据,并且当数据发生变化时,会触发组件的重新渲染。

**组件的普通变量:**指组件内部用来保存数据的变量,但数据发生变化时,并不会触发组件的重新渲染。

# 1.1、普通变量:数据更新无法驱动视图变化

以下App组件中的count就是一个普通变量

const App=()=>{
    // 普通变量
    let count=1;
    // 事件处理函数
    const handleClick=()=>{
        // 更新count的值
        count++;
        console.log("count:",count)
    }
    return (
        <button onClick={handleClick}>
            {count}
        </button>
    )
}

export default App

点击按钮更新 count 变量的值,页面数字并无变化

GIF2024-4-1016-16-08

通过上图可以看到,点击按钮时,变量的值一直有变化,但组件并不会重新渲染,页面效果不会有任何变化

# 1.2、state 状态:数据变化视图同步更新

React 为我们提供了useState Hook 来创建组件状态(State)变量。

Hook 是让你在函数组件中使用 state 和其他 React 特性的函数

语法

// 创建状态变量 state 
const [state, setState] = useState(initialState)

参数

  • initialState :状态变量 state 初始值

返回值

useState 返回一个由两个值组成的数组

  • state :创建的状态变量。首次渲染时,它的值为 initialState
  • setState:set函数,他可以让你更新 state 值并触发重新渲染。

创建一个 state 状态变量,需要以下几步

  • 在文件顶部从 react 包 中导入 useState 函数
// 1、导入useState
import { useState } from "react";
  • 调用 useState 创建 state 状态变量,并解构其返回的两个值
    • count 是一个状态变量,其初始值为 1,useState(1) 的参数就是 count 的初始值
    • setCount 是对应的 setter 函数,用来修改 count 状态变量的值
// 2、创建状态变量  count为状态变量,setCount方法用来修改状态变量的值
const [count,setCount] = useState(1)
  • 调用setCount方法更新Count状态变量的值
// 3、更新状态变量值
setCount(count+1)  

完整代码示例

// 1、导入useState
import { useState } from "react";
const App=()=>{
    // 2、创建状态变量
    const [count,setCount]=useState(1)
    
    // 事件处理函数
    const handleClick=()=>{
        // 3、更新状态变量值
        setCount(count+1)
        // 打印count的值,打印结果可能和你想的不一样,稍后会介绍
        console.log("count:",count)
    }
    return (
        <div>
            <button onClick={handleClick}>{count}</button>
        </div>
    )
}

export default App

渲染后效果

GIF2024-4-1016-45-17

通过上图可以看到,点击按钮时,状态变量的值一直有变化,同时页面视图也同步更新了。

# 2、state 状态是如何更新视图的

当组件的状态(state)值发生变化后,就会触发组件的一次重新渲染。但状态的更新是异步的,会等到事件处理函数中的所有同步代码都运行完毕后再批量处理你的状态(state)更新。

// 1、导入useState
import { useState } from "react";
const App=()=>{
    // 2、创建状态变量
    const [count,setCount]=useState(1)

    // 3、事件处理函数
    const handleClick=()=>{
        // 4、更新状态变量值
        setCount(count+1)
        // 打印count的值,打印结果可能和你想的不一样,具体原因,见下面说明
        console.log("count:",count)
    }
    return (
        <button onClick={handleClick}>
            {count}
        </button>
    )
}

export default App
  • 首次渲染App组件

    因为你将 1 作为了 count 的初始值传递给了useState,所以React 记住了1 是 count 状态变量的最新值,页面渲染后效果是image-20240410170649081

  • 更新state

    • 当用户点击按钮时,就会调用handleClick函数,在其内部会调用setCount(count+1)更新count状态变量的值。
    • 但是React会等到事件处理函数中的所有代码都运行完毕再处理你的状态(state)更新(state的更新是异步的)
    • 所以,会先在控制台打印 count:1,然后再更新 count 状态变量的值为2,此时React会记住 2 是count状态变量的最新值
  • 第二次渲染

    • count状态变量的值更新为2后,就会触发 App 组件的重新渲染,相当于重新调用 APP 函数。
    • 虽外React 仍然看到的是const [count,setCount]=useState(1),但 React 内部记住 count 的最新值是 2,于是这里得到的 count 值是2。最终在页面呈现的效果是image-20240410171708992

# 3、state 如同一张快照

也许 state 变量看起来和一般的可读写的 JavaScript 变量类似。但 state 在其表现出的特性上更像是一张快照

  • 设置它不会更改你已有的 state 变量,但会触发重新渲染。
  • 在重新渲染中,React 会为你提供该次渲染的一张 state 快照

代码示例

import { useState } from "react";
const App=()=>{
    // 状态变量
    const [count,setCount]=useState(1)
    // 事件处理函数
    const handleClick=()=>{
        // 更新状态变量值
        setCount(count+5)
        // setCount(count+3) //首次渲染 count=1 setCount(1+3)
        // setCount(count+2) //首次渲染 count=1 setCount(1+2)
        // 定时器
        setTimeout(()=>{
            console.log("定时器",count) // 1
        },2000)
    }
    return (
        <button onClick={handleClick}>
            {count}
        </button>
    )
}

export default App

以上代码渲染后结果如下图:

GIF2024-4-1018-53-39

原理分析

流程 count的值 定时器中count值 补充说明
首次渲染 1 1
第一次更新count(用户点击按钮) 更新为:1+5=>6 2秒后控制台输出1 定时器是在首次渲染时创建的,其内用的到count是首次渲染时count的值1(count 的值如同快照一样记录下来,并不会因为后面count 的值变为 6了,而发生改变)。
第二次渲染 6 6
第二次更新count(用户点击按钮) 更新为:6+5=>11 (4秒后控制台输出6) 定时器是在第二次渲染时创建的,其内用的到count是第二渲染时count的值6(count的值如同快照一样记录下来,并不会因为后面count的值变为11了,而发生改变)。
第三次渲染 11 11
  • 首次渲染

    • 因为你将1作为了count的初始值传递给了useState,所以React记住了1是count状态变量的最新值。此时组件内部所有使用到count状态变量的地方,他的值都是1,就如同count是一张快照,不会在本次渲染中发生改变。
    count的值
    setCount(count+5) 1
    console.log("定时器",count) 1
    • 所以setCount(count+5)中的count值为1,等同于setCount(1+5)被快照下来了
    • 所以setTimeout回调中的 count 值为1,等同于console.log("定时器",1)被快照下来了。
    • 所以本次渲染后的效果是image-20240410170649081
  • 第一次更新state

    • 首次渲染完成后,用户点击按钮,就会调用handleClick函数,在其内部调用setCount(count+5)更新count的值。因为state的更新是异步的,所以会等handleClick中所有同步代码执行完后再更新state。
    • 执行到setTimeout时,因定时器也是异步,所以并不会立即执行定时器中的代码。
    • 所以最终还是会先执行setCount(count+5)代码更新count的值,即setCount(1+5)得到count的最新值为 6。此时React会记住6是count状态变量的最新值
  • 第二次渲染

    • count的值更新后就会触发组件的重新渲染,App函数会重新被调用。
    • 因为React记住了count的最新值为6,所以此时const [count,setCount]=useState(1)代码返回的count值为6。
    • 在本次渲染中(第二次),count的值为6,如同一张快照被记录下来。所以在本次渲染中,组件内部所有用到count的地方,他的值都是6。
    count的值
    setCount(count+5) 6
    console.log("定时器",count) 6
    • 所以setCount(count+5)中的count值为6,等同于setCount(6+5)被快照下来了
    • 所以setTimeout回调中的count值为6,等同于console.log("定时器",6)被快照下来了。
    • 所以本次渲染后的效果是 image-20240410185203622
  • 2秒后,组件首次渲染中定时器被执行:即console.log("定时器",1),在控制台输出1。

  • 第二次更新state

    第二次渲染完成后,点击按钮会触发count的更新,即setCount(6+5)得到count的值为11,此时React会记住count的最新值为11。

  • 第三次渲染

    • count的值更新后就会触发组件的重新渲染,App函数会重新被调用。
    • 因为React记住了count的最新值为11,所以此时const [count,setCount]=useState(1)代码返回的count值为11。
    • 在本次渲染中(第三次),count的值为11,如同一张快照被记录下来。所以在本次渲染中,组件内部所有用到count的地方,他的值都是6。
    count的值
    setCount(count+5) 11
    console.log("定时器",count) 11
    • 所以setCount(count+5)中的count值为11,等同于setCount(11+5)被快照下来了
    • 所以setTimeout回调中的count值为11,等同于console.log("定时器",11)被快照下来了。
    • 所以本次渲染后的效果是 image-20240410185219878
  • 4秒后,组件第二次渲染中定时器被执行,即console.log("定时器",6),在控制台输出6

依此类推....

# 4、state 的自动批处理

如果我们在一次点击事件中更新多个状态(State)变量的值

  • React 并不会每更新一个状态变量的值就重新渲染组件

  • 而是会等到所有状态的值全部更新完成后,再一次性的渲染组件

我们称这种特性为自动批处理。

import { useState } from "react";
const App=()=>{
    console.log('组件重新渲染就会被打印')
    // 状态变量
    const [count,setCount]=useState(1)
    const [msg,setMsg]=useState('hello')
    // 事件处理函数
    const handleClick=()=>{
        // 以下多次更新状态的值,但最终只会触发一次组件的重新渲染
        // 在本次渲染中,count的值如同一张快照,所以任何用到count的地方,值都是1
        setCount(count+1); // 1 + 1 => 2
        setCount(count+5); // 1 + 5 => 6
        setCount(count+2); // 1 + 2 => 3    最终count的值为被修改为3
        setMsg('Hello React')
        // 等count的值更新为3、msg的值更新为“Hello React”后,才会触发组件的重新渲染。
    }
    return (
        <div>
            <button onClick={handleClick}>{msg}-{count}</button>
        </div>
    )
}

export default App

在上面代码的handleClick函数中,其内部对状态变量的值做了4次更新,但每当我们点击按钮后,控制台只输出了一次“组件重新渲染就会被打印”,并不会输出4次。

GIF2024-4-1019-25-37

这是因为React内部会对状态更新自动批处理

# 5、state 更新的函数写法

如果我们想在一次渲染中,基于当前 state 计算出新值,并且能立即拿到这个新值用于后续逻辑(而不需要等待组件重新渲染后才能拿到),我们可以选择函数式更新的写法:

setCount(c=>c+5)  // c=>c+5 为更新函数 

代码示例

import { useState } from "react";
const App=()=>{
    // 状态变量
    const [count,setCount]=useState(1)
    // 事件处理函数
    const handleClick=()=>{
        // 以下多次更新状态值,但最终只会触发一次组件的重新渲染
        // 首次渲染时,count =1
        setCount(count+5); // 1+5 =>6    添加到更新队列,count在队列中值为6
        setCount(20) ; // 20    添加到更新队列,覆盖前面的 6,count值为20
        setCount(count+1); // 1+1 =>2   添加到更新队列,覆盖前面的20,count值为2
        setCount(c=>c+10); // 2+10=>12  添加 c=>c+10更新函数到队列,计算后得到count值为12,覆盖前面的2
        setCount(c=>{
            console.log("n",c) // 12
            return c+5  // 17   添加 (c)=>{ ... return c+5}更新函数到队列,计算后得到count值为17,覆盖前面的12
        })
    }
    return (
        <div>
            <button onClick={handleClick}>{count}</button>
        </div>
    )
}

export default App

GIF2024-4-1112-51-42

代码执行流程分析

  • 当组件首次渲染时,执行到const [count,setCount]=useState(1),返回的count值为1,此时React内部记住了,当前 count 的最新值为1,本次渲染中,所有用到count 的地方,值都为1

  • 页面渲染结果为:image-20240411130402598

  • 更新 state 值:点击按钮,开始执行handleClick中的代码,执行流程如下:

    执行代码 count或c 的值 更新队列 返回值
    setCount(count+5) 1 count+5 1+5=6
    setCount(20) 1(未使用) 替换为“20” 20
    setCount(count+1) 1 count+1 1+1=2
    setCount(c=>c+10) 2(c为队列中最新的count值) c =>c+10 2+10=12
    setCount(c=>{ /* ...*/ return c+5 }) 12(c为队列中最新的count值) c => c+5 12+5=17
  • 最终count的值被更新为了17,然后触发了组件的重新渲染,最终页面渲染结果为:image-20240411130350851

# 5.1、命名惯例

通常可以通过 state 变量的单词首字母缩写来命名更新函数的参数

setCount(c=>c+1)
setMsg(m=>"Hello"+m)
setNumber(n=>n+3) 

# 5.2、练习题

运行以下代码,当我们第一次点击按钮后,页面中渲染出的 count 值是多少?

import { useState } from "react";
const App=()=>{
    // 状态变量
    const [count,setCount]=useState(1)
    // 事件处理函数
    const handleClick=()=>{
        setCount(10); 
        setCount(count+4) ;
        setCount(c=>c+5);
        setCount(20) 
    }
    return (
        <div>
            <button onClick={handleClick}>{count}</button>
        </div>
    )
}

export default App

答案解析

const [count,setCount]=useState(1)  // count = 1 

// 事件处理函数
const handleClick=()=>{
    // 以下多次更新状态值,但最终只会触发一次组件的重新渲染
    setCount(10); //      添加到更新队列,count在队列中值为10
    setCount(count+4) ; // =>1+4=5    添加到更新队列,count值为5,覆盖前面的10
    setCount(c=>c+5); // 5+5 =>10   添加到更新队列,count值为10,覆盖前面的5
    setCount(20) //  添加到更新队列,count值为20,覆盖前面的10
}

页面中渲染出的 count 值是20

# 6、计算变量

如果一个变量的值是根据另一个或多个状态变量 或props(后面会讲到)计算而来,这个变量被称为计算变量

React中的计算变量,类似于Vue中的计算属性。

单位换算:根据米,计算得到千米

GIF2026-4-1019-53-27

import { useState } from 'react'

const App=()=>{
    // 状态变量
    const [km,setKm]=useState(1);
    // 计算变量
    const m=km*1000;

    setTimeout(()=>{
        // 修改状态变量的值,会触发组件重新渲染,组件重新渲染时,计算变量m的值也会被更新。
        setKm(2.5);
    },3000)

    return (
        <div>
            {km}千米 = {m}</div>
    )
}

export default App

# 7、组件状态 state 的隔离性

组件状态的隔离性是指:每个组件的 state 都是独立的,互不影响

代码示例

我们在 App 组件中渲染了 Count 组件 3 次,但每个 Count 组件内部的状态变量 num 的值都是互相独立的。

所以当我们点击任何一个按钮时,只有当前按钮的数字会加 1,其它两个按钮是不会受影响的。

import { useState } from "react"

// Count组件
function Count(){
    const [count,setCount]=useState(0)
    return <button onClick={()=>setCount(count+1)}>{count}</button>
}

// App 根组件
function App(){
    return (
        <div>
            <Count/> 
            <Count/> 
            <Count/>
        </div>
    )
}

export default App

渲染后效果如下:

GIF2024-4-1519-05-13

# 8、什么是 Hook (钩子)

定义:在 React 中,所有以 use 开头的函数都称为 Hook,例如 useStateuseEffect 等。

本质:Hook 是特殊的函数,它们能让你“钩入”(hook into)React 的内部特性(例如:状态(state)、生命周期、上下文等)。

与普通工具函数的区别

对比项 Hook 普通工具函数
使用范围 只能在 React 组件或自定义 Hook 中调用 可在任何 JavaScript 环境中使用
依赖 内部依赖 React 提供的方法或其他 Hook 无框架依赖
命名规则 必须以 use 开头 无要求
调用位置 需要遵守固定的调用规则 无限制

一句话总结:

Hook 是 React 专属的特殊函数,用于连接 React 的核心功能,使用时必须遵守固定的调用规则。

# 9、Hook 的调用规则

Hook 的调用必须遵守顺序一致性原则,因此 Hook 的调用需要遵循以下规则:

  • 只能在顶层调用:在函数组件或自定义 Hook 的最顶层调用
  • 禁止在循环、条件语句、嵌套函数中调用

为什么要遵守?

  • React 依赖 Hook 的调用顺序来匹配每个状态。如果顺序或数量在两次渲染间发生变化,React 就无法正确关联状态,导致 bug。

  • 在 **循环、条件语句或嵌套函数中调用 Hook ** ,就会出现,有时调用有时不调用情况,造成顺序或数量发生变化,从而引发 bug。

注意useState 就是一个 Hook,所以使用时也要遵守以上规则。

错误用法

// ❌ 错误:在条件语句中调用 
if(show){
    const [msg,setMsg]=useState('Hello')
    }

const App=()=>{
    const fn=()=>{
        // ❌ 错误:在嵌套函数内部调用
        const [count,setCount]=useState(0)
        }
    }

// ❌ 循环语句中调用
function Bad3({ items }) {
    for (let i = 0; i < items.length; i++) {
        const [value, setValue] = useState('')   // 危险(循环次数可能变化)
        }
}

正确用法

const App=()=>{
	// ✅ 正确: 在组件内的最顶层调用Hook
    const [msg,setMsg]=useState('Hello')
}

// 自定义Hook ,自定义Hook我们后面会讲到,这里做了解
const useCustom=()=>{
    // ✅正确: 在 Hook 的最顶层调用另一个Hook
    const [msg,setMsg]=useState('Hello')
}

# 四、更新 state 中对象与数组

本小节我们将学习在 React 中如何正确地更新 state 中的对象和数组。

你将学习到如下内容:

  • 什么是 mutation
  • 将 state 的值视为只读的
  • 更新 state 中的对象
  • 更新 state 中数组

# 1、什么是 mutation

在 React 中,mutation 指的是直接修改 state 中的数据(对象或数组)的内容,而不是创建一个新的副本。mutation 不会触发组件重新渲染

❌ 错误示例:引发 mutation

下面示例代码中,我们直接将对象的属性x , y的值改变了。当你这样做时,就制造了一个 mutation。 其本质是 修改了当前 state 的快照的值,即为快照,那肯定值是不能变的了。所以在 React 中,这样做是不合理的

import { useState } from "react"
const App=()=>{
    const [position,setPosition]=useState({x:0,y:0})

    const handleClick=()=>{
        // ❌ 直接修改对象的属性值  制造了 mutaition 
        position.x=10
        position.y=10

        // 以下更新并不会触发重新渲染,因为position是一个对象,React内部在判断对象是否相同时判断的是地址,因为更新前和更新后是同一个对象,地址一样,所以React内部并不会认为值发生了改变,即不会触发组件的重新渲染。
        setPosition(position)
    }


    return (
        <button onClick={handleClick}>
            {position.x} - {position.y}
        </button>
    )
}

export default App

严格来说 React state 中存放的对象是可变的,你可以通过操作对象属性修改对象的值,但你应该像处理数字、布尔值、字符串一样将它们视为不可变的(只读的)。因此你应该替换它们的值,而不是对它们进行修改。

# 2、将 state 的值视为只读的

在React中,你应该把 state 变量的值当做是不可变的(immutable)。

  • 如果 state 中保存的是基本数据类型,其本身就是不可变的。

  • 如果 state 中保存的是对象,你也应该将其视为不可变的,只能读取不能修改对象

如果要修改的 state 变量的值是一个对象,我们应该创建一个新的对象,将 state 变量的值更改为新创建的对象。

import { useState } from "react"
const App=()=>{
    const [position,setPosition]=useState({x:0,y:0})

    const handleClick=()=>{
        // ✅ 创建一个新的对象,将State的值更新为新创建的对象
        setPosition({x:10,y:10})
    }

    return (
        <button onClick={handleClick}>
            {position.x} - {position.y}
        </button>
    )
}

export default App

想要了解更多,查阅React官方资料:为什么在 React 中不推荐直接修改 state? (opens new window)

# 3、更新 state 中的对象

如果 state 中保存的值是一个类似于下面的对象

const [userInfo,setUserInfo]=useState({  
    "name": "清心",  
    "age": 30,  
})

当你只想修改 userInfo.age的值时,你不应该按如下方式修改,因为会产生 mutation

// ❌ 错误写法
userInfo.age=36

你应该按下面的方式来修改,利用对象的展开运算符来帮助我们快速创建一个新对象,将新对象作为 state 的新值

// ✅ 创建一个新的对象,将state的值更新为新创建的对象
setUserInfo({...userInfo, age:36 })

完整版代码

import { useState } from "react"
const App=()=>{
    const [userInfo,setUserInfo]=useState({  
        "name": "清心",  
        "age": 34,  
    })
    const handleClick=()=>{
        // 创建一个新的对象,将State的值更新为新创建的对象
        setUserInfo({...userInfo,age:36})
    }

    return (
        <div>
            <p>{userInfo.name}的年龄{userInfo.age}</p>
            <button onClick={handleClick}>更新</button>
        </div>

    )
}

export default App

GIF2024-4-1117-25-44

# 3.1、更新一个嵌套的对象

如果 state 中保存的值是一个类似于下面这样的嵌套对象

const [userInfo,setUserInfo]=useState({  
    "name": "清心",  
    "age": 30,  
    "address": {  
        "province": "陕西省",  
        "city": "西安",  
        "area": "雁塔区"
    }
})

当你只想修改userInfo.address.area的值时,你不应该按如下方式修改,因为会产生 mutation

// 错误方式
userInfo.address.area="高新区"

你应该按下面的方式来修改

// 创建一个新的对象,将State的值更新为新创建的对象
// 想要更新嵌套对象,你需要从你更新的位置开始自底向上为每一层都创建新的拷贝。

// 复制地址对象
const newAddress={...userInfo.address,area:"高新区"}
// 复制用户对象
const newUserInfo={...userInfo,address:newAddress}
// 用新创建的用户对象更新state的值
setUserInfo(newUserInfo)

// 或 以下简写形式
setUserInfo({
    ...userInfo, // 复制其它字段数据
    address:{ // 替换address字段
        ...userInfo.address,  // 复制adress中其它字段数据
        area:"高新区", // 修改area字段的值
    }
})

完整版代码

import { useState } from "react"
const App=()=>{
    const [userInfo,setUserInfo]=useState({  
        "name": "清心",  
        "age": 30,  
        "address": {  
            "province": "陕西省",  
            "city": "西安",  
            "area": "雁塔区"
        }
    })
    const handleClick=()=>{
        // 创建一个新的对象,将State的值更新为新创建的对象
        /*
        // 复制地址对象
        const newAddress={...userInfo.address,area:"高新区"}
        // 复制用户对象
        const newUserInfo={...userInfo,address:newAddress}
        // 用新创建的用户对象更新state的值
        setUserInfo(newUserInfo)
        */

        // 或以下简写形式
        setUserInfo({
            ...userInfo, // 复制其它字段数据
            address:{ // 替换address字段
                ...userInfo.address,  // 复制adress中其它字段数据
                area:"高新区", // 修改area字段的值
            }
        })

    }

    return (
        <div>
            <p>
                {userInfo.name}所在地址
                {userInfo.address.city}-{userInfo.address.area}
            </p>
            <button onClick={handleClick}>更新</button>
        </div>

    )
}

export default App

GIF2024-4-1117-28-26

# 3.2、使用 Immer 编写简洁的更新逻辑

immer (opens new window) 是一个非常流行的第三方库,它可以让你使用直接操作对象的语法来修改不可变对象

state 变量值为对象或数组时,这个对象和数组应当看作是不可变的,被称为不可变对象

immer 库具体使用步骤

  • 第一步:安装 immer依赖包
# immer是一个用于处理不可变数据的JS库,其核心实现是利用 ES6 的Proxy来创建一个当前状态的代理(draft state)。对代理所做的任何修改都会被记录下来,并最终生成一个新的状态
# use-immer是一个基于immer 的React Hook。它使得在 React 组件中管理状态变得更加简单和直观
npm i immer use-immer
  • 第二步:导入 useImmer 这个 Hook
import { useImmer } from 'use-immer'
  • **第三步:**调用 useImmer 创建状态变量
// userInfo 为状态变量   setUserInfo 方法,用于更新状态变量的值
const [userInfo,setUserInfo]=useImmer({  
    "name": "清心",  
    "age": 30,  
    "address": {  
        "province": "陕西省",  
        "city": "西安",  
        "area": "雁塔区"
    }
})
  • 第四步:调用 setUserInfo 方法,更新状态变量的值。

  • setUserInfo 的参数是一个函数,这个函数会接收当前状态的代理你可以在这个代理上直接修改状态

  • 在函数执行完成后,immer 会记录所有的修改,并生成一个新的状态来替换当前状态

// draft 为当前状态 userInfo 的代理(proxy)对象
setUserInfo((draft)=>{
    // 可以通过代理对象直接修改属性值
    draft.address.area="高新区"
})

完整版代码

import { useImmer } from "use-immer"
const App=()=>{
    const [userInfo,setUserInfo]=useImmer({  
        "name": "清心",  
        "age": 30,  
        "address": {  
            "province": "陕西省",  
            "city": "西安",  
            "area": "雁塔区"
        }
    })
    const handleClick=()=>{
        // draft为状态userInfo的代理(proxy)对象
        setUserInfo((draft)=>{
            // 可以通过代理对象直接修改属性值
            draft.address.area="高新区"
        })
    }

    return (
        <div>
            <p>
                {userInfo.name}所在地址
                {userInfo.address.city}-{userInfo.address.area}
            </p>
            <button onClick={handleClick}>更新</button>
        </div>

    )
}

export default App

GIF2024-4-1117-28-26

# 4、更新 state 中数组

数组也是对象类型,同样,在 React state 中,你需要将它视为不可变的,只能读取不能修改。

假设state 中保存了如下数组

const [nums,setNums]=useState([1,2,3,4,5])

现在你想在数组尾部添加一个元素:

❌ 错误做法:直接修改原数组

你不能用以下方式直接修改数组,因为这种操作会改变原数组(产生 mutation)

// ❌ 错误:push 会直接修改原数组(产生 mutation)
// 同时更新前后,state状态的值都是同一个数组,地址相同,React并不认为state的值发生变化,所以不会触发组件的重新渲染
setNums(nums.push(6))  

✅ 正确做法:创建新数组替换旧数组

你应该创建一个新的数组,并用这个新的数组来更新 state 状态的值

// 通过创建一个新的数组,用新创建的数组来更新State的值
setNums([...nums,6])

完整代码示例

import { useState } from "react"
const App=()=>{
    const [nums,setNums]=useState([1,2,3,4,5])
    const handleClick=()=>{
        setNums([...nums,6])
    }
    return (
        <div>
            {nums.join(',')}<br/>
            <button onClick={handleClick}>更新</button>
        </div>

    )
}

export default App

# 4.1、 数组方法速查表

在 React 中,state 中的数组被视为不可变的。原生 JS 的数组方法分为两类:

  • 会修改原数组 → 避免使用
  • 返回新数组 → 推荐使用

以下是常见操作数组的方法对照表

操作类型 ❌避免使用(会修改原数组) ✅推荐使用(返回新数组)
添加元素 push 、 unshift concat 、[...arr]
删除元素 pop 、shift 、 splice filter 、slice
替换元素 splice 、arr[i]=... map
排序/反转 reverse 、 sort [...arr].reverse()[...arr].sort()

# 4.2、向数组中添加元素

向数组中添加元素,有以下几种情况

  • 在数组的前面添加元素
  • 向数组的尾部追加元素
  • 向数组的指定位置添加元素
const [nums,setNums]=useState([1,2,3,4,5])

// ✅ 在数组的前面添加元素
setNums([6,...nums]) 
// 向数组的尾部追加元素
setNums([...nums,6]) 
// 向数组的指定位置添加元素
setNums([...nums.slice(0,2),6,...nums.slice(2)]) 


// ❌ 错误方式
//    nums.splice(2,1,6)

# 4.3、删除数组中元素

向数组中删除元素,有以下几种情况

  • 删除数组的第一个元素
  • 删除数组的最后一个元素
  • 删除数组指定位置元素
const [nums,setNums]=useState([1,2,3,4,5])

// 删除数组的第一个元素
setNums(nums.slice(1))
// 删除数组的最后一个元素
setNums(nums.slice(0,nums.length-1))
// 删除数组指定位置(第3个)元素
setNums([...nums.slice(0,2),...nums.slice(3)])

先创建一个新数组,再利用 splice 方法删除数组项

// 方法二:删除数组中3和4
const newNums=[...nums];  // 复制一份新数组
newNums.splice(2,2)  // 将新数组中的3,4删除
setNums(newNums)  // 用新数组更新state的值

# 4.4、更新(替换)数组中元素

更新数组中的元素,可以使用数组的 map 方法

const [nums,setNums]=useState([1,2,3,4,5])

// 将数组中的 3 替换成A  ===> [1,2,'A',4,5]
setNums(nums.map(item=>{
    if(item === 3){
        return 'A'
    }else{
        return item
    }
}))

# 4.5、对数组排序

对数组排序,需要先创建一个新的数组,然后对新数组排序,再将新数组赋值给State

const [nums,setNums]=useState([1,2,3,4,5])
// 降序(大-->小)
setNums([...nums].sort((a,b)=>b-a))

# 4.6、更新数组中对象的值

以下是一个更新购物车中商品数量的示例

  • 点击+按钮时,购物车数量加1
  • 点击-按钮时,购物车数量减1

GIF2026-4-1022-47-25

代码示例

import { useState } from "react";
// App组件
const App = () => {
  // 状态变量和更新状态的函数
  const [fruits, setFruits] = useState([
    {
      id: 1,
      name: "苹果",
      count: 1,
    },
    {
      id: 2,
      name: "梨子",
      count: 0,
    },
    {
      id: 3,
      name: "香蕉",
      count: 2,
    },
  ]);

  // 更新数据(对应商品id的购物车数量 + 1)
  const handleAdd = (id) => {
    // 遍历数组
    setFruits(
      fruits.map((item) => {
        // 找到对应id的商品
        if (item.id === id) {
          return {
            ...item, // 复制商品其它属性
            count: item.count + 1, // 更新购物车数量
          };
        }
        return item;
      }),
    );
  };
    
    
  // 更新数据(对应商品id的购物车数量 - 1)
  const handleMinus = (id) => {
    // 遍历数组
    setFruits(
      fruits.map((item) => {
        // 找到对应id的商品
        if (item.id === id) {
          return {
            ...item, // 复制商品其它属性
            count: Math.max(0, item.count - 1), // 更新购物车数量,不能小于0
          };
        }

        return item;
      }),
    );
  };

  // 遍历fruits,生成jsx
  const result = fruits.map((item) => (
    <li key={item.id}>
      {item.name}
      <button onClick={() => handleAdd(item.id)}>+</button>
      {item.count}
      <button onClick={() => handleMinus(item.id)}>-</button>
    </li>
  ));
  return <ul>{result}</ul>;
};

export default App;

# 4.7、使用 Immer 简化数组中对象的操作

使用 immer,可以让你使用直接操作数组的方式来操作不可变数组。

import { useImmer } from "use-immer";
// 组件 
const App = () => {
  const [fruits, setFruits] = useImmer([
    {
      id: 1,
      name: "苹果",
      count: 1,
    },
    {
      id: 2,
      name: "梨子",
      count: 0,
    },
    {
      id: 3,
      name: "香蕉",
      count: 2,
    },
  ]);

  // 更新数据(对应商品 id 的购物车数量 + 1)
  const handleAdd = (id) => {
    // 找到满足条件的id,并将 count +1
    setFruits((draft) => {
      const fruit = draft.find((item) => item.id === id);
      fruit && fruit.count++;
    });
  };
// 更新数据(对应商品 id 的购物车数量 - 1)
  const handleMinus = (id) => {
    setFruits((draft) => {
      const fruit = draft.find((item) => item.id === id);
      if (fruit) {
        fruit.count = Math.max(0, fruit.count - 1);
        // fruit.count > 0 ? fruit.count-- : 0;
      }
    });
  };

  // 遍历数据,生成列表 
  const list = fruits.map((item) => (
    <li key={item.id}>
      {item.name}
      <button onClick={() => handleAdd(item.id)}>+</button>
      {item.count}
      <button onClick={() => handleMinus(item.id)}>-</button>
    </li>
  ));

  return <ul>{list}</ul>;
};

export default App;

# 五、动态操作元素 class 类名

在 React 中手动操作元素的 class 类名 ,有很多方式,以下为最常见的三种方式

  • 使用字符串模板(适用简单场景)
  • 使用数组或 Set 动态渲染(适用频繁切换)
  • 使用 classnames 库 (适合所有场景 - 推荐用法)

# 1、使用字符串模板动态渲染 class 类名

定义布尔类型的状态,通过三元运算符动态拼接类名。

适合场景:适用于简单的切换 1-2个类名

import { useState } from "react";

export default function App() {
  const [isActive, setIsActive] = useState(true);
  return (
    <>
      <button onClick={() => setIsActive(!isActive)}>显示与隐藏</button>
      <div className={`box ${isActive ? "active" : ""}`}></div>
    </>
  );
}

GIF2026-4-1010-53-44

# 2、使用数组或 Set 动态渲染 className 值

定义一个用来存放 class 类名的数组,后期通过操作数组来实现动态添加或删除类名

适用场景:适合批量管理多个类名

# 2.1、数组版本

import { useState } from "react";
const App = () => {
  /* 
    定义一个用来存放class类名的数组
    后期通过操作数组来实现动态添加或删除类名 
    */
  const [classnames, setClassNames] = useState(["box", "box1"]);

  // 移除指定 class
  const removeClassName = (classname) => {
    const filterClassName = classnames.filter((item) => item !== classname);
    setClassNames(filterClassName);
  };
  // 添加指定 class
  const addClassName = (classname) => {
    const isHas = classnames.includes(classname);
    if (!isHas) setClassNames([...classnames, classname]);
  };
  // 切换 class
  const toggleClassName = (classname) => {
    const isHas = classnames.includes(classname);
    isHas ? removeClassName(classname) : addClassName(classname);
  };

  return (
    <div className={classnames.join(" ")}>
      <button onClick={() => removeClassName("box")}>移除 box</button>
      <button onClick={() => addClassName("box2")}>添加 box2</button>
      <button onClick={() => toggleClassName("box")}>切换 box</button>
    </div>
  );
};

export default App;

GIF2026-4-921-48-27

# set 版本

import { useState } from "react";
const App = () => {
    /* 
    定义一个用来存放class类名的 Set
    后期通过操作Set 来实现动态添加或删除类名 
    */
    const [classnames, setClassNames] = useState(new Set(["box", "box1"]));

    // 移除指定 class
    const removeClassName = (classname) => {
        const newSet = new Set(classnames);
        newSet.delete(classname);
        setClassNames(newSet);
    };
    // 添加指定 class
    const addClassName = (classname) => {
        const newSet = new Set(classnames);
        newSet.add(classname);
        setClassNames(newSet);
    };
    // 切换 class
    const toggleClassName = (classname) => {
        const newSet = new Set(classnames);
        newSet.has(classname)
            ? removeClassName(classname)
        : addClassName(classname);
    };

    return (
        <div className={Array.from(classnames).join(" ")}>
            <button onClick={() => removeClassName("box")}>移除 box</button>
            <button onClick={() => addClassName("box2")}>添加 box2</button>
            <button onClick={() => toggleClassName("box")}>切换 box</button>
        </div>
    );
};

export default App;

# 2.1、封装 useClassNames hook

我们可以将操作 className 的代码封装成一个hook,这样后期可以复用

import { useState } from "react";
export default function useClassNames(initialClassNames = []) {
  /* 
    定义一个用来存放class类名的数组
    后期通过操作数组来实现动态添加或删除类名 
    */
  const [classnames, setClassNames] = useState(initialClassNames);

  // 移除指定 class
  const removeClassName = (classname) => {
    const filterClassName = classnames.filter((item) => item !== classname);
    setClassNames(filterClassName);
  };
  // 添加指定 class
  const addClassName = (classname) => {
    const isHas = classnames.includes(classname);
    if (!isHas) setClassNames([...classnames, classname]);
  };
  // 切换 class
  const toggleClassName = (classname) => {
    const isHas = classnames.includes(classname);
    isHas ? removeClassName(classname) : addClassName(classname);
  };
  return { classnames, removeClassName, addClassName, toggleClassName };
}

在组件中使用 useClassNames

import useClassNames from "./hooks/useClassNames";
const App = () => {
    const { classnames, removeClassName, addClassName, toggleClassName } =
          useClassNames(["box", "box1"]);
    return (
        <div className={classnames.join(" ")}>
            <button onClick={() => removeClassName("box")}>移除 box</button>
            <button onClick={() => addClassName("box2")}>添加 box2</button>
            <button onClick={() => toggleClassName("box")}>切换 box</button>
        </div>
    );
};

export default App;

# 3、使用 classnames 库操作 class 类名 (推荐用法)

classnames 是一个轻量级的 JavaScript 工具库,用于条件性地拼接 CSS 类名。它最主要的使用场景是在 React 项目中动态管理元素的 className 属性,但它同样适用于任何需要动态处理类名的 JavaScript 项目

以下列出了 classnames 库的具体使用

  • 安装 classnames 库
npm install classnames
  • 常见使用方法

如果想了解更多更全面用法可以查看插件说明文档- classnames (opens new window)

// 导入插件
import classNames from "classnames"

classNames('box','box1')  // "box box1"
classNames('box',{box1:true,box2:false}) // "box box1"
classNames({box1:true,box:false}) // "box1"
classNames(['a',{b:true,c:false}]) // "a b"

代码示例

import classNames from "classnames"
const App=()=>{

    return  (
        <>
        <div className={classNames('box','box1')}></div>
        <div className={classNames('box',{box1:true,box2:false})}></div>
        <div className={classNames({box1:true,box:false})}></div>
        <div className={classNames(['a',{b:true,c:false}])}></div>
        </>
    )
}

export default App

最终渲染效果如下:

<div id="root">
    <div class="box box1"></div>
    <div class="box box1"></div>
    <div class="box1"></div>
    <div class="a b"></div>
</div>

# 4、案例:控制元素显示与隐藏

利用 classnames 库与 useState 实现如下图所示效果:

GIF2026-4-922-24-48

import { useState } from "react";
import classNames from "classnames";
import "./App.css";
export default function App() {
    const [isHide, setIsHide] = useState(false);
    return (
        <div>
            <button onClick={() => setIsHide(!isHide)}> 
                显示与隐藏
            </button>
            <div className={classNames("box", { hide: isHide })}></div>
        </div>
    );
}
/* App.css 样式文件内容 */
.box {
    width: 100px;
    height: 100px;
    background-color: skyblue;
}
.hide {
    display: none;
}

# 5、实战案例:按钮提交 loading 效果

利用 classnames 库与 useState 实现如下图所示效果:

GIF2026-4-1011-37-05

图标素材:loading

import { useState } from "react";
import classNames from "classnames";
import "./App.css";
export default function App() {
    // 加载状态
    const [loading, setLoading] = useState(false);
    // 禁用状态
    // const [disabled, setDisabled] = useState(false);

    // const buttonClass = {
    //   loading,
    //   disabled,
    // };

    const handleClick = () => {
        setLoading(true);
        // setDisabled(true);
        // 模拟延迟
        setTimeout(() => {
            setLoading(false);
            //  setDisabled(false);
        }, 2000);
    };
    return (
        <>
        {/* className={classNames("btn", { loading, disabled })}*/}
<button
    className={classNames("btn", { 
        loading, 
        disabled: loading 
    })}
    disabled={loading}
    onClick={handleClick}
    >
    {loading ? "加载中..." : "提交订单"}
</button>
</>
)
}

.btn {
    width: 120px;
    height: 50px;
    border: none;
    font-size: 18px;
    border-radius: 4px;
    background-color: tomato;
    color: #fff;
    display: flex;
    justify-content: center;
    align-items: center;
}
.disabled {
    background-color: #ddd;
    color: #000;
}
.loading::before {
    content: "";
    width: 32px;
    height: 32px;
    background-image: url("/loading.png");
    animation: loading-animation linear 1s infinite;
}
/* 加载动画 */
@keyframes loading-animation {
    0% {
        transform: rotate(0deg);
    }
    100% {
        transform: rotate(360deg);
    }
}

# 六、补充知识:如何设置列表 Key 值

# 1、为什么需要 Key?

在 React 中,如果你按下面方式渲染列表(不在 li 上添加 key 属性),React 会警告

import { useState } from "react";
export default function App() {
    const [fruit, setFruit] = useState([
        { id: 1, text: "苹果" },
        { id: 2, text: "香蕉" },
        { id: 3, text: "橙子" },
    ]);
    return (
        <ul>
            {fruit.map((item) => (
                <li>{item.text}</li>
            ))}
        </ul>
    );
}

image-20260408181734224

# 2、Key 属性核心作用

Key 属性帮助 React 识别列表中的每一个元素,从而决定哪些节点可以复用、哪些需要更新或移动,避免状态错位和性能问题。

key 属性只用来标识元素的唯一性,它不会被传递到组件内部,也不会出现在最终的 HTML 结构里。

# 2.1、没有Key时

当你不提供 key 时,React 会使用元素在数组中索引作为默认 key。这会导致 React 按照 Key 值去虚拟DOM对比更新 DOM时,出现错乱情况。

示例:一个带输入框的列表

 // ❌ 列表项 li 没有 key 属性

import { useState } from "react";
export default function App() {
    const [fruit, setFruit] = useState([
        { id: 1, text: "苹果" },
        { id: 2, text: "香蕉" },
        { id: 3, text: "橙子" },
    ]);
    // 事件处理函数
    const handleClick = () => {
        setFruit([{ id: 4, text: "葡萄" }, ...fruit]);
    };
    
    return (
        <div>
            <button onClick={handleClick}>新增</button>
            <ul>
                {fruit.map((item) => (
                    <li>
                        {item.text}
                        <input type="text" />
                    </li>
                ))}
            </ul>
        </div>
    );
}

当我们在第一个输入框中输入 “苹果” 后,再点击更新按钮在列表最前面再新增一个水果“葡萄” 时,你会看到 葡萄右边的输入框中显示了苹果 。

GIF2026-4-819-21-00

布局错乱原因

React 在更新时,会拿新旧两个虚拟 DOM 做对比:把 key 相同的每一项拿出来对比,发现

  • 索引 0:旧的 li 中的是 苹果 ,新节点是 葡萄内容不同React 会更新这个 DOM 节点。但后面的 input 输入框,在 DOM 节点对比时( DOM 对比是看不到输入框输入的内容), → 内容相同 →React 会复用这个 DOM 节点
  • ....索引 1,2, 都按上面方式对比后,更新内容。
  • 索引 3,原来没有这一项 key,则直接新增 li 及内部所有内容。

image-20260408200100199

# 2.2、有 key 时

当你给每个列表项一个稳定、唯一key(比如数据自带的 id)时,React 就能准确知道每个元素的身份。当 React 按照 key 值去虚拟 DOM 对比更新内容时,能准确的判断内容是否有变动,从而准确无误的更新 DOM。

// ✅ 使用唯一 id 作为列表 li的 key属性值
import { useState } from "react";
export default function App() {
    const [fruit, setFruit] = useState([
        { id: 1, text: "苹果" },
        { id: 2, text: "香蕉" },
        { id: 3, text: "橙子" },
    ]);

    const handleClick = () => {
        setFruit([{ id: 4, text: "葡萄" }, ...fruit]);
    };
    return (
        <div>
            <button onClick={handleClick}>新增</button>
            <ul>
                {fruit.map((item) => (
                    <li key={item.id}>
                        {item.text}
                        <input type="text" />
                    </li>
                ))}
            </ul>
        </div>
    );
}

GIF2026-4-820-11-54

DOM 更新流程

React 在更新时,会拿新旧两个虚拟 DOM 做对比:把 key 相同的每一项拿出来对比,发现

  • key = 1的两个节点的内容一模一样,只是位置发生了变化 → React 移动 DOM 位置,但内容会被完整保留
  • key=2key=3 同理保留内容和状态,只移动 DOM 位置
  • key=4 是新增的 key → 新建一个li 及里面的所有内容

image-20260408203134635

# 3、Key 需要满足的条件

  • key 值在兄弟节点之间必须是唯一的。 不过不要求全局唯一,在不同的数组中可以使用相同的 key。
  • key 值不能改变,否则就失去了使用 key 的意义!所以千万不要在渲染时动态地生成 key。

# 4、Key 值的选择

不同来源的数据往往对应不同的 key 值获取方式 (opens new window)

  • 来自数据库的数据: 如果你的数据是从数据库中获取的,那你可以直接使用数据表中的主键,因为它们天然具有唯一性。
// ✅  推荐用法
function App() {
    // 假设这个数据是后台从数据库中查询出来的用户列表信息,则 id字段为数据库中表的主键,是唯一的
    const userList = [
        {
            id: 1,
            username: "清心",
        },
        {
            id: 2,
            username: "arry",
        },
        {
            id: 3,
            username: "晓冉",
        },
    ];

    return (
        <ul>
            {userList.map((user) => (
                <li key={user.id}>{user.username}</li>
            ))}
        </ul>
    );
}

export default App;
// ✅  推荐用法
import { v4 as uuidv4 } from "uuid";
function App() {
    const userList = [
        {
            id: uuidv4(),
            username: "清心",
        },
        {
            id: uuidv4(),
            username: "arry",
        },
        {
            id: uuidv4(),
            username: "晓冉",
        },
    ];

    console.log(userList);
    /*
  [
    {
        "id": "b44a2cfe-d84c-492a-ae07-29e889fe56fc",
        "username": "清心"
    },
    {
        "id": "7062e0f9-8a69-428b-b331-ee56581f7b74",
        "username": "arry"
    },
    {
        "id": "26210a78-1bd0-41a4-878a-912ce75f99a1",
        "username": "晓冉"
    }
]
     */

    return (
        <ul>
            {userList.map((user) => (
                <li key={user.id}>{user.username}</li>
            ))}
        </ul>
    );
}

export default App;
  • 数组索引 不能作为 key 属性值

如果渲染的列表,后期并不会对列表做增、删除操作,则key的值可以是每一项的索引。

如果需要做增删操作,则唯一值不能是index索引,因为操作数组时,会更改每一项的索引。

//  ❌:不推荐
<ul>
    {userList.map((user,index) => (
        <li key={index}>{user.username}</li>
    ))}
</ul>

大厂最新技术学习分享群

大厂最新技术学习分享群

微信扫一扫进群,获取资料

X