React useState Hook

摘要: 本教程将介绍 React 的 useState() hook 以及如何有效地使用它来使您的组件更加健壮。

React useState() hook 简介

在 React 中,状态是指随着时间推移而发生变化的数据。要向组件添加状态变量,请使用 useState() hook。

首先,导入 useState,它是 react 库中的一个函数

import { useState } from 'react';Code language: JavaScript (javascript)

其次,在您的组件中使用 useState() 函数

const [state, setState] = useState(initialValue);Code language: JavaScript (javascript)

useState() 函数返回一个包含两个元素的数组

  • 一个变量 (state),它保存当前状态值。在第一次渲染时,state 变量的值为 initialState
  • 一个函数 (setState),它允许您将 state 变量的当前值更改为新的值并触发重新渲染。

按照惯例,更新状态变量的函数名称以动词 set 开头,后跟变量名称。

例如,如果状态变量是 count,则更新 count 变量的函数是 setCount

const [count, setCount]  = useState(0);Code language: JavaScript (javascript)

useState() 函数接受任何有效数据类型(字符串、数字、数组、对象等)的初始值。

更新状态

有两种方法可以调用 setState() 函数来更新状态变量

  • 直接更新。
  • 函数更新。

在直接更新中,您不需要了解先前的状态,并将新值直接传递给 setState() 函数。例如

setCount(1);Code language: JavaScript (javascript)

在此示例中,setCount() 函数将新计数值设置为 1,无论当前状态值如何。

在函数更新中,您传递一个函数,该函数接受先前的状态作为输入参数并返回新状态。例如

setCount(prevCount => prevCount + 1);Code language: JavaScript (javascript)

在此示例中,setCount() 函数根据先前的 count 值将 count 变量增加 1。

useState hook 示例

让我们以一个简单的例子来演示如何在 Counter 组件中使用 useState hook

import React, { useState, useEffect } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <h1>Current count: {count}</h1>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default Counter;Code language: JavaScript (javascript)

在此示例中,以下代码使用 useState() hook 声明了 count 变量,其初始值为零

const [count, setCount] = useState(0);Code language: JavaScript (javascript)

当单击 Increment 按钮时,会调用 increment() 函数,该函数使用 setCount() 函数来更新 count 变量。

由于 count 变量发生了变化,React 会触发组件重新渲染并显示新的计数值。

状态和组件重新渲染

在 React 中,只要通过调用 setState() 函数更改了组件的状态,就会重新渲染组件。要监控组件重新渲染,可以使用 useEffect() hook,如下所示

import React, { useState, useEffect } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('Component rendered or rerendered');
  });

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <h1>Current count: {count}</h1>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default Counter;Code language: JavaScript (javascript)

每当单击 Increment 按钮时,count 变量都会增加 1,这会触发组件重新渲染。您将在第一次渲染时以及每次在控制台窗口中重新渲染时看到消息 Component rendered or rerendered

如果新状态值与先前状态值相同,React 不会重新渲染组件。

例如,如果您调用 setCount() 函数但没有更改 count 变量,React 不会触发重新渲染

const increment = () => {
   setCount(count);
};Code language: JavaScript (javascript)

在控制台窗口中,您将看到第一次渲染组件时只有一条消息 component rendered or rendered,并且在单击 Increment 按钮时不会看到相同的消息。

您可以立即在更新后记录 count 值。但日志将显示更新之前的计数值

const increment = () => {
    // update the count
    setCount(count + 1);

    // show the count before the update
    console.log(count);
};Code language: JavaScript (javascript)

increment() 函数中,我们将 count 值增加 1

setCount(count + 1);Code language: JavaScript (javascript)

React 不会立即重新渲染组件。而是会安排更新。因此,count 变量的值是在更新生效之前的当前计数值。

重要的是要注意,出于性能原因,React 会批量处理多个状态更新。这就是为什么您不会立即在控制台中看到新状态的原因。

例如,您可以多次调用 setCount() 函数,React 会将这些更新批量处理,从而导致一次重新渲染

const increment = () => {
    // update the count multiple times
    setCount(count + 1);
    setCount(count + 2);
    setCount(count + 3);

    // show the count before the update
    console.log(count);
};Code language: JavaScript (javascript)

在此示例中,React 将安排三个状态更新,这将导致一次重新渲染。您将在控制台窗口中看到 0、3、6 等计数值。

原因是 setCount() 将根据当前 count 值被调用三次

  • 第一次更新: count + 1 – 将计数增加 1。
  • 第二次更新: count + 1 – 将当前计数(不是更新后的计数)增加 1。
  • 第三次更新: count + 3 – 将当前计数(不是最新计数)增加 1。

如果当前计数为 0,则三次调用将计数设置为 3。如果当前计数为 3,则三次调用将计数设置为 6,依此类推。

要根据先前状态更新状态,应使用函数更新,该函数更新将先前状态作为参数。例如

const increment = () => {
  // update the count multiple times
  setCount((prevCount) => prevCount + 1);
  setCount((prevCount) => prevCount + 2);
  setCount((prevCount) => prevCount + 3);

  // show the count before the update
  console.log(count);
};Code language: JavaScript (javascript)

在此示例中

  • 第一次更新: prevCount + 1 – 将计数增加 1。
  • 第二次更新: prevCount + 2 – 将更新后的计数增加 2。
  • 第三次更新: prevCount + 3 – 将最新计数增加 3。

React 将批量处理这些更新,最终状态为 prevCount + 6。因此,您将在控制台窗口中看到 0、6、12、18 等。

处理复杂状态

当组件具有数组或对象状态时,应仔细处理它以确保不变性。

更新数组状态

在处理数组状态时,应在更新数组时创建原始数组的副本,其中包含修改后的项目,以避免直接更改数组。

让我们看一下以下 FruitList 组件

import React, { useState } from 'react';

const commonFruits = [
  '🍎 apple',
  '🍌 banana',
  '🍒 cherry',
  '🍈 date',
  '🥭 mango',
  '🍇 grape',
  '🥝 honeydew',
  '🥥 coconut',
  '🍋 kiwi',
  '🍊 lemon',
  '🍉 warter melon',
  '🍊 orange',
  '🍑 peach',
];

const FruitList = () => {
  const [fruits, setFruits] = useState([]);
  const addFruit = () => {
    const newFruit =
      commonFruits[Math.floor(Math.random() * commonFruits.length)];
    setFruits([...fruits, newFruit]);
  };
  return (
    <div>
      <h1>Fruit List</h1>
      <ul>
        {fruits.map((fruit, index) => (
          <li key={index}>{fruit}</li>
        ))}
      </ul>
      <button onClick={addFruit}>Add Fruit</button>
    </div>
  );
};

export default FruitList;
Code language: JavaScript (javascript)

FruitList 组件具有水果状态,它是一个数组。useState hook 将其初始化为空数组

const [fruits, setFruits] = useState([]);Code language: JavaScript (javascript)

要向水果数组添加新元素,我们通过复制现有水果并使用以下语法添加新水果来创建一个新数组

setFruits([...fruits, newFruit]);Code language: JavaScript (javascript)

请注意,如果您使用 push() 方法向数组添加项目,即使数组的项目数发生了变化,您仍然引用相同的数组引用。在这种情况下,React 不会触发组件重新渲染

// DON'T DO THIS
fruits.push(newFruit);
setFruits(fruits);Code language: JavaScript (javascript)

更新对象状态

与数组状态一样,在更新对象状态时,应确保创建一个新对象,而不是修改现有对象。例如

import React, { useState } from 'react';

const Person = () => {
  const [person, setPerson] = useState({
    name: 'John',
    age: 20,
  });

  const handleIncrement = () => {
    setPerson({ ...person, age: person.age + 1 });
  };

  return (
    <div>
      <p>Name: {person.name}</p>
      <p>Age: {person.age}</p>
      <button onClick={handleIncrement}>Increment Age</button>
    </div>
  );
};

export default Person;Code language: JavaScript (javascript)

在此示例中,我们通过复制原始对象的现有属性并更新 age 属性来创建一个新的 Person 对象

setPerson({ ...person, age: person.age + 1 });

状态的延迟初始化

有时,初始化状态可能涉及昂贵的计算。为了提高性能,React 允许您通过向 useState() 传递一个函数来延迟初始化状态。useState() 将在第一次渲染期间仅调用一次此函数。

在以下 Theme 组件中,useState() hook 在每次组件重新渲染时都会调用 getTheme() 函数,这没有必要

import React, { useState } from 'react';

function getTheme() {
  console.log('Getting the theme from the local storage');
  return localStorage.getItem('theme') || 'light';
}

const Theme = () => {
  const [theme, setTheme] = useState(getTheme());
  const handleClick = () => {
    console.log('Changing the theme');
    const newTheme = theme === 'light' ? 'dark' : 'light';
    setTheme(newTheme);
    localStorage.setItem('theme', newTheme);
  };
  return (
    <div>
      <p>Current Theme: {theme}</p>
      <button onClick={handleClick}>Swich Theme</button>
    </div>
  );
};

export default Theme;Code language: JavaScript (javascript)

要指示 useState hook 仅在第一次重新渲染期间调用 getTheme() 函数,您可以像这样向其传递一个函数

const [theme, setTheme] = useState(() => getTheme());Code language: JavaScript (javascript)

当您必须初始化一个需要您不想在每次渲染时都重复执行的计算的状态,或者当初始状态涉及从外部来源(如 localStorage)检索数据时,可以使用此延迟状态初始化技术。

总结

  • 调用 useState() hook 以向组件添加状态变量。
  • useState() 函数返回一个包含两个项目的数组:状态变量 (state) 和一个用于更新状态的函数 (setState)。
  • 始终使用 setState 函数更新状态变量。如果 state 发生了更改,setState 函数将触发组件重新渲染。
  • 在更新复杂状态时,返回数组或对象的副本,而不是直接修改它。
  • 如果初始状态的计算代价很高,请使用延迟初始化。
本教程对您有帮助吗?