在Hook中还是屏幕组件中取消监听器订阅?哪种方案更优?
你的当前方案并不合理,存在多个潜在问题,不建议将订阅的启动与取消逻辑拆分到组件和Hook两处。以下是具体弊端和优化建议:
当前方案的核心弊端
订阅泄漏风险极高
你的subscriber是普通let变量,每次组件重新渲染时,useUserHook会重新执行,subscriber会被重新初始化为空函数。而Hook内的useEffect依赖空数组,只在组件挂载时执行一次,它的清理函数引用的是第一次渲染时的空函数。当组件调用listenUsersCollection赋值真正的订阅函数后,后续组件重新渲染会丢失这个订阅函数的引用,导致组件卸载时无法执行取消订阅操作,最终引发Firestore订阅泄漏。重复订阅无法清理
如果组件因某种原因多次调用listenUsersCollection(比如组件重新渲染时useEffect的依赖变化),每次调用都会创建新的Firestore订阅,但旧的订阅函数会被subscriber变量覆盖,没有被主动取消,同样会造成订阅泄漏。逻辑割裂,维护性差
订阅的启动和取消是强关联的逻辑,拆分到组件和Hook两处后,后续维护时容易遗漏取消逻辑,或者在修改启动逻辑时忘记同步调整取消逻辑,增加出错概率。
更合理的实现方案
建议将订阅的完整生命周期(启动、更新、取消)都封装在Hook内部,避免将底层操作暴露给组件。这里提供两种常见实现方式:
方式1:Hook自动监听(组件无需手动调用)
如果users集合的监听是组件挂载时就需要的,直接在Hook内部的useEffect中完成订阅与取消:
export function useUser() { const [users, setUsers] = React.useState([]); React.useEffect(() => { // 创建订阅并保存取消函数 const unsubscribe = firestore().collection('users').onSnapshot(res => { const userList = res.docs.map(doc => doc.data()); setUsers(userList); }); // 组件卸载时取消订阅 return unsubscribe; }, []); return { users }; }
组件使用时直接获取数据即可:
const { users } = useUser();
方式2:支持手动触发/停止监听(需在Hook内部管理状态)
如果需要组件手动控制监听的启动(比如按需加载),要在Hook内部用状态保存订阅函数,确保每次启动新订阅前先取消旧的:
export function useUser() { const [users, setUsers] = React.useState([]); const [unsubscribe, setUnsubscribe] = React.useState(() => {}); // 手动启动监听的函数 const listenUsersCollection = React.useCallback(() => { // 先取消之前的订阅(如果存在) unsubscribe(); // 创建新订阅并保存取消函数 const newUnsubscribe = firestore().collection('users').onSnapshot(res => { const userList = res.docs.map(doc => doc.data()); setUsers(userList); }); setUnsubscribe(newUnsubscribe); }, [unsubscribe]); // 组件卸载时确保取消订阅 React.useEffect(() => { return () => { unsubscribe(); }; }, [unsubscribe]); // 可选:提供手动停止监听的函数 const stopListening = React.useCallback(() => { unsubscribe(); setUnsubscribe(() => {}); }, [unsubscribe]); return { users, listenUsersCollection, stopListening }; }
组件使用时:
const { users, listenUsersCollection } = useUser(); React.useEffect(() => { listenUsersCollection(); // 组件卸载时会自动触发Hook内的清理逻辑 return () => { /* 如需提前停止可调用stopListening */ }; }, []);
结论
永远不要将订阅的启动与取消逻辑拆分到不同层级,应该让自定义Hook完全封装订阅的生命周期,组件只需要关注业务逻辑(比如使用数据、触发监听),这样既能避免泄漏问题,也能提升代码的可维护性。
内容的提问来源于stack exchange,提问作者1110

