梳理React之useState

606 阅读10分钟

梳理React之useState

官网地址:react.dev/reference/r…

翻译自官网并在翻译的时候记录下自己的思考;

React Hook 之 useState:是一个允许你向你的组件内添加状态变量的hook;

const [state, setState] = useState(initialState);

useState(initialState)

const [age, setAge] = useState(28);

建议在组件的最上面定义状态变量;

initialState:状态的初始值,就比如上面的例子,状态值age在调用setAge设置函数之前的值就是28;

const [todos, setTodos] = useState(() => createTodos());但是函数的行为比较特殊,这个参数在初始渲染之后被忽略;这句话是什么意思啊

官网说:如果你的初始状态为函数,他会被作为初始化函数对待。它应该是纯净的,不应该传递参数,并且可以返回任何类型的返回值。React会在组件初始化的时候调用你的初始化函数,并且将返回值作为你的初始状态值。

useState返回有两个值的数组对象[age, setAge];这个命名方式建议使用[something, setSomething]

age:当前状态值;

setAge:让你更新一个不同的状态值的函数并且触发页面渲染

setAge函数可以直接将要更新的值传过去,也可以传一个依赖前一状态值的计算函数;

setAge((preAge)=> preAge + 1)

Usage

  1. useState是一个Hook函数,你只可以在你自己的组件或者你自定义的Hook中使用;你不能在循环状况里面使用;如果你需要那么做,那么说明你需要抽离一个新的组件并在新的组件里面使用;

  2. 在严格模式下,React会调用两次你的初始化函数,为了帮助你发现一些偶然的异常,但是这种状况只会发生在开发环境中不会影响生产环境;如果你的初始化函数是纯净的,是不会有什么异常行为的;

    🤔:这一点倒是跟useEffect函数一样;

  3. set更新函数只会在下一次渲染的时候更新状态变量;所以如果你在set更新函数调用后面读取状态变量,你拿到的将会是渲染之前的旧值;这个错误在开发中极易出现;

    // 这里我调用了更新函数
    setConfettiLocation({
          x: confettiOptions?.origin?.x,
          y: confettiOptions?.origin?.y
        })
    // 情况1:然后这里取状态值赋给一个变量x,但是因为页面还未渲染,这里的x还是上一次的旧值,并不是最新的状态值
        let x = confettiLocation.x;
    // 解决方案:直接使用上面赋值的变量confettiOptions?.origin?.x;
    // 情况2: 在调用更新函数后调用了一个其他函数,然后在otherfun函数中直接使用confettiLocation状态值,这时候其实也不是最新的状态值;
    otherfun()
    // 解决方案1,传参otherfun(confettiOptions?.origin?.x)
    // 2. 监听confettiLocation状态值,我平常用这种比较多;
    // 解决方案3: 使用函数setConfettiLocation((prevalue)=> {
    //   x = prevalue
    // })
    
  4. 如果你提供的新的值和当前状态值是相等的,可以用[Object.is]进行比较,React会跳过组件及其孩子的重新渲染;这是一种优化的方式;虽然在某些情况下,React仍然在跳过子组件之前调用你的组件,但是这是不会对你的代码造成影响的;

  5. React批量更新状态;这个意思就是在你的事件处理中可能同时调用了多个状态值的更新函数,他会在一次组件的渲染中进行更新,并不是每调用一次更新函数就造成一次渲染;这也可以上面的第三点;这样可以在单事件中降低组件的多次渲染;

    这里补充一点,如果在同一次渲染中多次更新一个状态值,以最后一次为准;

    在极少数的情况中,你需要强制react的渲染的时机更早,比如访问DOM,可以使用flushSync ??

    🤔:先存个疑

  6. 在渲染的时候调用set更新函数只允许你在当前组件内部渲染;React将丢弃它的输出,并立即尝试用新状态再次呈现它。这种模式很少需要,但您可以使用它来存储以前呈现的信息

    🤔:这个警告点我没有理解;这个可以看下第10点,因为平常几乎没有在渲染的时候调用set更新函数;

  7. 在状态值为对象时,更新对象的部分值时,要重新创建新的状态值,避免直接修改(对象地址引用);

    setConfettiLocation({
          ...confettiLocation,
          y: confettiOptions?.origin?.y
        })
    
  8. 避免重新创建初始状态

    🤔:看这个标题是很迷惑的对不对,都初始状态了,肯定是第一次生效啊!其实,确实是这样的!

    React只会存一次状态的初始值并且在下一次渲染的时候忽略它;

    但是呢,如果我们的初始状态值是一个函数就需要注意一下了:

    // 这两种写法存在差别的
    const [todos, setTodos] = useState(createInitialTodos())
    
    const [todos, setTodos] = useState(createInitialTodos)
    

    对于第一种写法:createInitialTodos();虽然函数的返回值只会被用在初始渲染,但是在每次组件渲染的时候,该函数都会执行;

    使用的是官网提供的示例:

    useState(createInitialTodos())的log输出:

image.png

我在页面的操作:在输入框中输入 “add item”对应中间的八次执行,点击add按钮进行添加操作,对应下面的那次执行,最上面才是对应初期状态的执行;

image.png 然后再来试一下第二种写法:useState(createInitialTodos),传入函数变量useState(createInitialTodos)的log输出(同样的操作): image.png

只传函数本身的时候,React只会在初始化的时候去调用该函数,只会执行一次;

  1. 使用key值重置状态

    在渲染列表的时候,经常会使用“key”属性,但是呢,他还有另一种用法;

    你可以重置一个组件的状态通过给这个组件传递一个不同的key值。

    来个🌰(也是官网的例子):现在页面上有个<Form/>子组件,然后父组件有个重置按钮来重置传入<Form/>子组件的key值,当改变这个key值的时候,<Form/>子组件就会重新创建;

    🤔:感觉这个用法应该会很有用;

key.gif

  1. 从上一次渲染中存储信息

    通常状况下,你是在事件处理中来更新状态值;然而,在极少数的情况中,你可能希望在响应渲染的时候调整状态;例如,在属性改变的时候更改状态值;

    在大多数情况中,你不需要这样做:

    • 如果这个值你需要通过当前的某个属性或者其他状态来计算,那么删除这个冗余的状态;

      如果你担心计算太频繁,可以借助[useMemo](react.dev/reference/r…) Hook;

    • 如果你想要重置整个组建树的状态,请通过传递一个不同的key值来实现(就是上面9的例子了);

    • 如果可以的话,请在事件处理中更新所有相关的状态值;

    在这些都不适用的极少数情况,你可以使用一种模式,基于目前为止渲染的值来更新状态值,通过在组建渲染时调用set更新函数;

    下面是一个🌰:这个CountLabel组件展现传过来的属性值count;

    现在你想在页面中展示当前的计数器与上一次相比是进行了增加还是减少;

    只依靠count属性是不能告诉你的,你需要跟踪上一次的状态值;

    在CountLabel组件中添加状态值prevCount来跟踪上一次的状态值;

    然后添加状态值:trend来记录count是增加还是减少;

    比较prevCountcount,只要两者不相等,就更新prevCountcount

    现在就可以看到count和上一次渲染之间的变化了;

    CountLabel.tsx:

    function CountLabel({ count }: any) {
      const [prevCount, setPrevCount] = useState(count)
      const [trend, setTrend] = useState<any>(null)
      if (prevCount !== count) {
        setPrevCount(count)
        setTrend(count > prevCount ? 'increasing' : 'decreasing')
      }
      return (
        <>
          <h1>{count}</h1>
          {trend && <p>The count is {trend}</p>}
        </>
      )
    }
    

    请注意,如果你在渲染的时候调用set更新函数,它必须在像prevCount !== count这样的条件中,并且在条件中必须有一个像setPrevCount(count)这样的调用。这样,你的组件就会陷入一个重复渲染的死循环,直到页面崩溃;而且,你只能像这样更新当前渲染组件的状态,在渲染的过程中调用另一个组件的set更新函数是错误的;最后,您的set调用仍然应该更新状态而不发生突变——这并不意味着您可以打破纯函数的其他规则。

    这种模式理解起来是困难的并且很容易去避免;最好是在一个Effect中去更新状态;

    当你在渲染的时候调用set更新函数,在你的组件用return语句退出后,React会在呈现子组件之前立即重新呈现该组件,这样,子组件就不会渲染两次;组件函数的其余部分仍将执行(结果将被丢弃)。如果你的条件低于所有的Hook调用,你可以添加一个提前返回;提前重新开始呈现。

    🤔: 看完官网对于CountLabel这个例子的用法后,我觉得还是尽量避免在组件渲染的时候调用set更新函数吧;

Troubleshooting

  1. 我已经更新了状态值,但是控制台打印的是旧的状态值?

    function handleClick() {
      console.log(count);  // 0
    
      setCount(count + 1); // Request a re-render with 1
      console.log(count);  // Still 0!
    
      setTimeout(() => {
        console.log(count); // Also 0!
      }, 5000);
    }
    

    这个问题在上面其实提到过,React的状态更新不是一调用立马就执行的,在页面渲染后状态才是最新的状态值;

  2. 我已经更新了状态值,但是页面没有渲染?

    obj.x = 10;  // 🚩 Wrong: mutating existing object
    setObj(obj); // 🚩 Doesn't do anything
    

    这个也在上面提到过,如果你的状态值是对象或者数组这种类型,就涉及到对象的引用,obj.x = 10;这种只是修改了对象的某一个属性值,对象obj指向的对象的地址并没有改变,所以set更新函数监测不到状态值改变,自然不会渲染;

    如果你想让页面进行渲染,就必须要对对象重新赋值;

    setObj({
      ...obj,//利用解构赋值
      x: 10
    });
    
  3. 报错: “Too many re-renders”

    如果你遇到Too many re-renders. React limits the number of renders to prevent an infinite loop.的报错;

    很明显,你的状态设置肯定陷入了一个死循环;

    // 🚩 Wrong: calls the handler during render
    return <button onClick={handleClick()}>Click me</button>
    
    // ✅ Correct: passes down the event handler
    return <button onClick={handleClick}>Click me</button>
    
    // ✅ Correct: passes down an inline function
    return <button onClick={(e) => handleClick(e)}>Click me</button>
    

    如果你找不到造成这个错误的原因,单击控制台错误的箭头,查看JS的错误信息堆栈,找到导致错误的set更新函数;

  4. 我的初始化和状态更新执行了两遍

    在严格模式下,React会执行两次,不会影响你的代码逻辑;如果对你的代码造成了影响,则证明你的代码逻辑可能有点子问题😂;

    setTodos(prevTodos => {
      // 🚩 Mistake: mutating state
      prevTodos.push(createTodo()); // 这种写法在严格模式下会添加两次createTodo()返回值,从而导致状态值的数据重复,切状态值并没有发生变化,不会重新渲染;
    });
    
    setTodos(prevTodos => {
      // ✅ Correct: replacing with new state
      return [...prevTodos, createTodo()]; // 这种写法数据不会出错,该是啥就是啥
    });
    
  5. 我试图将状态值设置为一个函数,但是函数却被调用了

    // 这个应该是跟状态的初始值的用法混了,这是将someFunction的返回值作为状态的初始值;
    // 但是,确实很容易误解
    const [fn, setFn] = useState(someFunction);
    
    function handleClick() {
      setFn(someOtherFunction);
    }
    

    所以,以后如果想将函数作为状态值,一定要记住按照👇的这种写法:

    const [fn, setFn] = useState(() => someFunction);
    
    function handleClick() {
      setFn(() => someOtherFunction);
    }