在 React Hooks
中应用第三方库的事件时,很多人会写成这样(指的就是我):
const [count, setCount] = useState(0);
useEffect(() => {const library = new Library();
library.on("click", () => {console.log(count); // 拿不到最新的 count
});
}, []);
这样写会有问题:
它只会在这个组件加载时,绑定事件,如果这个事件中用到了其余的 state
,那么这个状态发生变化时事件中是拿不到最新的 state
你会想到,我把 state
放到依赖项中:
const [count, setCount] = useState(0);
useEffect(() => {const library = new Library();
// click 事件会反复绑定
library.on("click", () => {console.log(count);
});
}, [count]);
这样做又会有新问题:click
事件会反复绑定
这时候你说那我先卸载 click
事件,在绑定事件:
const [count, setCount] = useState(0);
useEffect(() => {const library = new Library();
library.on("click", handleClick);
return () => {
// 卸载不掉事件,还是会反复绑定
handleClick && library.un("click", handleClick);
};
}, [count]);
const handleClick = () => {console.log(count);
};
你惊奇的发现,竟然卸载不掉之前的事件,还是会反复绑定事件。
如何解决这个问题呢?
应用 addEventListener 代替第三方库的事件
这里应用 addEventListener
代替第三方库的事件,初始代码
const Test = (props) => {const ref = useRef();
const [count, setCount] = useState(0);
useEffect(() => {const handleClick = (event) => {console.log("clicked");
console.log("count", count);
};
const element = ref.current;
element.addEventListener("click", handleClick);
return () => {element.removeEventListener("click", handleClick);
};
}, []);
const onClickIncrement = () => {setCount(count + 1);
};
return (
<>
<h2>Test</h2>
<button onClick={onClickIncrement}> 点击 +1</button>
<div>count: {count}</div>
<button ref={ref}>Click Test Button</button>
</>
);
};
办法一:state 变动,卸载 / 绑定事件
将 state
放在依赖项中,就要解决 state
变动时,事件反复绑定的问题
解决事件反复绑定问题,首先想到的是事件卸载
你很容易就会想到这样写
useEffect(() => {handleClick && ref.current.removeEventListener("click", handleClick);
ref.current.addEventListener("click", handleClick);
}, [count]);
const handleClick = () => {console.log(count);
};
这在 React Hooks
中是一个坑,state
变动后会 handleClick
事件函数会从新申明,新的 handleClick
和之前的 handleClick
不是一个事件函数,导致 removeEventListener
移除的事件函数不是之前的事件函数
那你又会想到,我给 handleClick
加个 useCallback
useEffect(() => {handleClick && ref.current.removeEventListener("click", handleClick);
ref.current.addEventListener("click", handleClick);
}, [count]);
const handleClick = useCallback(() => {console.log(count);
}, []);
这样写的话还是会有同一个问题:依赖项为空数组,就拿不到最新的 state
;依赖项中放入 state
,state
变动后就不是同一个事件函数了,无奈移除事件
如何解决这个问题呢?
把事件函数保留为状态:
- 当
count
变动时,挂载事件,同时将事件函数保留为state
- 当
eventFn.fn
变动时,在useEffect return
中卸载之前的事件函数(这里利用的是闭包)
具体的代码:
const Test = () => {const ref = useRef();
const [count, setCount] = useState(0);
const [eventFn, setEventFn] = useState({fn: null});
useEffect(() => {mountEvent();
}, [count]);
const mountEvent = () => {if (!ref.current) return;
// eventFn.fn && ref.current.removeEventListener("click", eventFn.fn); // 上面看不懂的话,也能够这样写
ref.current.addEventListener("click", handleClick);
setEventFn({fn: handleClick});
};
useEffect(() => {return () => {eventFn.fn && ref.current.removeEventListener("click", eventFn.fn); // 这里用的是闭包,和下面正文局部任选其一
};
}, [eventFn.fn]);
const handleClick = () => {console.log(count);
};
const onClickIncrement = () => {setCount(count + 1);
};
return (
<>
<h2>Test</h2>
<button onClick={onClickIncrement}> 点击 +1</button>
<div>count: {count}</div>
<button ref={ref}>Click Test Button</button>
</>
);
};
办法二:应用闭包的形式卸载事件
利用闭包,能够将办法一简化
const Test = () => {const ref = useRef();
const [count, setCount] = useState(0);
useEffect(() => {
const element = ref.current;
element.addEventListener("click", handleClick);
return () => {element.removeEventListener("click", handleClick);
};
}, [count]);
const handleClick = () => {console.log(count);
};
const onClickIncrement = () => {setCount(count + 1);
};
return (
<>
<h2>Test</h2>
<button onClick={onClickIncrement}> 点击 +1</button>
<div>count: {count}</div>
<button ref={ref}>Click Test Button</button>
</>
);
};
useEffect return
中的变量用的是闭包,这点刚开始学的时候不好了解
办法三:应用 ref 保留状态
ref
保留的数据尽管不能用于页面渲染,但能够作为 state
备份,在 state
变动时更新 ref
在事件函数中就能拿到最新的 stateRef
const Test = () => {const ref = useRef();
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {countRef.current = count;}, [count]);
useEffect(() => {
const element = ref.current;
element.addEventListener("click", handleClick);
}, []);
const handleClick = () => {console.log(countRef.current);
};
const onClickIncrement = () => {setCount(count + 1);
};
return (
<>
<h2>Test</h2>
<button onClick={onClickIncrement}> 点击 +1</button>
<div>count: {count}</div>
<button ref={ref}>Click Test Button</button>
</>
);
};
优化 state 手动保护
下面三种办法,都有个问题,state
须要手动保护
这一步如何优化呢?
办法一和办法二,优化的形式都一样:将依赖项是 count
改为 state
const [state, setState] = useState({count: 0});
useEffect(() => {// ...}, [state]);
办法三的优化是,用 stateRef
保留 ref
对象,当 state
变动时,遍历 state
给 stateRef
赋值
事件函数中应用 stateRef
const [state, setState] = useState({count: 0});
const stateRef = useRef({});
useEffect(() => {Object.keys(state).forEach((key) => {stateRef.current[key] = state[key];
});
}, [state]);
写在最初
这个问题困扰了我很久,写业务时,我始终用办法一,随着依赖项越来越多,保护是个噩梦(办法三也是噩梦)
我始终在想如何在 addEventListener
中拿到最新的 state
明天原本想记录下办法一和办法三的
在一点点写下笔记的时候,发现了办法二,能够利用闭包解决事件卸载问题,从而又发现了优化保护依赖项的办法
如果明天不写笔记,这个问题还会持续困扰着我