主要做状态管理

useState

两个功能:

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

语法

const [thing, setThing] = useState(0);
  • useState 参数:初始值
  • useState 返回的结果:
    1. 第一个是保存的上次渲染的值
    2. 第二个是更新 state 变量并触发 React 重新渲染组件的函数

state 是不可变的(只读的)

当修改一个对象 state 时,需要创建一个新对象并把它传递给 state 的设置函数

对象

如果只修改对象中的部分值,使用展开语法:

setPerson({
  ...person, // 复制上一个 person 中的所有字段
  firstName: e.target.value // 但是覆盖 firstName 字段 
});

数组

避免使用 (会改变原始数组)推荐使用 (会返回一个新数组)
添加元素pushunshiftconcat[...arr] 展开语法(例子
删除元素popshiftsplicefilterslice例子
替换元素splicearr[i] = ... 赋值map例子
排序reversesort先将数组复制一份(例子

为什么在 React 中不推荐直接修改 state?

有以下几个原因:

  • 优化:React 常见的 优化策略 依赖于如果之前的 props 或者 state 的值和下一次相同就跳过渲染。通过对象地址是否改变可以快速判断是否需要优化
  • 快照:每次渲染类似于快照,记录当时的 state
  • 简单:React 并不需要像很多“响应式”的解决方案一样去劫持对象的属性、总是用代理把对象包裹起来,或者在初始化时做其他工作。React 中直接修改对象属性值并不会触发重渲染

setState 执行机制

  • 在组件生命周期或 React 合成事件中,setState 是异步
  • 在 setTimeout 或者原生 dom 事件中,setState 是同步
class Example extends React.Component {
  constructor() {
    super()
    this.state = {
      val: 0
    }
  }
  
  componentDidMount() {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)
    // 第 1 次 log
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)
    // 第 2 次 log
    setTimeout(() => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)
      // 第 3 次 log
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)
      // 第 4 次 log
    }, 0)
  }
  
  render() {
    return null
  }
}
 
// 0, 0, 2, 3
// componentDidMount中是异步的,状态不会立即更新,当前this.state.val仍然是0
// setTimeout中的代码是同步的,而且此时React已经处理了前两个setState的批处理更新,将val更新为1

setState 多次执行

由于 React 会对 state 更新进行批处理,导致多次 setState 修改只会有最后一次有效

import { useState } from 'react';
 
export default function Counter() {
  const [number, setNumber] = useState(0);
 
  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
      }}>+3</button>
    </>
  )
}
 
// 相当于:
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);

解决方法:给 setState 传入一个 根据队列中的前一个 state 计算下一个 state 的 函数

import { useState } from 'react';
 
export default function Counter() {
  const [number, setNumber] = useState(0);
 
  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(n => n + 1);
        setNumber(n => n + 1);
        setNumber(n => n + 1);
      }}>+3</button>
    </>
  )
}

Info

  • 设置 state 不会更改现有渲染中的变量,但会请求一次新的渲染。
  • React 会在事件处理函数执行完成之后处理 state 更新。这被称为批处理。
  • 要在一个事件中多次更新某些 state,可以使用 setNumber(n n + 1) 更新函数。

不用 state 的情况

如果一个值可以基于现有的 props 或 state 计算得出,不要把它作为一个 state,而是在渲染期间直接计算这个值。

useReducer

对于拥有许多状态更新逻辑的组件来说,可以使用 reducer 整合状态逻辑

Reducer 是处理状态的另一种方式。可以通过三个步骤将 useState 迁移到 useReducer:

  1. 编写 一个 reducer 函数替换掉原来更新 state 的逻辑
  2. 在组件中 使用 useReducer
  3. 将设置状态的逻辑 修改 成 dispatch 一个 action

1. 写 reducer:

export default function myReducer(states, action) {
  switch (action.type) {
    // // 每个 case 块包装到 { 和 } 花括号中,保证独立的作用域
    case 1: {
      return ... // 给 React 返回更新后的状态
    },
    case 2: {
      return ... // 给 React 返回更新后的状态
    },
    // ...
  }
}
  • 声明当前状态(states)作为第一个参数
  • 声明 action 对象作为第二个参数
  • 从 reducer 返回 下一个 状态(React 会将旧的状态设置为这个最新的状态)

2. 使用 useReducer:

import { useReducer } from 'react';
import myReducer from './myReducer.js';
 
const initialData = ...
 
export default function App() {
  const [states, dispatch] = useReducer(myReducer, initialData)
  
  // ...
}

useReducer 钩子接受 2 个参数:

  • 一个 reducer 函数
  • 一个初始的 state

它返回如下内容:

  • 一个有状态的值
  • 一个 dispatch 函数(用来 “派发” 用户操作给 reducer)

3. 使用 dispatch 触发更新:

export default function App() {
  const [states, dispatch] = useReducer(myReducer, initialData)
  
  function handleState(val) {
    // dispatch 传入的对象就叫 action
    dispatch({
      // 针对特定的组件
      type: 'what_happened',
      // 其它字段放这里
    });
  }
  
  // ...
}