cache.ts 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. import { useCallback, useEffect, useRef, useState } from 'react';
  2. import { getApiCache } from './storage';
  3. interface UseSWCOptions<T> {
  4. onError?: (e: unknown) => void;
  5. onLoad?: (data: T) => void;
  6. cacheOnly?: boolean;
  7. cacheTimeout?: number;
  8. autoStart?: boolean;
  9. }
  10. const DEFAULT_CACHE_TTL = 86400 * 120;
  11. // 该 hook 在组件中按 key 和 action 拉取数据,自动处理加载/错误/缓存(stale-while-revalidate)
  12. export function useSWC<T>(key: string, action: () => Promise<T>, options?: UseSWCOptions<T>) {
  13. const optionsRef = useRef(options || {});
  14. optionsRef.current = options || {};
  15. const keyRef = useRef(key);
  16. const [data, setData] = useState<T | null | undefined>(() =>
  17. getApiCache().getObject<T>(keyRef.current)
  18. );
  19. const [loading, setLoading] = useState<true | 'background' | false>(true);
  20. const [error, setError] = useState<unknown>(null);
  21. const actionRef = useRef(action);
  22. actionRef.current = action;
  23. const dataRef = useRef(data);
  24. dataRef.current = data;
  25. const isMountedRef = useRef(true);
  26. const load = useCallback(async () => {
  27. setLoading(dataRef.current ? 'background' : true);
  28. setError(null);
  29. const opts = optionsRef.current;
  30. const cacheKey = keyRef.current;
  31. try {
  32. const result = await actionRef.current();
  33. if (result) {
  34. getApiCache().setObject(cacheKey, result, opts.cacheTimeout || DEFAULT_CACHE_TTL);
  35. }
  36. if (isMountedRef.current) {
  37. opts.onLoad?.(result);
  38. setData(result);
  39. setLoading(false);
  40. }
  41. } catch (err) {
  42. if (isMountedRef.current) {
  43. opts.onError?.(err);
  44. setError(err);
  45. setLoading(false);
  46. }
  47. }
  48. }, []);
  49. useEffect(() => {
  50. isMountedRef.current = true;
  51. if (dataRef.current && optionsRef.current.cacheOnly) {
  52. setLoading(false);
  53. setError(null);
  54. } else if (optionsRef.current.autoStart !== false) {
  55. load();
  56. }
  57. return () => {
  58. isMountedRef.current = false;
  59. };
  60. }, [load]);
  61. const getOrLoad = useCallback(async () => {
  62. if (dataRef.current && optionsRef.current.cacheOnly) {
  63. setLoading(false);
  64. setError(null);
  65. return;
  66. }
  67. await load();
  68. }, [load]);
  69. return { data, loading, error, load: getOrLoad, refresh: load };
  70. }