React

State Hooks - useState

useState允许你向组件添加一个状态变量
This entry is part 1 of the seriesenjoy-react-hooks

语法

让我们从一个最简单的计数器案例来介绍useState的基本使用

点击按钮,并观察计数器的变动👇
import React from 'react';

export default function Counter() {
  const [count, setCount] = React.useState(0)

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  )
}

useState 是一个 React Hook,它允许你向组件添加一个状态变量

其语法如下:

const [state, setState] = useState(initialState)

参数

  • initialState:state 初始化的值。它可以是任何类型的值(包括函数)
    • 如果initialState是一个函数,则它将被视为 初始化函数。当初始化组件时,React 将调用你的初始化函数,并将其返回值存储为初始状态
      const [count, setCount] = useState(() => 0)

返回值

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

  • State 变量 用于保存渲染间的数据。
  • State setter 函数 更新变量并触发 React 再次渲染组件。

渲染流程

通过上述案例,我们可以深入了解到:每当用户点击按钮时,会触发 setCount 函数的调用,这进而引发了 UI 的更新。然而,这一过程背后的关键逻辑是什么呢?

这个问题实际上揭示了 React 的核心概念之一 据驱动视图的原则,即 v = f(s)

  • v: 视图(View),即用户界面(UI)
  • f: 渲染函数(Render Function),即你的组件
  • s: 状态变量(State Variables)

让我们回到计数器案例中,将 Counter 组件中返回的 JSX 替换为 React.createElement,这样做的目的是为了更直观地看到每次点击按钮时,React 如何根据状态变化生成新的 React Element 并更新 UI。

请注意,每次点击按钮时,控制台中的打印信息将展示出 React 如何根据状态的变化(通过 setCount 函数)来生成新的 React Element,从而实现 UI 的更新

点击按钮,并观察Console中children属性的变动👇
import React from 'react';

export default function Counter() {
  const [count, setCount] = React.useState(0)

  const reactElement = React.createElement(
    'button',
    {onClick: () => setCount(count + 1)},
    count
  )

  // 👉 为了让打印信息更直观,这里对reactElement对象进行了处理
  console.log({...reactElement, props: {
    onClick: reactElement.props.onClick.toString(),
    children: reactElement.props.children
  }})


  return reactElement
}
Preview
Console

不难发现,React Element 就是一个普通的 JavaScript 对象,它被 React 用来描述一部分用户界面(UI)。

Mount

当应用启动时,会触发初次渲染,React会从根组件开始遍历执行你的函数组件, 当执行到 Counter时生成一个 React Element,该元素代表了Counter的初始状态

js
{
type: 'button',
key: null,
ref: null,
props: {
children: '0',
},
}
jsx
<button>
0
</button>

Trigger

当用户再次点击按钮时,setCount() 函数被触发,导致 count 从 0 更新为 1。这个状态的变化会促使 React 再次进行渲染(re-render)。

Render

这个阶段 React 会重新调用你的函数组件(re-render),以便生成一个新的 React Element,这个新元素反映了组件状态的最新变化

js
{
type: 'button',
key: null,
ref: null,
props: {
children: '1',
},
}
jsx
<button>
1
</button>

每次渲染都像是拍摄一张快照,每个组件根据其当前的 props 和 state 生成一个 React Element,这个元素描绘了该组件在当下的外观和结构,就像是一份静态的记录,捕获了组件在那个瞬间的状态。这种方式确保了组件的表现形式始终与其内部状态保持同步,为用户界面的动态更新提供了坚实的基础

Commit

现在,我们有了两张快照

<button>
0
</button>
<button>
1
</button>

React现在必须要弄清楚如何更新DOM, 使UI与第二份快照保持一致。

不知道你有没有玩过在两张图片中寻找差异的游戏?

React本质上就是在玩这种游戏,在两份快照之间寻找差异,然后提交更新到DOM。

Counter这个案例中,更新DOM的操作应类似于

button.innerText = '1'

我们可以用下面这个可视化组件来描述React的渲染流程

Mount

在首次渲染组件时,由于没有先前的快照可供对比,React会从零开始构建所有必需的DOM节点,并将这些节点直接插入到网页中。

Note:你可以 click the boxes 来了解该阶段的详细介绍

总结

  1. React渲染流程份为三个阶段 Trigger、Render、Commit
  2. 所谓Render,其实就是React在调用你的组件函数
  3. State可以理解为具体某次Render的UI快照

异步更新

阅读下面代码,点击按钮后比对控制台输出和页面

点击按钮后比对控制台输出和页面
import React from 'react';

export default function Counter() {
  const [count, setCount] = React.useState(0)

  const handleClick = () => {
    setCount(count + 1)
    console.log('count:', count)
  }
  return (
    <button onClick={handleClick}>
      {count}
    </button>
  )
}
Preview
Console

你会发现, 首次渲染时,我们将count初始化为0。然后,当我们点击按钮时,调用setCount(count + 1)将其增加1,即1。那么它不应该打印1吗,为什么是0呢?而且后续每次点击后,Console中打印的 count值,都比页面上的值小1。

const [count, setCount] = React.useState(0) // 初始为0
const handleClick = () => {
setCount(count + 1) // 增加1
console.log('count:', count) // ❓此时打印为什么是0 而不是1呢
}

其实,当我们调用 setCount(count + 1) 时,我们实际上是在向 React 发出一个信号,表明我们希望修改某个状态变量的值(请求使用+1后的count值重新渲染)。然而,React 并不会立即执行这个修改操作;相反,它会等待当前正在进行的操作(例如处理用户点击事件)完成后,再进行状态的更新和组件的重新渲染。

到目前为止,我们需要知道的是:

React状态更新是异步的, 在具体的某一次Render过程中state是不会发生变化的,set函数只会影响下一次useState返回的结果

下面我们再用一个案例来证明上面的结论。

点击按钮,并观察计数器的变动👇
import React from 'react';

export default function Counter() {
  const [count, setCount] = React.useState(0)

  const inscrementOne = () => {
    setCount(count + 1)
  }
  const inscrementTwo = () => {
    setCount(count + 1)
    setCount(count + 1)
  }
  return (
    <main>
      <span>{count}</span>
      <div>
        <button onClick={inscrementOne}>+1</button>
        <button onClick={inscrementTwo}>+2</button>
      </div>
    </main>
  )
}

在这个案例中,我们通过两个按钮+1+2来增加count的值,其中+2点击时会调用两次setCount(count + 1)。但当我们点击+2时发现,其效果和+1一样,每次点击只会让count的值增加1。

按照之前的结论,我们可以这样理解

const [count, setCount] = React.useState(0) // 初始为0
const inscrementTwo = () => {
setCount(count + 1) // 此时count为0, 我们请求React使用1作为新值重新渲染
setCount(count + 1) // 此时count❗仍为0, 我们再请求React使用1作为新值重新渲染
}

我们发送了两次完全相同的请求,那结果下次渲染时Count值是1而不是2。

那有什么办法能实现+2的效果吗? 自然是有的。

基于前值更新状态

为了解决上面的问题,你可以向 setCount 传递一个 更新函数,而不是下一个状态:

点击按钮,并观察计数器的变动👇
import React from 'react';

export default function Counter() {
  const [count, setCount] = React.useState(0)

  const inscrementOne = () => {
    setCount(count + 1)
  }
  const inscrementTwo = () => {
    setCount(prevCount => prevCount  + 1)
    setCount(prevCount => prevCount  + 1)
  }
  return (
    <main>
      <span>{count}</span>
      <div>
        <button onClick={inscrementOne}>+1</button>
        <button onClick={inscrementTwo}>+2</button>
      </div>
    </main>
  )
}
const inscrementTwo = () => {
setCount(count + 1)
setCount(count + 1)
}
const inscrementTwo = () => {
setCount(prevCount => prevCount + 1)
setCount(prevCount => prevCount + 1)
}

通过setState(prevState => nextState)这种方式,每次调用set函数时都能拿到上一次的state。

更新复杂对象

你可以将对象和数组放入状态中。在 React 中,状态被认为是只读的,因此 你应该替换它而不是改变现有对象。

Code Playground
import React from 'react';

export default function Counter() {
  const [state, setState] = React.useState({
    count: 0,
    color: ''
  })

  const inscrement= () => {
    setState({
      ...state,
      count: state.count + 1,
      color: 'green'
    })
  }
  const decrement = () => {
    setState({
      ...state,
      count: state.count - 1,
      color: 'red'
    })
  }
  return (
    <main>
      <span style={{color: state.color}}>{state.count}</span>
      <div>
        <button onClick={decrement}>-</button>
        <button onClick={inscrement}>+</button>
      </div>
    </main>
  )
}

Questions? Let's chat

discord logoOPEN DISCORD
6423
members online
next article
Effect Hooks - useEffect

A TypeScript Full Stack Blog

Share articles about Typescript, React, Next.js, Node.js and Css from time to time.
No spam, unsubscribe at any time.