# React常见的易错用法

日常中在使用React开发,记录一些易错的用法。

# 「不可变性」相关

由于“不可变性”,使得React在渲染过程中的每一处都有可能产生Bug。

以下搜集于TC39提案(Records & Tuples)对React的影响

  • 性能上:可以避免的重复渲染
  • 行为上:重复执行一些多余的effect、无限循环
  • API上:对于一些重要的对象不能做到准确表示

大部分的React开发者都已经习惯了不可变性(immutability)

通俗点来说,就是每次试图去更新一个不可变的对象时,都会创建新的对象

alt

alt

# 事件监听器里的state不会更新

# 现象

处理事件监听时注册监听器,发现监听器内无法获取最新state值。

  const printId = () => {
    console.log(id);
  };

  // 绑定事件
  useEffect(() => {
    document.getElementById("btn").addEventListener("click", printId);
    console.log("事件绑定完成");
  }, []);
1
2
3
4
5
6
7
8
9

Demo地址 (opens new window)

# 分析

  1. 初步怀疑为“js监听器本身逻辑处理问题”
  2. 原组件是用hook实现。通过改成class组件写法(Demo (opens new window)),发现在监听器内可正常获取最新state
  3. 和state值的存储形式、更新机制有关

# 总结

Hooks可以用来实现绝大部分class能解决的事情,但也有许多不一样的特性在日常开发要注意:

  1. 它是个纯函数,每次触发更新拥有完全独立的函数作用域
  2. 所以每次render,hooks里定义的每个函数都是一个新的引用地址(如不用useCallback包裹)
  3. 自身的useState、ussEffect等都是用单链表管理。重渲染时通过更新链表

所以,事件监听注册的监听器函数会在下次render时改变地址,而“前监听器”内部管理的state依然是绑定时的那个旧值(类似闭包)。

# 解决

法1:利用useEffect清除函数,并将id加入依赖项

  useEffect(() => {
    document.getElementById("btn").addEventListener("click", printId);
    console.log("事件绑定完成");

    return () => {
      document.getElementById("btn").removeEventListener("click", printId);
    };
  }, [id]);
1
2
3
4
5
6
7
8

法2:组件改成class写法

# React合成事件、生命周期、原生事件下的setState

先点击 test 按钮,再点击 test1 按钮。求输出

import React from "react";
import "./styles.css";

export default class App extends React.Component {
  state = {
    count: 0
  };
  componentDidMount() {
    this.setState({ count: this.state.count + 1 });
    console.log("1: ", this.state.count);

    this.setState({ count: this.state.count + 2 });
    console.log("2: ", this.state.count);

    this.setState({ count: this.state.count + 1 });
    console.log("3: ", this.state.count);

    setTimeout(() => {
      console.log("4: ", this.state.count);
      this.setState({ count: this.state.count + 1 });
      console.log("5: ", this.state.count);
    }, 0);

    document.getElementById("test1").addEventListener("click", () => {
      this.setState({ count: this.state.count + 1 });
      console.log("7", this.state.count);
    });
  }

  increment = () => {
    this.setState({ count: this.state.count + 1 });
    console.log("6", this.state.count);
  };

  render() {
    return (
      <div className="App">
        <div onClick={this.increment}>test</div>
        <div id="test1">test1</div>
      </div>
    );
  }
}

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
// 输出
1:  0
2:  0
3:  0
4:  1
5:  2
6:  2
7:  4
1
2
3
4
5
6
7
8
更新时间: 11/21/2021, 2:45:24 AM