You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 04:36:13