useState高级用法

2/20/2023 React

React Hook useState高级用法

所谓的高级用法,只不过是一些深层知识点和实用技巧,这是对前面知识点的一个巩固。

# 恢复默认值

组件需求:实现一个计数器,有3个按钮,点击后分别实现:恢复默认值、点击+1,点击-1

实现代码:

import React, {useState} from 'react';

const countComponent = () => {
    
    const initCount = 0;
    const [count, setCount] = useState(initCount);
    
    return <div>
        {count}
        <button onclick={() => {setCount(initCount)}}>init</button>
        <button onclick={() => {setCount(count+1)}}>+1</button>
        <button onclick={() => {setCount(count-1)}}>-1</button>
    </div>
}

export default countComponent;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

代码分析:

  1. 通过额外定义一个变量initCount = 0, 作为count的默认值;
  2. 任何时候想恢复默认值,直接将initCount赋值给count;

# 解决数据异步

还是基于上面那个示例,例如现在新增1个按钮,点击该按钮后执行以下代码:

for(let i = 0; i < 3; i++) {
    setCount(count+1);
}
1
2
3

通过for循环,执行了3次setCount(count+1), 真的会觉得count会 +3 ?
答案是:肯定不会的

无论for循环执行几次,最终实际结果都将是仅执行了一次 +1。

为什么呢?
类组件中setState赋值过程是异步的,同样在Hook中 setXXX 赋值也是异步的,比如上述代码中的setCount。

虽然执行了3次setCount(count+1),可是每一次修改后的count并不是立即生效的。当第2次和第3次执行时获取到count的值和第一次获取到的count值是一样的,所以最后其实相当于执行了1次。

# 解决办法

是不是第一时间想到下面的解决办法:

let num  = count;
for(let i = 0; i < 3; i++) {
    num +=1;
}
setCount(num);
1
2
3
4
5

这样做肯定没问题,只不过有更简单、性能更高的方式。

和类组件中解决异步的方法类似,就是不直接赋值,而是采用"箭头函数返回值的形式"赋值。

把代码修改为:

for(let i = 0; i < 3; i++) {
    setCount(prevData => prevData + 1);
}
1
2
3

代码分析:

  1. prevData为我们定义的一个行参,指当前count应该的值;
  2. prevData + 1并将计算结果直接return出去;
  3. 最终将prevData赋值给count;
  4. 补充一下:可以将prevData修改你喜欢的变量名称,比如prev,只需要确保和后面return里的一致就行。

# 数据类型为Object的修改方法

之前的示例代码中,每个useState对应的值都是简单的string或number,如果对应的值是object,又该如何处理?

例如:

const [person, setPerson] = useState({name: 'liam', age: 28});
1

若想将gae的值修改为18,该怎么写?

我相信有类组件编程经验的,肯定是这么写的:

setPerson({age: 18});
1

在类组件中,setState是执行的是"异步对比累加赋值",何为"对比"?就是先对之前数据属性中是否有age,如果有则修改age的值,同时也不会影响到其他属性的值。我猜测react是使用es6中新增的Object.assign()这个函数来实现这一步的。

但是,用useState定义的修改函数 setXxxx,例如setPerson中,执行的是"异步直接赋值"。

实际执行的结果:

console.log(person); // {name: 'liam', age: 28}
setPerson({age: 18});
console.log(person); // {age: 18}
1
2
3

没错,虽然只是希望修改age的值,但是由于是"直接赋值",导致{age: 18}直接替换了整个{name: 'liam', age: 28}

# 修改方法

我们需要先将person拷贝一份,修改之后再进行赋值(个人建议直接使用结构赋值直接写)。

setPerson({...person,age: 18});
1

完整示例:

import React, {useState} from 'react';

const ObjComponent = () => {
    
    const [person, setPerson] = useState({name: 'liam', age: 28});
    
    const handleChangeName = (event) => {
        setPerson({...person, name: event.target.value})
    }
    
    const handleChangeAge = (event) => {
        setPerson({...person, age: event.target.value})
    }
    
    return <div>
        <input type="text" value={person.name} onChange={handleChangeName} />
        <input type="number" value={person.age} onChange={handleChangeAge} />
        {JSON.stringify(person)}
    </div>
}

export default ObjComponent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 数据类型为Array的修改方法

和数据类型为object相似,都是需要通过先拷贝一次,修改后再整体赋值。

写一个简单的🌰,实现了一个类似学习todolist的功能组件。

import React, {useState} from 'react';

const todoListComponent = () => {
    
    const [str, SetStr] = useState('');
    const [arr, setArr] = useState(['react', 'vue']);
    
    const handleChangeInput = (event) => {
        setStr(event.target.value);
    }
    
    const handleAddHead = () => {
        setArr([str, ...arr]); //添加至头部
        setStr('');
    }
    
    const handleAddEnd = () => {
        setArr([...arr, str]);
        setStr('');
    }
    
    const handleDelHead = () => {
        let newArr = [...arr];
        newArr.shift(); // 从头删除1列表
        setArr(newArr);
    }
    
    const handleDelEnd = () => {
        let newArr = [...arr];
        newArr.pop(); // 从尾删除1列表
        setArr(newArr);
    }

    const delByIndex = (event) => {
        let index = event.target.attributes.index.value;
        let newArr = [...arr];
        newArr.splice(index, 1); // 删除当前项
        setArr(newArr);
    }

    return <div>
        <input type="text" value={str} onChange={handleChangeInput} />
        <button onClick={handleAddHead}>添加至头部</button>
        <button onClick={handleAddEnd}>添加至尾部</button>
        <button onClick={handleDelHead}>从头删除1</button>
        <button onClick={handleDelEnd}>从尾删除1</button>
        <ul>
            {arr.map((item, index) => {
                return <li key={`item${index}`}>学习{index} - {item}
                    <span index={index} onClick={delByIndex} style={{cursor: 'pointer'}}>删除</span>
                </li>
            })}
        </ul>
    </div>
}

export default todoListComponent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

# 性能优化

通过 setXxx 设置新值,但是如果当前新值和当前值完全一样,那么会引发React重新渲染吗?

通过React官方文档可以知道,当使用 setXxx 赋值时,Hook会使用Object.is()来对比当前值和新值,结果为true则不渲染,结果为false就会重新渲染。

let str = 'a';
Object.is(str, 'a'); // true

let str = '18';
Object.is(str, 18); // str为String类型,18为Number类型,因此结果为false

let obj = {name: 'a'};
Object.is(obj, {name: 'a'}); // false
// 虽然obj和{name: 'a'}字面上相同,但是obj==={name: 'a'}为false,并且在Object.is()运算下认为两者不是同一个对象
// 事实上他们确实不是同一个对象,他们各自占用了一份内存。

let obj = {name: 'a'};
let a = obj;
let b = obj;
Object.is(a, b); // 因为a和b都指向obj,因此结果为true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

由上面测试可以看出:

  1. 对于简单类型的值,例如String、Number新旧值一样的情况下是不会引起重新渲染的;
  2. 对于复杂类型的值,即使新旧值"看上去是一样的"也会引起重新渲染,除非新旧值指向同一个对象,或者可以说成新旧值分别是同一个对象的引用;

采用复杂类型的值不是不可以用,很多场景下都需要用到,但是必须得记得上面的测试结果。

为了可能存在的性能问题,如果可以,最好避免使用复杂类型的值。

# 自定义Hook

所谓自定义Hook,就是将Hook函数从函数组件中抽离出来,抽离之后多个函数组件可以公用该自定义Hook,共享该Hook的逻辑。

# 总结

useState是讲解的第一个Hook函数,同时也是使用频率最高的Hook,甚至可以说useState是函数组件开发的基石,所以我在这啰里八嗦的,就是希望自己能理解透彻。加油,共勉