在我们创建组件时,我们不仅可以通过 props 接收外部传入的数据,还可以通过 useState
来定义和管理组件内部的状态。
10import { useState } from 'react'2030export default function Counter() {40// 定义一个状态 count,初始值为 050const [count, setCount] = useState(0)60return (70<div className='flex justify-between items-end'>80<p>Count: <span className='text-2xl font-bold font-din'>{count}</span></p>90<button className='button' onClick={() => setCount(count + 1)}>10Increment11</button>12</div>13)14}
当我们期望页面上的 UI 元素能够在用户操作或者数据变化时发生改变,我们就可以使用 useState
来定义一个状态
useState(initialState)
我们通常在函数组件中的开头几行代码中,使用 useState
来定义一个状态。
1import { useState } from 'react';23function MyComponent() {4const [age, setAge] = useState(28);5const [name, setName] = useState('Taylor');6const [todos, setTodos] = useState(() => createTodos());7// ...
useState
接收一个参数,这个参数是状态的初始值。初始值有两种方式,一种是直接传入一个值,另一种是传入一个函数。该函数会在组件初始化时执行,最终的初始值是该函数的返回值。
这里需要特别注意的是:在后续的更新中,组件函数会再次执行,但是 useState 的初始值赋值行为只会发生一次。 useState 初始值传入函数时,该函数也仅会执行一次,后续更新过程不再执行。
useState
返回一个数组,该数组包含两个元素:
我们通常使用数组解构来获取这两个值:
1const [count, setCount] = useState(0);2// ^ ^3// 状态值 更新函数
约定命名:状态更新函数通常以 set
开头,后面跟上状态变量的名称。
重点: 当我们调用 setXxx
方法去更新状态时,React 会自动重新执行组件函数,并使用最新的状态值来渲染 UI。
我们可以有以下两种方式来更新状态:
直接赋值更新
最简单的状态更新方式是直接传入新值:
1const [count, setCount] = useState(0);23// 点击按钮时更新状态4const handleClick = () => {5setCount(count + 1); // 直接传入新值6};
函数式更新
当新状态值需要基于前一个状态计算时,推荐使用函数式更新:
1const [count, setCount] = useState(0);23// 推荐:函数式更新4const handleClick = () => {5setCount(prevCount => prevCount + 1);6};
函数式更新的优势:
useEffect
等 Hook 时更加安全注意,无论采用哪种方式进行更新,如果新值和旧值相同,都不会重新渲染组件。只有当新值和旧值不同时,才会重新渲染组件。
在最新的 React 版本中,状态更新是异步的,这意味着调用 setState
后,状态不会立即更新。React 会在合适的时机批量处理状态更新,以提高性能。
1const [count, setCount] = useState(0);23const handleClick = () => {4setCount(count + 1);5console.log(count); // 仍然是旧值!6};
如果需要在状态更新后执行某些操作,可以使用 useEffect
Hook:
1const [count, setCount] = useState(0);23useEffect(() => {4console.log('count 更新了:', count);5}, [count]);
在 React 之前的版本中,由于底层实现的问题,合成事件和生命周期钩子中,状态更新是异步的,但是在原生时间和 setTimeout 等异步钩子中,由于脱离了 React 的控制,状态更新是同步的。
其他注意事项:
1、useState
是一个 Hook,因此你只能在组件的顶层或你自己的 Hook 中调用它。你不能在循环或条件语句中调用它。如果需要,请提取一个新组件并将状态移入其中。
2、在严格模式下,React 会调用你的初始化函数两次,以帮助你发现意外的不纯代码。这仅限于开发环境,不会影响生产环境。如果你的初始化函数是纯函数(理应如此),这应该不会影响其行为。其中一次调用的结果将被忽略。
当我们多次调用 set
方法去更新状态时,React 会自动将这些更新合并成一个批量更新,以提高性能。如下例子所示,三次调用 setCount
方法,最终只会触发一次组件的重新渲染。
1const [count, setCount] = useState(0);2setCount(count + 1);3setCount(count + 1);4setCount(count + 1);
这也是为什么我们说,状态更新是异步的核心原因
ok,了解了这些基础知识之后,接下来,我们要用大量的示例来演示如何使用 useState
。
具体的使用比较简单,我们直接通过代码来学习如何使用 useState
。主要需要从代码里关注的细节是
10import { useState } from 'react'20import { Plus, Minus, RotateCcw } from 'lucide-react'30import CounterButton from './CounterButton'40import StepControl from './StepControl'5060export default function EnhancedCounter() {70const [count, setCount] = useState(0)80const [step, setStep] = useState(1)9010const increment = () => {11setCount(prevCount => prevCount + step)12}1314const decrement = () => {15setCount(prevCount => prevCount - step)16}1718const reset = () => {19setCount(0)20setStep(1)21}2223return (24<div className="flex flex-col gap-4">25<div className="text-center">26<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-2">27增强版计数器28</h3>29<div className="text-4xl font-bold font-din text-blue-600 dark:text-blue-400 mb-4">30{count}31</div>32</div>3334<div className="flex gap-2 justify-center">35<StepControl step={step} onStepChange={setStep} />36<CounterButton37onClick={decrement}38icon={<Minus size={16} />}39variant="decrease"40>减少</CounterButton>4142<CounterButton43onClick={increment}44icon={<Plus size={16} />}45variant="increase"46>增加</CounterButton>4748<CounterButton49onClick={reset}50icon={<RotateCcw size={14} />}51variant="reset"52>重置</CounterButton>53</div>5455<div className="text-xs text-gray-500 dark:text-gray-400 text-center">56点击按钮来增加或减少计数,每次变化 {step}57</div>58</div>59)60}
通常情况下,我们会把变化的 UI 抽象成状态,然后通过改变状态的方式来改变 UI,因此,我们会分成几个步骤来思考
useState
是 React 开发的核心 API,学会了该 API 的使用,基本上也就意味着学会了 React,因此,在后续的章节中,我们还需要多通过几个案例来加深对该 API 的理解。