React Native:useEffect监听NativeEventEmitter时useState状态无法更新
我在React Native中实现了按钮点击调用原生模块函数,原生模块通过RCTDeviceEventEmitter发射textColor事件,JS端用NativeEventEmitter捕获事件,每次捕获后切换ncolor这个useState变量(在false/true间切换)来控制Text组件样式。
现有代码
JS端监听代码
const [ncolor, setnColor] = useState(false); useEffect(() => { const eventEmitter = new NativeEventEmitter(NativeModules.ToastExample); this.eventListener = eventEmitter.addListener('textColor', event => { console.log('event', ncolor, event); // "someValue" setnColor(!ncolor); }); return () => { this.eventListener.remove(); //Removes the listener }; }, []);
原生模块触发函数(Java)
@ReactMethod public void bangNotification(){ Log.d("bang", "Bannnngg"); notificationClick(); }
原生事件发射函数(Java)
public void notificationClick(){ Log.d("bang", "Notification clicked"); WritableMap map = Arguments.createMap(); map.putString("val", "red"); try{ reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("textColor", map); } catch (Exception e){ Log.d("ReactNative", "Caught Exceptioin:" + e.getMessage()); } }
遇到的问题
- 目前切换行为仅在初始化后生效一次,无法反复切换状态值,原因是什么?
- 为何必须在
useEffect的依赖数组中指定ncolor?即使添加了依赖,监听器能正常触发日志,但状态依旧无法更新,这又是为什么?
useEffect(() => { const eventEmitter = new NativeEventEmitter(NativeModules.ToastExample); this.eventListener = eventEmitter.addListener('textColor', event => { console.log('event', ncolor, event); setnColor(!ncolor); }); return () => { this.eventListener.remove(); }; }, [ncolor]); // 添加了ncolor依赖
问题原因及解决方案
1. 初始代码仅生效一次的原因
这是闭包陷阱导致的:useEffect的依赖数组是空的,只会在组件初始化时执行一次。此时事件监听器里的ncolor捕获的是初始化时的状态值(false),后续无论状态怎么更新,监听器里的ncolor永远是初始的false,所以每次调用setnColor(!ncolor)都会把状态设为true,之后再触发事件时,还是基于false取反,状态不会再变化。
2. 添加ncolor依赖后仍失效的原因
当把ncolor加入依赖数组后,每次ncolor变化时,useEffect会重新执行:移除旧的监听器,创建新的监听器。但问题在于,函数组件里没有实例,this指向全局对象,你用this.eventListener存储监听器实例的操作无效,旧监听器并没有被真正移除,导致多个监听器同时存在。
当事件触发时,多个监听器会依次执行:比如第一次状态是false,新监听器触发后把状态设为true;此时useEffect重新执行创建新监听器(捕获的ncolor是true),但旧监听器还在,下次事件触发时,旧监听器把true变回false,新监听器又把false变回true,最终状态看起来没变化,日志里会看到ncolor交替打印,但实际状态被来回修改抵消了。
正确的解决方案
- 不要用
this存储监听器,改用函数组件的useRef来保存监听器实例,确保每次移除的是正确的监听器。 - 切换状态时,使用函数式更新(
setnColor(prev => !prev)),这样不需要依赖ncolor,也能获取到最新的状态值,避免闭包问题。
修改后的JS代码:
const [ncolor, setnColor] = useState(false); const eventListenerRef = useRef(null); // 用useRef存储监听器 useEffect(() => { const eventEmitter = new NativeEventEmitter(NativeModules.ToastExample); eventListenerRef.current = eventEmitter.addListener('textColor', event => { console.log('event', ncolor, event); // 使用函数式更新获取最新状态 setnColor(prevColor => !prevColor); }); return () => { // 移除当前的监听器 if (eventListenerRef.current) { eventListenerRef.current.remove(); eventListenerRef.current = null; } }; }, []); // 依赖数组为空,只初始化一次
补充说明
- 函数式更新
setnColor(prev => !prev)的作用是:React会把当前最新的状态值作为参数传给回调函数,这样不管监听器什么时候触发,都能拿到最新的状态,不会受闭包影响。 useRef存储的变量不会随组件重新渲染而变化,能始终保存最新的监听器实例,确保清理函数能正确移除监听器,避免内存泄漏和多监听器冲突。
内容的提问来源于stack exchange,提问作者Saheel Sapovadia

