skip the docs of react query

239 阅读10分钟

这篇是这周要作为组内分享的文档,复制过来时候由于格式问题可能有些地方没改完,如果有什么不足的地方或者有错误的地方可以指出一下,立马修改。

背景

早在 2022 年底,umi 就在最佳实践里把 react query 作为了请求方案的最佳实践。

说来惭愧,时隔两年我才迈上 react query 的学习之路(其实中间我看过几次,都被它的文档劝退了)。

写这篇的时候遇到了很多问题,最大的问题其实还是自我怀疑有没有必要写这一篇,2024年了还能有人看不懂英文文档吗,真的还需要专门写一篇文档来介绍吗?直到有一天,我听到组内的同学把标题的英文 title /ˈtaɪt(ə)l/ 念成了 /ˈtiːt(ə)l/,我觉得还是有意义的(开玩笑)。

React Query是什么

简介

react query 是由一个来自犹他的开发者用他的空余时间开发的一个库,每周约有 330w 的下载量,平均每 6 个 react 的应用程序,就有一个使用了 react query

但它不是银弹,所有的流行库都会有自己的优势,不然就不会 6 个应用程序才有 1 个使用了,大家还是可以根据自己的喜好来选择使用的库。

you-might-not-need-react-query(你可能并不需要react query)

配置

怎么在umi中开启配置

剩下的就不用看 umi 里的文档了,那儿也没打算怎么教你好好用。

React Query 使用

react query 几乎提供了所有场景下的解决方案。

缓存管理、缓存失效、自动重新请求、离线支持、列表滚动恢复、页面获取焦点重新发起请求、依赖查询、分页查询、请求取消、预请求、轮询、无限滚动、状态变更、数据选择器 等等。

基本配置

QueryClientProviderreact-query 提供的 Provider,包裹在应用的最外层,保证我们整个应用能获取到缓存。

QueryClient 包含了并所有缓存的数据,它是一个静态对象(可以看成一个 Map),永远不会改变,所以虽然 react query 依赖 context 实现,但是缓存的改变不会直接触发 re-render

import { 
  QueryClient, 
  QueryClientProvider 
} from '@tanstack/react-query'

const queryClient = new QueryClient()

export default function App () {
  return (
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  )
}

useQuery基础用法

useQueryreact query 最核心的 api ,用来获取请求。

基本用法

import { useQuery } from '@tanstack/react-query'

const { data } = useQuery({
  queryKey: ['luckyNumber'],
  queryFn: () => Promise.resolve(7),
})

这是最基本的用法,我们给 useQuery 提供一个 queryKeyqueryFn ,从返回的结果的中解构出 data 在页面中使用,data 会有一个 undefined -> 7 的转变。

如果我们要根据不同的参数来重新发起请求,可以把参数加到 queryKey,当 queryKey 发生改变时,react query 会重新调用 queryFn

const { data } = useQuery({
  queryKey: ['list', { page: page, pageSize: 10 }],
  queryFn: () => getData(page, 10),
})

注意: queryKey 必须是可以序列化的,因为 react query 是把 key 序列化之后比较 hash

常用参数和返回内容

基础流程

image.png

缓存机制

useQuery 是否重新请求取决于 useQuery 的一个配置项 -- staleTime 单位为毫秒,代表的意思为,在一次数据请求完成后,在 staleTime 设置的这个时间段里,不会再重新发起请求,而是直接使用缓存中存在的数据,默认为 0。

react-query 并不会主动重新去发起请求更新数据,但是下面几种情况下,queryFn会重新执行。

  1. queryKey 变更
  2. 新组件挂载,并且订阅了这个 queryKey,对应的配置项,refetchOnMount 默认 true
  3. 窗口重新聚焦 ,对应的配置项,refetchOnWindowFocus,默认为 true
  4. 断网重连,对应的配置项,refetchOnReconnect,默认为 true

控制按需获取

enabled可以用来控制 useQuery 是否可用。

如果在 enabledfalse 的时候,key 在缓存中已经有了数据,会把已有的数据返回,不重新触发 queryFn

useMutation

前面的案例和用法基本都是获取数据的用法,但是我们实际开发场景中,也有很多是通过请求,来变更服务端数据的,比如修改表单。

这样的请求和之前的 useQuery 有着明显的区别 -- 不需要缓存。

react query 提供了 useMutation 来处理这个场景。

声明

mutationFn 就是我们实际的请求函数。

onMutate onError onSuccess onSettled 分别代表 运行时 运行失败 运行成功 运行结束

variables 代表传递进来的参数。

onMutate 返回的内容,会作为 context 传递给后面的生命周期函数。

export const useAddTodo = () => {
  return useMutation({
    mutationFn: addTodo,
    onMutate: (variables) => {
      // A mutation is about to happen!

      // Optionally return a context containing data to use when for example rolling back
      return { id: 1 }
    },
    onError: (error, variables, context) => {
      // An error happened!
      console.log(`rolling back optimistic update with id ${context.id}`)
    },
    onSuccess: (data, variables, context) => {
      // Boom baby!
    },
    onSettled: (data, error, variables, context) => {
      // Error or success... doesn't matter!
    },
  })
}

使用

mutate 其实就是我们声明的 mutationFnmutate 中的生命周期函数,会运行在声明的周期函数之后。

const {
  mutate,
  isPending,
  isError,
  isSuccess,
  data,
  error,
  isIdle
} = useAddTodo();

mutate(todo, {
  onSuccess: (data, variables, context) => {
    // I will fire second!
  },
  onError: (error, variables, context) => {
    // I will fire second!
  },
  onSettled: (data, error, variables, context) => {
    // I will fire second!
  },
})

queryClient 使用

初始配置

react query 提供了设置初始配置的方法,就像 umi 文档里说到的,推荐关闭 refetchOnWindowFocus 或者统一设置一个 staleTime

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
      staleTime: 10 * 1000
      // ...
    },
  },
})

项目中使用

因为 queryClient 中存储了所有的缓存信息,所以我们可以轻松地通过它对缓存进行操作。这边简单介绍几个常用的方法。

获取缓存
const queryClient = useQueryClient();

const data = queryClient.getQueryData>(['todos']);
修改缓存
const queryClient = useQueryClient();

queryClient.setQueryData(['todos'], data);
失效缓存

通过传递给 queryClient.invalidateQueries() 一个 queryKey ,它会把匹配到的 query 全部失效,并重新发起请求。

还有个参数为 refetchType ,默认为 active ,表示在活跃状态下(有订阅)的 query 会重新请求。

还有其他几个选项 ,inactive all none,具体含义看名字就了解了。

const queryClient = useQueryClient();

queryClient.invalidateQueries({ queryKey: ['todos'] });
queryKey匹配规则

由于我们 queryKey 是按照数组的形式传入的,匹配 queryKey 的时候,也是按层级来的。

比如我们调用 queryClient.invalidateQueries 失效缓存。

如果传入的 queryKey['lol'],那么 ['lol', 1] ['lol', 'list', '1'] 这些都会失效。

如果需要只匹配到 ['lol'] 可以加一个 exact: true 表达精确匹配。

const queryClient = useQueryClient();

queryClient.invalidateQueries({ queryKey: ['todos'], exact: true });

useQuery 的部分使用场景

轮询

refetchInterval 可以用来控制轮询的间隔,可以返回一个数值代表,时间间隔,也可以返回函数,参数是存储的 query 对象,返回值为 false 代表终止轮询,返回值为数字代表下一次轮询的间隔。

useQuery 中返回 dataUpdatedAt 可以用来表示,数据更新的时间戳。

const { data, dataUpdatedAt } = useQuery({
    queryKey: ['lol'],
    queryFn: () => getData(),
    refetchInterval: (query) => {
      console.log('query', query);
      if (query.state.data) {
        return false;
      }
      return 3000;
    },
});

预查询

实际项目中,我们很少会用预查询,因为查到了也没有地方存储数据,专门建个 context 或者 map 使用起来也太过麻烦,而且我们的交互要求也没这么高。

很简单的一个例子,在用户在从列表页点击进详情页时,有时候我们的选择框会先出现 id ,等 options 请求完成后,才会显示 label,我们完全可以在用户浏览列表页,或者完成列表页查询后的时间,完成这些数据的预加载。

import{ useQueryClient } from '@tanstack/react-query'

const Component = () => {
  const queryClient = useQueryClient()

  const handlePrefetch = () => {
    queryClient.prefetchQuery({
      queryKey: ['lol'],
      queryFn: () => fetchLol(),
      staleTime: 5000
    })
  }
}

并发查询

可以使用 useQueries 将多个请求放到一个数组里,返回的是一个带有 query 的数组(每个都和 useQuery 返回的对象一致),每个 query 的配置,和使用单个 useQuery 是一致的。
而且可以使用 combine 属性,来进行自定义的返回。

const { isPending, isError, t1, ig } = useQueries({
  queries: [
    {
      queryKey: ['t1', id],
      queryFn: () => getData(id, { loading: true }),
      refetchInterval: 30000,
    },
    {
      queryKey: ['ig', id],
      queryFn: () => getData(id, { loading: true, change: true }),
    },
  ],
  combine: (queries) => {
    const isPending = queries.some((query) => query.status === "pending");
    const isError = queries.some((query) => query.status === "error");
    const [t1, ig] = queries.map((query) => query.data);

    return {
      isPending,
      isError,
      t1,
      ig
    };
  }
});

依赖查询

很多时候,我们一个请求需要的 params 会来自另一个请求的 response

举个例子,比如我们的 dima 一个需求关联了另一个需求,这个 dima 本身有一个 id,在通过这个 id 查询到详情后,我们从 response 里拿到 relationId ,去查询关联需求的详情。

你可能会这样写:

const useDimaDetail = (id) => {
  return useQuery({
    queryKey: ['dima', id],
    queryFn: async () => {
      const dimaDetail = await getDima(id);
      const relationId = dimaDetail.data.id;
      const relationDimaDetail = await getRelationDima(relationId);

      return {
        dimaDetail,
        relationDimaDetail
      }
    }
  })
}

这样写会有几个问题,

  1. 两个请求共用 error loading pending 这些信息不容易区分状态,并且一个请求挂了另一个也挂了
  2. 需要两个请求都完成后才会返回结果,消耗了不必要的时间
  3. 单个请求不能够使用缓存,两个请求会一起刷新
  4. 共用了配置,比如 staleTime gctime ,可能在后续维护中会出现问题。

更好的写法

const useDimaDetail = (id) => {
  return useQuery({
    queryKey: ['dima', id],
    queryFn: () => getDima(id)
  })
}

const useRelationDimaDetail = (id) => {
  return useQuery({
    queryKey: ['relationDima', id],
    queryFn: () => getRelationDima(id),
    enabled: id !== undefined
  })
}

const useDetail = (id) => {
  const dimaDetail = useDimaDetail(id);
  const relationId = dimaDetail.data.id;
  const relationDimaDetail = useRelationDimaDetail(relationId);

  return {
    dimaDetail,
    relationDimaDetail
  }
}

todoList示例

掘金的代码块功能太难用了,只能手贴代码了。

先加一个模拟数据获取更新的 data.js

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const list = [
  {
    id: 1,
    label: 'react',
    done: false,
  },
  {
    id: 2,
    label: 'vue',
    done: false,
  },
  {
    id: 3,
    label: 'angular',
    done: false,
  },
  {
    id: 4,
    label: 'solid',
    done: true,
  },
];

export const getData = () => {
  const data = JSON.parse(JSON.stringify(list));

  return Promise.resolve(list);
};

export const setData = async (id) => {
  await sleep(1000);

  const num = Math.random();

  if (num < 0.5) {
    return Promise.reject(false);
  }

  const item = list.find((item) => item.id === id);
  item!.done = !item!.done;

  return Promise.resolve(true);
};

简单的todoList

添加了获取todo列表和修改todo状态

import React from 'react';
import { Checkbox, message, Spin } from 'antd';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

import { getData, setData } from './data';

export const TodoList = () => {
  const queryClient = useQueryClient();

  const { data } = useQuery({
    queryKey: ['todo'],
    queryFn: () => getData(),
    placeholderData: [],
  });

  const { mutate, isPending } = useMutation({
    mutationFn: (id) => setData(id),
    onError: (err, vars, context) => {
      console.log('err', err, vars, context);
      message.error('操作失败');
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'], exact: true });
    },
  });

  const onChange = (id) => {
    mutate(id);
  };

  return (
    <div>
      <h2>todoList: </h2>
      <Spin spinning={isPending}>
        {data.map((item) => {
          return (
            <Checkbox checked={item.done} key={item.id} onChange={() => onChange(item.id)}>
              {item.label}
            </Checkbox>
          );
        })}
      </Spin>
    </div>
  );
};

优化后的todoList

因为每次修改状态都需要发起请求再修改状态,状态更新的反应比较慢,可以稍微优化下体验,点击时候先把todo显示勾上,再去发起请求,然后同步状态。

import React from 'react';
import { Checkbox, message, Spin } from 'antd';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

import { getData, setData } from './data';

export const TodoList = () => {
  const queryClient = useQueryClient();

  const { data } = useQuery({
    queryKey: ['todo'],
    queryFn: () => getData(),
    placeholderData: [],
  });

  const { mutate, isPending } = useMutation({
    mutationFn: (id) => setData(id),
    onMutate: async (id) => {
      // 取消正在进行中的查询
      await queryClient.cancelQueries({ queryKey: ['todo'], exact: true });
      queryClient.setQueryData(['todo'], (list) => {
        return list.map((item) => {
          if (item.id === id) {
            item.done = !item.done;
          }

          return {
            ...item,
          };
        });
      });
    },
    onError: (err, vars, context) => {
      console.log('err', err, vars, context);
      message.error('操作失败');
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['todo'], exact: true });
    },
  });

  const onChange = (id) => {
    mutate(id);
  };

  return (
    <div>
      <h2>todoList: </h2>
      <Spin spinning={isPending}>
        {data.map((item) => {
          return (
            <Checkbox checked={item.done} key={item.id} onChange={() => onChange(item.id)}>
              {item.label}
            </Checkbox>
          );
        })}
      </Spin>
    </div>
  );
};

其他技巧

自定义默认配置

虽然设置了默认值,如果在 useQuery 里写了对应的 options ,还是按 useQuery 里的为准。

统一配置

react query 提供了设置初始配置的方法,就像 umi 文档里说到的,推荐关闭 refetchOnWindowFocus 或者统一设置一个 staleTime

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
      staleTime: 10 * 1000
      // ...
    },
  },
})

可以用这个来统一配置 queryFn

const client = new QueryClient({
  defaultOptions: {
    queries: {
      queryFn: async ({ queryKey }) => {
        const slug = queryKey.join("/");
        const url = `${BASE_URL}/${slug}`;
        const response = await fetch(url);

        if (!response.ok) {
          throw new Error("Unable to fetch data");
        }

        const data = await response.json();
        return data;
      },
    },
  },
});

手动修改

我们还可以通过 queryClient.setQueryDefaults 来修改我们的默认值,第一个参数也是 queryKey, 匹配的逻辑和之前的一样 ,第二个参数是 options

// 影响 ['todo'] 、 ['todo', id] ...
queryClient.setQueryDefaults(
  ['todos'],
  { staleTime: 10 * 1000 }
)

管理 queryKey

有时,你会在多个组件内使用同一套 queryOptions,或者你觉得每次都写字面量的 queryOptions 不规范难以维护,你可以在其他地方定义一套 options ,然后导入使用,为了确保你的参数命名正确,react-query 也提供了 api 来帮助你实现。

import { queryOptions } from '@tanstack/react-query'

const lolQueryOptions = queryOptions({
  queryKey: ['lol'],
  queryFn: fetchLol,
  staleTime: 5000
})

甚至你可以这样,把一个模块下的请求放到一起,类似一个 controller

const todoQueries = {
  all: () => ['todos'],
  lists: () => [...todoQueries.all(), 'list'],
  list: (filters: string) =>
    queryOptions({
      queryKey: [...todoQueries.lists(), filters],
      queryFn: () => fetchTodos(filters),
    }),
  details: () => [...todoQueries.all(), 'detail'],
  detail: (id: number) =>
    queryOptions({
      queryKey: [...todoQueries.details(), id],
      queryFn: () => fetchTodo(id),
      staleTime: 5000,
    }),
}

性能优化

减少重复渲染

react query 会把返回的数据结果和之前储存的数据进行比较,如果是一致的,就不会触发重新渲染。

react query 还会订阅你使用的内容,只有你使用的内容变更,才会触发重新渲染,如下面的 2 个例子,只有 dataisStale 变更了,才会触发组件的重新渲染

const { data, isStale } = useQuey({
  ...
})
const result = useQuery({
  ...
})

console.log(result.data);
console.log(result.isStale);

使用 select 减少重复渲染

有些数据会返回 traceId 或者时间戳,导致数据不一样,可以用 select 函数进行筛选。

const { data } = useQuery({
  queryKey: [],
  queryFn: () => {},
  select: (data) => ({ name: data.name })
})

如果返回数据的 name 没有变化,就不会触发重新渲染。

短时间内重复触发

使用 debounce 解决

我们可以自己使用 debounce 函数来控制

取消之前的请求

react query 会提供 signal 用来取消请求,只要当作参数传给我们的请求方法就行。

比如 axios 就可以使用 signal 取消请求,详情可以看文档。

const query = useQuery({
  queryKey: ['todos'],
  queryFn: ({ signal }) =>
    axios.get('/todos', {
      // Pass the signal to `axios`
      signal,
    }),
})

错误处理

retry && retryDelay

react query 提供了 retryretryDelay,作为在请求失败时,自动重新请求的参数,retry 默认为 3 次,同样可以在 defaultOptions 中配置成 falseretryDelay 是每次间隔的时间。

两个参数都可以传数字或者函数,failureCount 表示失败次数。

retry: (failureCount, error) => {},
retryDelay: (failureCount, error) => {},

同样在 useQuery 的返回内容中,也有 failureCount

onError

在最开始创建 queryClient 时,我们也可以创建一个全局的 onError 事件,用来捕获我们的失败的请求。可以用来弹 message 或者其他事情。

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // ...
    }
  },
  queryCache: new QueryCache({
    onError: (error, query) => {
      // 在 useQuery 时候,可以传 meta 参数记录信息
      if (!query.meta.notShow) {
        message.error(error.message);
      }
    }
  })
})

配合 ErrorBoundary

首先 ErrorBoundary 是一个处理 react 页面渲染问题的组件,当你的 react 页面发生错误时,会显示最近的 ErrorBoundary 中的内容,并提供方法,让你可以重新渲染页面。

<ErrorBoundary
  onError={(err) => console.log("err", err)}
  fallbackRender={({ resetErrorBoundary }) => (
    <div>
      There was an error!
      <Button onClick={() => resetErrorBoundary()}>Try again</Button>
    </div>
  )}
>
  <div>
    <Content />
  </div>
</ErrorBoundary>

但是 ErrorBoundary 不支持捕获事件中的报错和异步的报错。

通过配置 useQuery 中的 throwOnError 可以把请求报错抛出,展现出 ErrorBoundary 中的内容。

同时,react query 也提供了 api 来更好地结合 ErrorBoundary 的使用。可以使用 reset 将最近的 ErrorBoundary 下出错的请求重新发起。

import { useQueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'

const App = () => {
  const { reset } = useQueryErrorResetBoundary()
  return (
    <ErrorBoundary
      onReset={reset}
      fallbackRender={({ resetErrorBoundary }) => (
        <div>
          There was an error!
          <Button onClick={() => resetErrorBoundary()}>Try again</Button>
        </div>
      )}
    >
      <Page />
    </ErrorBoundary>
  )
}

验证请求数据

可以配合 zod 验证返回的请求的数据格式

插件和适配器

持久化缓存

可以配合 PersistQueryClientProvider 将请求保存到 storage ,重新打开网页时恢复缓存内容。一般用到的比较少,想要了解的同学可以查看官方文档或者私聊我。

useQuery 丐版实现

简单的请求获取

带上 error loading 竞态请求 状态的封装。

const useQuery = () => {
  const [data, setData] = useState();

  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    let ignore = false;

    const handleQuery = async () => {
      setLoading(true);
      setError(null);
      setDetail();
      try {
        const data = await getData()
        if (ignore) {
          return;
        }
        setData(data);
        setLoading(false);
      } catch(e) {
        if (ignore) {
          return;
        }
        setError(err);
        setLoading(false);
      }
    };

    handleQuery();

    return () => {
      ignore = true;
    };
  }, [id]);

  return {
    detail,
    loading,
    error,
  };
};

带上发布订阅

也是上面说的问题,掘金的代码块功能太鸡肋了,就简单描述下实现过程。

通过 subscribe 类,配合 reactuseSyncExternalStore,实现发布订阅的流程,在数据更新时候通知到订阅的组件,实现 rerender

export class Subscribable {
  constructor(data?) {
    this.data = data;
    this.listeners = new Set();
    this.subscribe = this.subscribe.bind(this);
  }

  subscribe(listener: any): () => void {
    this.listeners.add(listener);

    this.onSubscribe();

    return () => {
      this.listeners.delete(listener);
      this.onUnsubscribe();
    };
  }

  changeData(data) {
    this.data = data;
    this.dispatchEvent();
  }

  hasListeners(): boolean {
    return this.listeners.size > 0;
  }

  dispatchEvent() {
    this.listeners.forEach((listen) => {
      listen(this.data);
    });
  }

  onSubscribe(): void {
    // Do nothing
  }

  onUnsubscribe(): void {
    // Do nothing
  }
}

export const useQuery = (id) => {
  const map = useContext(MyContext);

  if (!map[id]) {
    map[id] = new Subscribable({
      data: [],
      loading: false,
    });
  }

  const data = useSyncExternalStore(
    map[id].subscribe,
    useCallback(() => map[id].data, []),
  );

  const handleQuery = () => {
    map[id].changeData({
      data: [],
      loading: true,
    });
    getData(id).then((res: any) => {
      map[id].changeData({
        data: res,
        loading: false,
      });
    });
  };

  useEffect(() => {
    handleQuery();
  }, []);

  return data;
};

带上属性监听

原理其实和上文一致,只需要加一步,监听 datagetter,得到每个组件使用了哪些 key,在 dispatch 时候对比一下使用的 key 的数据有没有变更,如果没有变更,就不重新 update data

dispatchEvent(oldData) {
    this.listeners.forEach((listen) => {
      const uuid = Object.keys(this.listenerKeys).find((k) => this.listenerKeys[k] === listen);

      const keys = this.subKeys[uuid];

      let change = false;

      for (let k of keys) {
        const oD = JSON.stringify(oldData[k]);
        const nD = JSON.stringify(this.data[k]);

        if (oD !== nD) {
          change = true;
          break;
        }
      }

      if (change) {
        listen(this.data);
      }
    });
  }

总结

首先,这篇分享的标题是 skip the docs react query,意思是帮助我们跳过阅读文档就能够使用 react query

肯定是不可能的,但是应该可以帮助我们了解大多数的概念和使用方法,在使用的时候遇到有疑问的地方,再去看官方文档,会变得更加简单,至少知道该从哪个地方入手去看文档。

这篇分享我大概准备了一个多月,从系统地学习 react query 到整理文档.

整个过程下来给我的感受就是,不用它其实并不会有太大的影响,反而用它的时候,你可能会因为了解得不透彻,被它的一些内置的操作感到困惑。

举个例子,在断网情况下,它不会发请求也不会报错,而是会把请求的状态置为 paused,你搁那疯狂操作发现什么表现都没有,请求也不发,一看 wifi 才发现断网了,需要配置下 networkMode ,才能和我们正常的请求一致。

但是如果你想在请求上做一些 ui 的展示优化,或者性能的优化, react query 能很方便的帮你做到这些,比如一些跨组件的数据重新获取。

最后,如果在使用上遇到什么问题都可以和我来讨论。

参考文献

  1. 为什么你不应该在 React 中直接使用 useEffect 从 API 获取数据
  2. 官方文档
  3. The Official React Query Course
  4. umi文档
  5. you-might-not-need-react-query(你可能并不需要react query)