React BLE Provider中useCallback循环依赖问题的解决咨询
你遇到的是典型的React Hooks循环依赖问题:connect 依赖 onDisconnected,而 onDisconnected 又依赖 connect,导致两个useCallback的依赖形成闭环,同时去掉useCallback又会触发ESLint关于依赖变化的警告。下面是几种实用的解决办法:
方法一:用useRef保存函数引用(最直接)
利用useRef的引用稳定性,让onDisconnected始终调用最新版本的connect,同时不用把connect加入它的依赖列表:
const BLEDataContext = createContext<{ bleData: number | null; }>({ bleData: null }); export const BLEDataProvider = ({ children }) => { const [bleData, setBleData] = useState<number | null>(null); // 创建ref保存connect的最新引用 const connectRef = useRef<typeof connect>(null); const connect = useCallback(async () => { const device = await navigator.bluetooth.requestDevice({ filters, optionalServices, }); // [...] device.addEventListener('gattserverdisconnected', onDisconnected); return device; }, [onDisconnected, /* 其他依赖 */]); // 用useEffect更新ref,确保connectRef始终指向最新的connect useEffect(() => { connectRef.current = connect; }, [connect]); const onDisconnected = useCallback((event: any) => { // [...] if (someCondition) { // 调用ref里保存的最新connect connectRef.current?.(); } }, [/* 这里只放除了connect之外的其他依赖 */]); return ( <BLEDataContext.Provider value={{ bleData }}> {children} </BLEDataContext.Provider> ); }
这样一来,onDisconnected的依赖列表里不再需要connect,循环依赖直接被打破,同时ESLint警告也会消失——因为connectRef是稳定的引用,不会随渲染变化。
方法二:用useReducer解耦逻辑
如果你的连接逻辑比较复杂,可以把connect和onDisconnected的逻辑迁移到useReducer中,利用dispatch的稳定性来避免依赖循环:
const BLEDataContext = createContext<{ bleData: number | null; }>({ bleData: null }); // 定义reducer处理连接相关动作 const bleReducer = (state, action) => { switch (action.type) { case 'SET_DATA': return { ...state, bleData: action.payload }; case 'RECONNECT': // 在这里执行重连逻辑 action.payload.connect(); return state; default: return state; } }; export const BLEDataProvider = ({ children }) => { const [state, dispatch] = useReducer(bleReducer, { bleData: null }); const { bleData } = state; const connect = useCallback(async () => { const device = await navigator.bluetooth.requestDevice({ filters, optionalServices, }); // [...] const handleDisconnected = () => { if (someCondition) { // 用dispatch触发重连,dispatch是稳定的,不会变 dispatch({ type: 'RECONNECT', payload: { connect } }); } }; device.addEventListener('gattserverdisconnected', handleDisconnected); return device; }, [dispatch, /* 其他依赖 */]); // 初始化连接(如果需要) useEffect(() => { connect(); }, [connect]); return ( <BLEDataContext.Provider value={{ bleData }}> {children} </BLEDataContext.Provider> ); }
这里dispatch是React保证稳定的引用,所以connect的依赖列表里只需要dispatch,而handleDisconnected内联在connect中,不需要额外的useCallback,彻底避免了循环依赖。
方法三:调整监听逻辑,避免重复绑定
如果之前尝试内联onDisconnected没成功,可能是因为每次connect都会创建新的监听函数,导致旧的监听还存在。可以在连接前移除旧的监听,同时把onDisconnected的逻辑内联,并且不依赖外部的connect:
const BLEDataContext = createContext<{ bleData: number | null; }>({ bleData: null }); export const BLEDataProvider = ({ children }) => { const [bleData, setBleData] = useState<number | null>(null); // 保存设备引用,方便移除监听 const deviceRef = useRef<BluetoothDevice | null>(null); const connect = useCallback(async () => { // 如果之前有设备,先移除旧的监听 if (deviceRef.current) { deviceRef.current.removeEventListener('gattserverdisconnected', handleDisconnected); } const device = await navigator.bluetooth.requestDevice({ filters, optionalServices, }); deviceRef.current = device; // 内联断开处理函数,直接调用connect async function handleDisconnected(event: any) { // [...] if (someCondition) { connect(); } } device.addEventListener('gattserverdisconnected', handleDisconnected); return device; }, [/* 这里只放除了onDisconnected之外的依赖 */]); return ( <BLEDataContext.Provider value={{ bleData }}> {children} </BLEDataContext.Provider> ); }
这种方式把handleDisconnected内联在connect里,虽然每次connect都会创建新的函数,但我们在连接前移除了旧的监听,避免内存泄漏,同时因为函数内联,不需要把connect加入外部依赖,也打破了循环。
内容的提问来源于stack exchange,提问作者Aurélien Gasser

