03|在 React 中正确使用 useState 的姿势

3,379 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

⚠️注意:因为我们不涉及 Class Component,所有内容的都会以 Function Component形式进行

上一节中,我们学习了如何创建一个 React 项目,并了解到了 JSX 语法的本质就是 JavaScript。希望你有完成我留下来的小作业~

本节,我们来学习在 React 中如何使用 useState 来支持页面动态渲染,以及使用需要注意的一些点,怎样避免这些坑。

image.png

在 React 中,如果你想要使用一个支持可操作的变量,像下面👇这样写是不行的

function App() {

  let num = 0 // 声明一个变量 num

  const add = () => { // add 函数用于每次点击 btn 时将 num + 1
    num++
    console.log(num)
  }

  return (
    <div style={{ margin: 100 }}>
      <div style={{ marginBottom: 16 }}>{num}</div>
      <button onClick={add}>加1</button>
    </div>
  )
}

我们在浏览器中打开

May-24-2022 17-25-21.gif

明明num被改变了,可为什么渲染的还是 0 呢? 这还是因为,我们写的代码就是普通的 JavaScript 代码,上面的这段代码其实就等价于我们在原生JavaScript中这样写

let num = 0
const el = document.getElmentById('root')
el.innerHTML = num

document.getElementById('btn').onclick = () => {
  num++
  console.log(num)
}

试问,我们没有在每次num++以后将这个值更新到elinnerHTML中,页面怎么会展示呢?

一、使用 useState

我们使用着React,如果我们还需要每次num++以后来手动更新,那岂不是又回到了刀耕火种的时代了,所以React 官方的 API useState就出来了

image.png

useState是React上的一个方法,它接收一个默认值,并返回一个数组,数组包含两项,第一项是我们的数据,第二项是用来修改这个数据的函数。

function App() {

  const [num, setNum] = React.useState(0) // 声明一个变量 num

  const add = () => { // add 函数用于每次点击 btn 时将 num + 1
    setNum(num + 1)
    console.log(num)
  }

  return (
    <div>
      <div>{num}</div>
      <button onClick={add}>加1</button>
    </div>
  )
}

我们在浏览器中看一下效果

May-24-2022 18-08-53.gif

芜湖~,页面如我们预期的更新成功了🥳 🥳 🥳

二、多次使用同一个 setState

我们有时可能会在一次事件中多次调用同一个 setState

function App() {

  const [num, setNum] = React.useState(0) // 声明一个变量 num

  const add = () => {
    setNum(num + 1)
    console.log(num)
    setNum(num + 2)
    console.log(num)
  }

  return (
    <div>
      <div>{num}</div>
      <button onClick={add}>加1</button>
    </div>
  )
}

也许我们想要的效果是先将num + 1,接着再将num + 2这样的效果,但是很抱歉的告诉你,这是不行的,生效的只会是最后一个**setState**,页面上只会渲染每次加2的结果,所以这里需要注意。 那可能有的大聪明会说,那我业务当中就是会有这样的需求啊,就是可能会经过多次设置呢。

image.png

好吧,的确是有这样的需要的,React 中提供了另外一种写法,很简单👇

const add = () => {
  setNum(n => n + 1)
  // 一些操作
  setNum(n => n + 2)
}

这种写法是传入的就不是直接的值,而是一个function,它接受前面的state,我们可以根据这个stat 来做一些处理以后,再返回一个state。 所以我们这里调用完以后,页面上就会成功显示加3后的结果了。(但是一定要记住我说的:生效的只会是最后一个**setState**,如果你在最后仍写的是setNum(x),最后都会以x的值作为渲染)

三、使用 useState 存储对象类型数据

使用useState存储对象类型的数据应该是很多刚开始使用 React 遇到的坑点了,比如我~

image.png

我们有一个对象,存放的是小明的信息,希望在每次点击按钮时,小明都能长大一岁(小明:wdnmd😡 😡 😡


function App() {

  const [user, setUser] = React.useState({
    name: '小明',
    age: 18
  })

  // 每次点击都将年龄 + 1
  const plus = () => {
    user.age += 1
    setUser(user)
    console.log('点击打印', user) // 每次点击打印
  }
  
  console.log('render打印', user) // 每次 render 都会打印

  return (
    <div style={{ margin: 100 }}>
      <div style={{ marginBottom: 16 }}>姓名:{user.name}</div>
      <div style={{ marginBottom: 16 }}>年龄:{user.age}</div>
      <button onClick={plus}>长大一岁</button>
    </div>
  )
}

export default App;

我们在浏览器中跑一下看下效果是否如我们所愿 May-25-2022 18-37-37.gif

我们发现,每次点击触发了,并且也更新了 age,可是为什么没有触发 render 让页面更新呢?我们到源码中看一下怎么回事

const objectIs = typeof Object.is === 'function' ? Object.is : is;

function dispatchSetState(fiber, queue, action) {
  // 省略......
  if (objectIs(eagerState, currentState)) {
    // Fast path. We can bail out without scheduling React to re-render.
    // It's still possible that we'll need to rebase this update later,
    // if the component re-renders for a different reason and by that
    // time the reducer has changed.
    return;
  }
  // 省略......
}

其中eagerState就是我们想要更新渲染的值,currentState就是当前页面上渲染的值 ,而objectIs就是判断这两个值是否相等,如果相等,则直接退出更新。

而我们这里两个值都是同一个引用,自然也就无法触发React的后续操作了,所以每次点击时数据都更新了,但是页面就是没有反应。

同时,这意味着基本类型如果更新前后的值都一致的话objectIs也会判定为true,导致页面不会更新(重新 render )。 知道了是什么原因,那么我们就知道怎么解决了,不就是新给一个对象嘛,我改就完了,于是一顿操作如下👇


// 省略......

// 每次点击都将年龄 + 1
const plus = () => {
  setUser({ age: user.age + 1 })
  console.log('点击打印', user) // 每次点击打印
}

console.log('render打印', user) // 每次 render 都会打印

// 省略......

如果你是ts用户,并且开了代码检查,那肯定会发现报错了,提示你setUser中缺少参数name字段,如果你不是,页面会成功展示,并在你点击按钮时,会发现页面上的年龄虽然更新了,但是姓名不见了~

image.png

如果你是之前写 Class 方式的小伙伴,你肯定会觉得很疑惑。这是因为在 hook 中,每次修改的值都以传入的值来作为最后的值,已经不像之前写 Class 那般增量添加了~你必须这样写才可以保证字段不会被丢失👇


// 省略......

// 每次点击都将年龄 + 1
const plus = () => {
  setUser({
    ...user,
    age: user.age + 1 
  })
  console.log('点击打印', user) // 每次点击打印
}

console.log('render打印', user) // 每次 render 都会打印

// 省略......

也就是我们需要做一个浅拷贝,同时将想要改变的值添加进去,才会达到我们想要的目的👇 May-25-2022 19-03-46.gif

总结

下面我们进行技术总结:

  1. useState接受一个初始值,它返回一个数组,里面包含两个值,第一个是拿来使用的值(state),第二个是用来更新这个值的函数(setState)

  2. 使用setState更新数据时,在它后面使用state还是未更新的值

  3. 如果想要setState依赖上一个setState的值,可以写一个函数,函数接收上一个state,并返回要设置的state,类似于setState(prevState => newState)

  4. setState会将原本的值与传过来的值进行浅比较,如果前后两个值对比相等,则不进行渲染操作,直接return,所以对于Object类型的值在setState时需要进行一个浅拷贝操作。

希望能对你有所收获,如果你有兴趣从源码层面探索更多关于useState相关的知识,你可以看下一篇从源码层面来理解useState,不过这可能需要你具备一定的技术功底。当然你也可以选择粗略的过一下甚至直接跳过,这都不影响你学习React。(不过我还是建议你有精力的话可以粗略的过一下~)

image.png