react hook与绑定原生事件

379 阅读3分钟

背景

小伙伴在需求实现中,不得不手动绑定click事件到dom元素上。绑定正常但是绑定后无法获取到最新的state。 代码示意如下:

const App = () => {
    const [count, setCount] = useState(0);

    const handleClick = event => {
        setCount(count + 1);
    };

    useEffect(() => {
        const btnDom = document.querySelector('#btn');
        btnDom.addEventListener('click', handleClick);

        return () => {
            window.removeEventListener('click', handleClick);
        };
    }, []);

    return (
        <div>
            <button id="btn">增加</button>
            <span>{count}</span>
        </div>
    );
}

结果发现,点击增加按钮,数字只会增加一次,并不是想象中的点击一次增加一次数字。

原因

乍一看好像挺奇怪,但是仔细分析一下其实原因很简单。 第一次渲染,执行App函数,count = 0,组件挂载完成时,将会在按钮上绑定事件handleClick

当第一次点击按钮时,count将由0变为1,触发下一次渲染,App函数被再次执行

第二次渲染,执行App函数,count = 1,此时不再绑定事件,按钮上绑定的回调函数还为第一次App函数执行时的handleClick函数

当第二次点击按钮时,执行的函数为第一次App函数执行时声明的handleClick函数,因为js为静态作用域(词法作用域),此时handleClick里的count变量为该函数声明时的值,即第一次App执行时所能拿到的count = 0

所以无论多少次点多少次按钮,count都只能为1

解决方案

极其不推荐例子中用原生api来绑定事件的方式,但是如果避免不了要怎么办呢?

【思路一】获得最新的state

既然普通的写法获取不到最新的state,那就想办法拿到最新的state呗。

setState

react已提供了解决方案,只要进行这样的修改即可:

setCount((prevCount) => prevCount + 1);

但是这样会有个问题 如果依赖了多个state值的话 就不能使用这样的方法了

useReducer

还有一种方法,可以使用useReducer, 利用useReducer可以获得最新的state的特性来实现

const App = () => {
    const [count, dispatchCount] = useReducer((state, action) => {
        if (action === 'add') {
            return state + 1;
        }
        return state;
    }, 0);

    const handleClick = event => {
        dispatchCount('add');
    };

    useEffect(() => {
        const btnDom = document.querySelector('#btn');
        btnDom.addEventListener('click', handleClick);

        return () => {
            window.removeEventListener('click', handleClick);
        };
    }, []);

    return (
        <div>
            <button id="btn">增加</button>
            <span>{count}</span>
        </div>
    );
}

useRef

还有一种通过ref来存值,不过官方不推荐使用useRef

const App = () => {

    const [count, setCount] = useState(0);

    const countRef = useRef(0);

    const handleClick = () => {
        countRef.current = countRef.current + 1;
        setCount(countRef.current);
    }

    useEffect(() => {
        const btnDom = document.querySelector('#btn');

        btnDom.addEventListener('click', handleClick);

        return () => {
            window.removeEventListener('click', handleClick);
        };
    }, []);

    return (
        <div>
            <button id="btn">增加</button>
            <span>{count}</span>
        </div>
    );
}

【思路二】值更新时重新声明函数

既然我们只能拿到声明时的值,那么我们值改变时重新声明函数不就能拿到最新的state了吗。

useEffect

还有一种方法,就是使用useEffect,以实现在值变更时,重新绑定事件, 这样保证每一次点击时都能拿到最新的值

const App = () => {
    const [count, setCount] = useState(0);

    const handleClick = event => {
        setCount(count + 1);
    };

    useEffect(() => {
        const btnDom = document.querySelector('#btn');
        btnDom.addEventListener('click', handleClick);

        return () => {
            window.removeEventListener('click', handleClick);
        };
    }, [count]); // count一变 就重新绑

    return (
        <div>
            <button id="btn">增加</button>
            <span>{count}</span>
        </div>
    );
}

更为细节的解决方案,可以看stackoverflow的这个问题How to register event with useEffect hooks?里的的第二个答案,本文大部分思路也来自于此回答。

延伸阅读

useState
Why am I seeing stale props or state inside my function?
Dealing With Stale Props and States in React’s Functional Components

参考

本文参考自: How to register event with useEffect hooks?