React hooks 一个bug

131 阅读3分钟

        偶然间发现一个hooks的bug,给window或者body绑定事件,事件回调里拿不到最新的状态(只能拿到绑定时的状态值),但是给dom元素添加的绑定事件是可以的。简化后的代码如下:

import React, { useState, useEffect } from 'react';

const addLabel: React.FC = () => {
    const [dataSource, setDataSource] = useState<any>([]);

    useEffect(() => {
        window.onclick = () => {
            setDataSource([...dataSource, {
                labelIdentify: Math.random(),
                labelName: 0,
            }])
        }
    }, []);

    return (<div>
        {
            dataSource?.map((item: any) => (<p>{item?.labelIdentify}</p>))
        }
    </div>);
};

export default addLabel;

calss写法不存在这个问题:

import React from 'react';

class addLabel extends React.Component {
    constructor(props: any) {
        super(props)
        this.state = {
            dataSource: []
        }
    }

    componentDidMount(): void {
        window.onclick = () => {
            let { dataSource } = this.state as any
            this.setState({
                dataSource: [...dataSource, {
                    labelIdentify: Math.random()
                }]
            })
        }
    }

    render(): React.ReactNode {
        let { dataSource } = this.state as any
        return <div>
            {
                dataSource?.map((item: any) => (<p>{item?.labelIdentify}</p>))
            }
        </div>
    }
}

export default addLabel;

再看给dom元素绑定事件,效果是正常的。观察给dom元素和window绑定事件的区别,给window绑定事件是在组件加载完成时候完成的,所以只触发一次,而给dom元素绑定事件是在render里边绑定的,每次组件渲染(状态更新)都会执行,也会重新绑定事件,所以给dom元素绑定的事件回调里能拿到最新的状态值(状态更新->绑定事件->事件触发拿到最新的状态值)。找到问题,解决就好解决了。

第一种思路:只要让给window绑定的操作在状态值更新后再绑一次就好了。

方法一:把绑定事件写在render函数里(这里只说实现,不考虑性能)

import React, { useState, useEffect } from 'react';

const addLabel: React.FC = () => {
    const [dataSource, setDataSource] = useState<any>([]);

    window.onclick = () => {            setDataSource([...dataSource, {
                labelIdentify: Math.random(),
                labelName: 0,
            }])
        }
    return (<div>
        {
            dataSource?.map((item: any) => (<p>{item?.labelIdentify}</p>))
        }
    </div>);
};

export default addLabel;

方法二:监听要获取状态值,值改变重新绑定事件

import React, { useState, useEffect } from 'react';

const addLabel: React.FC = () => {
    const [dataSource, setDataSource] = useState<any>([]);

    useEffect(() => {
        window.onclick = () => {
            setDataSource([...dataSource, {
                labelIdentify: Math.random(),
                labelName: 0,
            }])
        }
    }, [dataSource]);    return (<div>
        {
            dataSource?.map((item: any) => (<p>{item?.labelIdentify}</p>))
        }
    </div>);
};

export default addLabel;

第二个思路,为什么class写法可以,hooks写法不行?class和hooks最大的区别是什么?我们都知道class和hooks最大的区别就是this,class中有this概念,有实例概念,实例是什么,简单来说就是个对象。而hooks是个函数,对某些特定数据封装过的函数。所以从数据类型角度考虑,利用引用数据类型引用的是地址,初次绑定事件回调里边取值不直接取状态值,而是把状态放到一个地址上,状态更新时候更新地址里的值(render或者useEffect 或者其他都可以),取值通过地址来取,这样通过地址取到的状态也就是最新的状态。

import React, { useState, useEffect } from 'react';

let object = {}
const addLabel: React.FC = () => {
    const [dataSource, setDataSource] = useState<any>([]);
    object.state = dataSource
    useEffect(() => {
        window.onclick = () => {
            setDataSource([...dataSource, {
                labelIdentify: Math.random(),
                labelName: 0,
            }])
        }
    }, []);    
return (<div>
        {
            dataSource?.map((item: any) => (<p>{item?.labelIdentify}</p>))
        }
    </div>);
};

export default addLabel;

当然这个object 可以是window的一个属性或者任意一个全局属性,ref也可以,只要是个全局对象就行。

        其中一个应用场景就是扫码枪监听keydown事件,如果绑定在组件加载完成,而在事件回调中又需要获取最新状态的时候,就只能获取绑定时候的状态值(初始值)

        后边查看文章发现定时器也有类似的问题,详细看这里 zhuanlan.zhihu.com/p/579103609

附一篇useref文章blog.csdn.net/u011705725/…