手把手带你实现一个简单的React Query

721 阅读5分钟

为什么会有React Query的出现

要从 API 获取数据并在 React 应用程序中显示响应,您需要创建一个处理 API 请求的组件。在组件内部,您可以使用 useState 挂钩来初始化状态变量以保存获取的数据。然后,在组件安装时使用 useEffect 挂钩来获取数据。在 useEffect 挂钩内,使用 fetch 等方法或 Axios 等库向 API 发出 HTTP 请求。收到响应后,使用获取的数据更新状态变量。最后,在组件的 JSX 中呈现响应,从状态访问相关数据。

这样实现简单易懂,但是代码太多而且效率不高,最主要的是你无法缓存 API 响应,所以如果你的服务器有点慢并且你不想继续发出相同查询的请求,那么在客户端缓存就会是您的一个选择。缓存 API 响应可以通过减少不必要的网络请求来提高应用程序的性能和效率。

什么是缓存

在计算中,缓存是一个临时存储位置,用于存储经常访问的数据或指令,以便加快将来对该数据的请求。缓存的目的是通过减少从原始源检索数据所需的时间和资源来提高系统性能。

缓存实现

比较高效的访问一个特定的数据最好的方法是创建一个以键和数据作为值的对象,或者使用映射数据结构来执行相同的操作。接下来实现缓存,该缓存应该可供所有组件使用,因此让我们将其保留在上下文中并覆盖主组件。

import { createContext, useContext, ReactNode } from "react";

type ContextType = {
  getCache: (key: string) => any;
  setCache: (key: string, value: any, ttl?: number) => void;
  clearCache: () => void;
  deleteCache: (key: string) => void;
};

type cacheBody = {
  expiry: Date;
  data: any;
};

const CacheContext = createContext<ContextType | null>(null);

export function useCache() {
  return useContext(CacheContext) as ContextType;
}

export default function CacheProvider({ children }: { children: ReactNode }) {
  const map = new Map<string, cacheBody>();

  function getCache(key: string) {
    const cacheValue = map.get(key);
    if (!cacheValue) return undefined;
    if (new Date().getTime() > cacheValue.expiry.getTime()) {
      map.delete(key);
      return undefined;
    }
    return cacheValue.data;
  }

  function setCache(key: string, value: any, ttl: number = 10) {
    var t = new Date();
    t.setSeconds(t.getSeconds() + ttl);
    map.set(key, {
      expiry: t,
      data: value
    });
  }

  function clearCache() {
    map.clear();
  }

  function deleteCache(key: string) {
    map.delete(key);
  }

  const contextValue = {
    getCache,
    setCache,
    clearCache,
    deleteCache
  };

  return (
    <CacheContext.Provider value={contextValue}>
      {children}
    </CacheContext.Provider>
  );
}

在应用中使用

现在上下文已创建,让我们用这个 CacheContext 包装我们的应用程序。我们可以看到缓存上下文为您提供了四种方法来设置、删除、获取和清空缓存,设置方法采用一个键字符串、一个类型为any的值和一个ttl(以秒为单位的生存时间),它确定该缓存何时应该被宣告无效。get 方法为您提供缓存(如果在到期时间内存在),否则为您提供未定义的信息。另外两个函数分别是删除缓存或清空缓存。

<CacheProvider>
    <App />
</CacheProvider>

实现useFetch

现在我们的缓存机制已经准备好了,但由于我们的目标是缓存 API 响应,但现在如果我们在发出请求时检查每个组件中是否存在缓存并不是一个好主意。相反,我们可以创建一个自定义hooks,它将为我们做所有开箱即用的事情,我们只需要提供 API URL、密钥和一些配置。

import { useEffect, useState } from "react";
import { useCache } from "../contexts/Cache";
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from "axios";

type CustomAxiosConfig = AxiosRequestConfig & {
  key: Array<unknown>;
  initialEnabled?: boolean;
  cache?: {
    enabled?: boolean;
    ttl?: number;
  };
  onSuccess?: (data: AxiosResponse) => void;
  onFailure?: (err: AxiosError) => void;
};

function keyify(key: CustomAxiosConfig["key"]) {
  return key.map((item) => JSON.stringify(item)).join("-");
}

export default function useFetch<T = any>({
  key,
  initialEnabled = true,
  cache,
  ...axiosConfig
}: CustomAxiosConfig) {
  const [loading, setLoading] = useState(false);
  const [data, setData] = useState<T | undefined>();
  const [error, setError] = useState<any>();
  const { getCache, setCache, deleteCache } = useCache();

  const refetch = (hard: boolean = false) => {
    setLoading(true);
    setError(undefined);
    const cacheKey = keyify(key);
    if (cache?.enabled && getCache(cacheKey) !== undefined && !hard) {
      setData(getCache(cacheKey));
      setLoading(false);
      setError(undefined);
      return;
    }
    axios(axiosConfig)
      .then((data) => {
        setData(data as T);
        if (cache?.enabled) setCache(cacheKey, data, cache.ttl);
      })
      .catch((err) => {
        setError(err);
      })
      .finally(() => {
        setLoading(false);
      });
  };

  function inValidate(invalidationKey: CustomAxiosConfig["key"]) {
    deleteCache(keyify(invalidationKey));
  }

  useEffect(() => {
    if (initialEnabled) refetch();
  }, []);

  return { loading, data, error, refetch, inValidate } as const;
}

这是一个名为 useFetch 的自定义hooks,可以在组件中调用它,它为您提供状态管理的所有好处。您不必维护组件中的加载、错误和数据等多种状态。这都是由hooks本身返回的。该挂钩还为您提供重新获取查询和使查询无效的方法。

如何使用useFetch

import useFetch from "./hooks/useFetch";

export default function App() {
  const { loading, error, data, refetch } = useFetch({
    url: "https://randomuser.me/api",
    method: "get",
    key: ["app", "get", "user", { name: "nisab" }],
    cache: {
      enabled: true,
      ttl: 10
    }
  });

  if (loading) {
    return <p>Loading...</p>;
  }
  if (error) {
    return <p>Something went wrong</p>;
  }
  return (
    <div className="App">
      {JSON.stringify(data, null, 4)}
      <br />
      <button onClick={() => refetch()}>get user</button>
    </div>
  );
}

小结

了解事物的工作原理而不是仅仅依赖库可以有几个优点:

  • 灵活性和定制:当您深入了解事物的工作原理时,您对代码就有更多的控制力和灵活性。您可以自定义和调整解决方案以满足特定要求,而不受库提供的功能的限制。
  • 故障排除和调试:当您在代码中遇到问题或错误时,深入了解底层机制可以帮助您更有效地进行故障排除和调试。您可以通过检查代码及其背后的逻辑来识别和解决问题。
  • 效率和性能:库通常会带来额外的开销,例如额外的依赖项、大小或处理时间。通过了解事物的工作原理,您可以优化代码以提高效率和性能,从而有可能避免不必要的依赖或简化流程。
  • 学习和成长:探索事物的基本运作方式可以让您扩展知识和技能。它增强您掌握新概念、解决复杂问题以及适应不断变化的技术和框架的能力。

然而,保持平衡很重要。库和框架可以提供便利、加快开发速度并更有效地处理复杂任务。它们通常由专家构建并经过严格的测试。利用库可以节省时间和精力,特别是对于常见或完善的功能。

最终,选择了解事物的工作原理还是依赖库取决于具体的环境、项目要求和个人喜好。两种方法的结合通常可以产生最佳结果,在适当的时候利用库并在必要时深入研究底层概念。