实现带有缓存优先策略的 React Hook

发布:2024-11-13 09:53 阅读:10 点赞:0

在这篇博客文章中,我们将逐步介绍如何实现一个 React Hook,该 Hook 采用缓存优先策略从后端获取数据,仅在必要时才进行网络请求。我们的目标是展示如何通过缓存改善用户体验(UX),减少延迟,使应用感觉更快。

问题:高延迟的 API 调用

假设你正在构建一个从 API 获取产品数据的应用。在现实场景中,这些数据可能来自具有延迟的远程服务器(例如,2-3 秒的延迟)。在等待数据时,用户可能会感到沮丧。那么,我们如何解决这个问题呢?

通过使用 IndexedDB 缓存数据,我们可以立即显示之前获取的数据,同时在后台获取新数据。这样可以给用户带来更快、更流畅的体验。

逐步解析 useGetProducts Hook

让我们逐步解析 Hook,该 Hook 处理缓存逻辑和 API 调用。useGetProducts

步骤 1:定义 Product 接口

首先,我们定义 接口来表示我们要处理的数据:Product

interface Product {
  id: number;
  name: string;
}

这是一个简单的对象结构,包含 和 ,代表一个产品。idname

步骤 2:模拟具有延迟的 API 调用

接下来,我们使用 函数模拟一个具有 2 秒延迟的 API 调用,以模拟真实的后端 API:getProducts

const getProducts = async (): Promise<Product[]> => {
  return await new Promise<Product[]>((resolve) =>
    setTimeout(
      () =>
        resolve([
          { id: 1, name: 'Product A' },
          { id: 2, name: 'Product B' },
        ]
),
      2000
    
)
  
);
};

这个函数模拟了一个延迟的 API 调用,返回一个产品数组,延迟时间为 2 秒。

步骤 3:实现 useGetProducts Hook

现在,我们来构建 Hook。核心思想是首先检查是否有可用的缓存数据,并立即使用它,然后在后台获取最新数据。useGetProducts

import React from 'react';
import { getCachedData, updateCache } from './cacheUtils';

const useGetProducts = (): {
  data: Product[] | undefined;
  loading: boolean;
} => {
  const [products, setProducts] = React.useState<Product[] | undefined>(undefined);
  const [loading, setLoading] = React.useState(true);
  const cacheKey = 'cache_products';

  // 加载产品数据,如果有缓存则立即显示,然后从 API 获取最新数据更新缓存
  const loadProducts = React.useCallback(async () => {
    setLoading(true);

    // 第一步:从缓存加载数据,如果有缓存则立即显示
    const cachedProducts = await getCachedData<Product[]>(cacheKey);
    if (cachedProducts && cachedProducts.length > 0) {
      setProducts(cachedProducts); // 立即显示缓存数据
      setLoading(false);
    }

    // 第二步:从 API 获取最新数据,即使已经使用了缓存
    try {
      const response = await getProducts();
      setProducts(response); // 更新为最新数据
      updateCache(response, cacheKey); // 更新缓存
    } catch (err) {
      console.error('Error fetching products:', err);
    } finally {
      setLoading(false);
    }
  }, []);

  React.useEffect(() => {
    loadProducts();
  }, [loadProducts]);

  return { data: products, loading };
};

解释:

  • 状态管理:我们使用 来管理两个状态:React.useState

    • products:产品数据数组。
    • loading:一个标志,表示数据是否仍在获取中。
  • 缓存查找:在 函数中,我们首先尝试使用 函数从缓存中获取产品数据。如果有缓存数据,它将立即显示,并将 设置为 。loadProductsgetCachedDataloadingfalse

  • API 调用:同时,我们使用 函数从 API 获取最新数据。一旦获取到新数据,我们更新状态并使用 函数将其缓存起来以供未来使用。getProductsupdateCache

  • 乐观 UI:这种方法有助于改善用户体验,因为它会立即显示缓存数据,而新数据在后台加载,减少了感知延迟。

步骤 4:缓存实用函数

缓存功能由两个实用函数 和 提供,它们与 IndexedDB 交互以存储和检索数据。getCachedDataupdateCache

import {
  deleteFromIndexedDB,
  getAllKeysFromIndexedDB,
  getFromIndexedDB,
  saveToIndexedDB,
from '../indexDb';

const encryptData = (data: string): string => btoa(encodeURIComponent(data));
const decryptData = (encryptedData: string): string =>
  decodeURIComponent(atob(encryptedData));

export const updateCache = async <T>(data: T, cacheKey: string) => {
  try {
    const [, cacheConst] = cacheKey.split('_');

    const allKeys = await getAllKeysFromIndexedDB();

    const existingKey = allKeys.find((key) => key.endsWith(`_${cacheConst}`));
    if (existingKey) {
      await deleteFromIndexedDB(existingKey);
    }

    const serializedData = JSON.stringify(data);
    const encryptedData = encryptData(serializedData);
    await saveToIndexedDB(cacheKey, encryptedData);
  } catch (error) {
    console.error('Failed to update cache:', error);
  }
};

export const getCachedData = async <T>(cacheKey: string): Promise<T | null> => {
  try {
    const cached = await getFromIndexedDB(cacheKey);
    if (cached) {
      const decryptedData = decryptData(cached);
      return JSON.parse(decryptedData) as T;
    }
    return null;
  } catch (error) {
    console.error('Failed to retrieve cached data:', error);
    return null;
  }
};

这些函数允许我们轻松地使用 IndexedDB 在浏览器中访问和更新缓存,提供持久的数据存储。

步骤 5:最终使用

以下是如何在组件中使用 Hook 来显示产品的示例:useGetProducts

import React from 'react';
import useGetProducts from './useGetProducts';

const ProductList: React.FC = () => {
  const { data: products, loading } = useGetProducts();

  if (loading && !products) return <div>Loading products...</div>;
  if (!products || products.length === 0) return <div>No products found.</
div>;

  return (
    <div>
      <h3>Available Products:</h3>
      <ul>
        {products.map(product => (
          <li key={product.id}>{product.name}</
li>
        ))}
      </ul>
    </
div>
  );
};

export default ProductList;

结论

通过这种方式在 IndexedDB 中实现缓存,我们可以显著改善依赖于高延迟外部 API 的应用的用户体验。用户会立即看到缓存数据,而新数据会在后台更新,确保用户总是获得最新的信息而不必等待。

这种方法减少了应用的感知延迟,使其感觉更快、更响应,即使在后端可能较慢的情况下也是如此。