自定义hook内使用useEffect导致React Native应用多处调用时冻结
基于AsyncStorage封装的自定义Hook触发应用冻结问题修复
问题复现
开发了一个基于AsyncStorage封装的配置读取自定义hook,原始实现代码如下:
// Takes the key/name of the setting to retrieve export const useSettings = (key) => { // stores the json string result in the setting variable const [setting, setSetting] = useState(""); const deviceStorage = useContext(DeviceStorageContext); useEffect(() => { getValue() .then(value => setSetting(value)); }, []); // gets value from my custom api that uses Async-Storage to handle r/w of the data. const getValue = async () => await deviceStorage.getValueStored(key); const setValue = async (value) => { await deviceStorage.setValueStored(key, value); getValue().then(value => setSetting(value)); }; const removeValue = async () => { } return [setting, { setValue,removeValue }]; };
该hook在Main.jsx中调用符合预期,对应代码如下:
const Main = () => { const [units, operations] = useSettings('units'); useEffect(() => { const initSettings = async () => { if (units) { console.log(units) return; } await operations.setValue({ pcs: 1, box: 20 }); }; initSettings(); }, []);
在Form.jsx中仅调用该hook就会导致应用完全冻结,Form.jsx的调用代码如下:
const FormView = ({ handleReset, handleSubmit }) => { const [setting,] = useSettings('units');
移除hook内的useState和useEffect后问题消失,但不希望在业务代码中随处编写async/await处理异步存储逻辑。
根因分析
- 内部异步方法未做引用稳定处理,每次组件渲染都会生成新的函数引用,结合未补全的useEffect依赖项,容易触发不必要的重复执行
- 返回的操作对象每次渲染生成新的引用,若被其他依赖项监听会触发连锁重渲染,极端情况下会进入无限重渲染循环导致应用冻结
- 未做组件卸载判断,异步回调执行已卸载组件的setState会触发额外的性能损耗,严重时会引发运行异常
- 未做异常捕获,存储操作抛出的未处理异常会导致应用运行阻塞
修复方案
优化后完整Hook代码
import { useState, useEffect, useCallback, useMemo, useContext } from 'react'; import { DeviceStorageContext } from './你的context文件路径'; export const useSettings = (key) => { // 初始值设为null,区分「未加载」和「空值」状态 const [setting, setSetting] = useState(null); const deviceStorage = useContext(DeviceStorageContext); // 用useCallback固定getValue引用,仅在依赖变化时重建 const getValue = useCallback(async () => { try { const value = await deviceStorage.getValueStored(key); // 适配AsyncStorage的字符串存储特性,自动反序列化对象类型值 return typeof value === 'string' ? JSON.parse(value) : value; } catch (e) { console.error(`获取配置${key}失败:`, e); return null; } }, [key, deviceStorage]); // 用useCallback固定setValue引用 const setValue = useCallback(async (value) => { try { // 自动序列化对象类型值适配存储 const storedValue = typeof value === 'object' ? JSON.stringify(value) : value; await deviceStorage.setValueStored(key, storedValue); const latestValue = await getValue(); setSetting(latestValue); } catch (e) { console.error(`保存配置${key}失败:`, e); } }, [key, deviceStorage, getValue]); // 用useCallback固定removeValue引用 const removeValue = useCallback(async () => { try { await deviceStorage.removeValueStored(key); setSetting(null); } catch (e) { console.error(`删除配置${key}失败:`, e); } }, [key, deviceStorage]); useEffect(() => { let isMounted = true; getValue().then(value => { // 组件卸载后跳过setState,避免内存泄漏警告 if (isMounted) setSetting(value); }); // 注册清理函数 return () => { isMounted = false; }; // 补全useEffect依赖项 }, [getValue]); // 用useMemo固定返回的操作对象引用,避免触发额外重渲染 const operations = useMemo(() => ({ setValue, removeValue }), [setValue, removeValue]); return [setting, operations]; };
额外检查点
- 确认DeviceStorageContext的实现不会在存储更新时无差别刷新所有消费者,可通过拆分context、memo优化context value的方式避免全局重渲染
- 确认deviceStorage的所有方法都正确返回Promise,不存在永远pending的异常情况
- 确认FormView组件在DeviceStorageContext的作用域内,可正常获取到deviceStorage实例
内容的提问来源于stack exchange,提问作者foreign-native
相关产品推荐
相关产品推荐

