React离线优先:useEffect链式请求混乱重构方案
原有代码的核心问题
- 闭包与时序错误:IndexedDB的count、getAll都是异步操作,你在回调外直接判断
nbSuppliers的值,拿到的永远是初始状态,判断逻辑完全失效;写入IndexedDB时遍历的suppliers也是effect捕获的旧空值,根本写不进去API返回的数据 - 重复执行逻辑:把
nbSuppliers放在useEffect依赖里,每次状态更新都会重跑整个effect,导致重复打开数据库、重复发请求 - 逻辑耦合严重:数据库连接、建表、读写、API请求全堆在组件里,没有抽离,其他页面要复用相同逻辑只能复制粘贴代码
- 缺失边界处理:没有处理IndexedDB首次打开时的建表逻辑、事务错误、API请求错误,也没处理重复写入的主键冲突问题
分层可维护实现方案
按照「通用能力抽离-业务逻辑封装-页面渲染」三层拆分,后续多页面复用只需要直接调用封装好的方法即可,不需要重复写存储和请求逻辑。
第一层:通用IndexedDB工具封装
单独抽成src/utils/db.js文件,全局复用数据库连接和通用读写方法,避免重复打开数据库、重复写基础操作逻辑:
// 全局单例数据库实例,避免重复创建连接 let dbInstance = null; /** * 打开/初始化IndexedDB连接,全局仅执行一次 * @returns Promise<IDBDatabase> */ export const getDB = () => { if (dbInstance) return Promise.resolve(dbInstance); return new Promise((resolve, reject) => { const openRequest = indexedDB.open("salon", 1); // 首次打开/版本升级时初始化表结构 openRequest.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains("suppliers")) { // 主键根据你的实际数据字段调整,这里默认用id作为主键 db.createObjectStore("suppliers", { keyPath: "id" }); } }; openRequest.onsuccess = (event) => { dbInstance = event.target.result; dbInstance.onclose = () => { dbInstance = null; }; resolve(dbInstance); }; openRequest.onerror = (event) => { reject(event.target.error); }; }); }; /** * 读取指定表全量数据 * @param {string} storeName 表名 * @returns Promise<Array> */ export const getAllFromStore = async (storeName) => { const db = await getDB(); return new Promise((resolve, reject) => { const tx = db.transaction(storeName, "readonly"); const store = tx.objectStore(storeName); const req = store.getAll(); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); }; /** * 批量写入数据到指定表,用put替代add避免主键重复报错 * @param {string} storeName 表名 * @param {Array} dataList 待写入数据 * @returns Promise<void> */ export const bulkWriteToStore = async (storeName, dataList) => { const db = await getDB(); return new Promise((resolve, reject) => { const tx = db.transaction(storeName, "readwrite"); const store = tx.objectStore(storeName); dataList.forEach(item => store.put(item)); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); }; /** * 获取指定表数据总数 * @param {string} storeName 表名 * @returns Promise<number> */ export const getStoreCount = async (storeName) => { const db = await getDB(); return new Promise((resolve, reject) => { const tx = db.transaction(storeName, "readonly"); const store = tx.objectStore(storeName); const req = store.count(); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); };
第二层:供应商数据加载逻辑封装
抽成自定义Hooksrc/hooks/useSuppliers.js,把「优先读本地、无缓存走API、写入本地缓存」的业务逻辑完全封装,任何页面需要供应商数据直接调用这个Hook即可:
import { useState, useEffect } from 'react'; import axios from 'axios'; import { getAllFromStore, bulkWriteToStore, getStoreCount } from '../utils/db'; const SUPPLIERS_API = "/suppliers.php"; const STORE_NAME = "suppliers"; export default function useSuppliers() { const [suppliers, setSuppliers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const loadData = async () => { try { setLoading(true); // 优先读本地缓存 const localCount = await getStoreCount(STORE_NAME); if (localCount > 0) { const localData = await getAllFromStore(STORE_NAME); setSuppliers(localData); return; } // 本地无缓存才请求API const { data: apiData } = await axios.post(SUPPLIERS_API, "data"); // 先写本地缓存再更新状态 await bulkWriteToStore(STORE_NAME, apiData); setSuppliers(apiData); } catch (err) { setError(err); } finally { setLoading(false); } }; loadData(); // 空依赖数组,逻辑仅在组件挂载时执行一次,不会重复触发 }, []); return { suppliers, loading, error, count: suppliers.length }; }
第三层:页面组件实现
页面只负责拿数据渲染,完全不碰底层存储和请求逻辑,代码非常简洁:
import useSuppliers from '../hooks/useSuppliers'; const Fournisseurs = () => { const { suppliers, count, loading, error } = useSuppliers(); if (loading) return <div>加载中...</div>; if (error) return <div>加载失败,请刷新重试</div>; return ( <div className="suppliers-container"> <h2>供应商列表(共{count}家)</h2> <ul className="supplier-list"> {suppliers.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> </div> ); }; export default Fournisseurs;
方案优势
- 复用性强:IndexedDB通用方法、供应商加载逻辑都做了抽离,其他页面需要同类能力直接调用即可,不需要复制代码
- 逻辑可靠:所有异步操作用async/await按顺序执行,不存在旧闭包、时序错乱问题,加载逻辑仅执行一次,不会造成网络和性能浪费
- 易维护:三层逻辑完全解耦,后续要加缓存过期、错误重试、其他业务表的缓存逻辑,只需要修改对应层级的代码,不需要动页面渲染部分
- 鲁棒性更好:补全了数据库初始化、异常捕获、事务完成监听、主键冲突处理等边界逻辑,不会出现莫名其妙的IndexedDB报错
内容的提问来源于stack exchange,提问作者val
相关产品推荐
相关产品推荐

