React-Query

1,267 阅读11分钟

React Query 是一个开箱即用,零配置的服务端状态管理库,支持Restful和GraphQL两种类型的请求,它能帮助你很好的获取、同步、管理和缓存你的远程数据。它提供了几个简单的Hooks,借助它们可以很轻松的完成对后端数据的增删改查等操作,无需再写繁琐的数据拉取和状态判断等代码;简单来说,`React-query`是一个异步状态管理的hook,内置了loading,error等状态;

官方文档tanstack.com/query/lates…

Reqct-query的作用

  • 封装了loading,error等方便使用,减少代码量
  • 多个组件请求同一个query时只发送一个请求;如下所示,List和List1都使用了getListData这个query,但是最终只会请求一次
function List() {
  const { isLoading, error, data } = useQuery('getListData', () => fetchList().then(res => res.data));
  if (error) {
    return 'An error has occurred: ' + error.message
  }
  if (isLoading) {
    return 'data is loading.'
  }
  return data.map(l => <div key={l.id}>
    {l.name}
  </div>);
}
function List1() {
  const { isLoading, error, data } = useQuery('getListData', () => fetchList().then(res => res.data));
  if (error) {
    return 'An error has occurred: ' + error.message
  }
  if (isLoading) {
    return 'data is loading.'
  }
  return data.map(l => <div key={l.id}>
    {l.name}
  </div>);
}
function App() {
  const queryClient = new QueryClient();
  return <div className='app'>
    <QueryClientProvider client={queryClient}>
      <List />
      <List1/>
      <ReactQueryDevtools initialIsOpen={false} position='top-right'/>
    </QueryClientProvider>
  </div>
  • 缓存更新和时效策略

React-query的一些主要特性:

  • 数据获取和缓存: React-query可以帮助你从远程服务器或本地存储获取数据,并在本地缓存这些数据。他会自动处理数据的获取、缓存和更新等操作。
  • 自动数据刷新: React-query可以根据设定的条件自动刷新数据,例如定时刷新或者当页面重新聚焦时刷新等
  • 无需手动管理状态:React-query会自动管理所有一步操作的状态,并提供一系列钩子函数来让您轻松地与数据交互
  • 查询时效和重新获取:当数据过期或失效时, React-query会自动触发重新获取机制,确保得到最新的数据
  • 错误处理: React-query提供了方便的错误处理机制,可以很容易地处理请求失败或其他错误情况
  • 多种查询功能:除了基本的查询外,React-query还支持带有参数,依赖关系和无限滚动等高级查询功能。

React-query和redux的区别

React-queryredux都能进行状态管理和异步数据的处理,React-query更偏向于服务器之间的异步数据的管理

简单使用

QueryClientProvider

首先,需要在组件外层定义一个queryClient作为组件操作和使用数据的一个共同容器,通过QueryClientProvider组件注入到项目中。

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

const queryClient = new QueryClient()

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

在创建QueryClient的时候,我们可以传入一些参数,用于管理项目中的请求,缓存,日志的相关配置,这些配置会对整个项目生效,其中包含了四个模块的配置参数

new QueryClient({
  queryCache?: QueryCache;//请求缓存相关配置
  mutationCache?: MutationCache;//数据修改缓存相关配置
  logger?: Logger;//日志相关配置
  defaultOptions?: DefaultOptions;//请求基础配置
})

其中,defaultOptions用于配置项目中useQuery请求的管理,常用的配置如下:

  • staleTiem:重新获取数据的时间间隔,默认为0
  • cacheTime:设置数据在缓存中保留的时间,超过此时间后数据将被自动清除,默认值为5分钟
  • retry:失败重试次数,默认3次;
  • refetchOnWindowFocus:窗口重新获得焦点时重新获取数据,默认为false
  • refetechOnReconnect:网络重新连接
  • refetchOnMount:实例重新挂载时重新拉取请求
  • enabled:如果为false,useQuery不会触发,需要使用其返回的refetch来触发操作
  • queryFn:全局定义请求方法,其他地方使用时只需要直接传入请求参数

useQuery

useQueryReact-query提供的用于请求接口并管理请求状态等信息的Hook

export const useGetAttachments = (subjectId: string) => {
  const { data, ...rest } = useQuery({
    queryKey: ['querySubjectAttachments',subjectId],
    queryFn: () => {
      return queryAllAttachments(subjectId).then((data: ResBasic<IAttachmentRes>) => {
        return data;
      });
    },
  });
  return {
    dataSourceList: (data?.data?.files ?? []).map((item: IAttachmentItem) => {
      return {
        title: item.file_name,
        size: item.file_size,
        fileType: item.type,
        fileId: item.file_uuid,
        downloadUrl: item.download_url,
      };
    }),
    ...rest,
  };
};

useQuery接收一个配置对象,其中:

  • queryKey:必传,用于请求数据缓存唯一的key值,也可以在数组中,写入多项如:['querySubjectAttachments', subjectId]这样React-Query在使用的时候会自动把它拼接为/querySubjectAttachments/subjectId,这个在缓存用户访问过的页面时,非常有用。
    • 参数较多的情况,可以在queryKey键值里面分别展开,如下所示:
export const useGetPatientList = (params: IQuerySubejctPatientList) => {
  const { data, ...rest } = useQuery({
    queryKey: ['querySubjectPatientList', params.subjectId, params.patient_filter],
    queryFn: () => {
      return querySubejctPatientList(params).then((data: ResBasic<IPatientListRowsRes>) => {
        return data;
      });
    },
  });
  return {
    viewRows: [...processData(data)],
    totalCounts: data?.data.rows?.length ? data?.data.page_info.total_rows : 0,
    ...rest,
  };
};
const processData = (data: ResBasic<IPatientListRowsRes> | undefined) => {
  return (data?.data?.rows ?? []).map((row: IPatientListRows) => {
    const map = new Map();
    row.records.forEach((record) => {
      map.set(record.field_uuid, record.value);
    });
    map.set(RowKey, row.row_uuid);
    map.set(PatientKey, row.patient_uuid);
    map.set(IsFavoriteKey, row.is_favorite);
    return Object.fromEntries(map.entries());
  });
};
  • queryFn:用于请求的方法,如果在QueryClient中配置了,这里可以不必再写,需要返回请求完成后所处理的数据;
  • 除了上述两项基本的参数,useQuery还可以传入上面defaultOptions的所有参数,来表示对这个单独的配置。
  • useQuery会返回一个对象,里面包含着请求相关的所有信息,这些信息会随着请求的进度而改变,就无需我们再使用一组state变量来进行管理了,常用的包括:
    • isLoading:请求是否正在进行;
    • error:请求是否出错,这里只会对500,404等做出反应,如果有其他情况,需要再请求方法里面throw
    • isSuccess:请求是否成功
    • status:请求状态,包含上面几种情况
    • data:返回数据 除此之外,useQuery拉取回来的数据,会被默认缓存起来,然后可以通过配置过期时间,重新拉取等

useQueryClient

通过useQueryClient,我们可以获取到之前注入的容器实例,里面保存着所有我们缓存的信息,以及配置信息,而它本质上其实也是对React.useContext的封装。

function Example() {
  const queryClient = useQueryClient()

  const data = queryClient.getQueryData(['querySubjectAttachments'])

  return (
    //...
  )
}

除了访问缓存数据,queryClient还有很多API:

  • queryClient.queryKeyCache: 这个属性包含了所有当前已经缓存的查询键和相应的查询结果;
  • queryClient.fetchQuery: 是一个异步方法,可用于获取和缓存查询。它要么用数据解析,要么用错误抛出。如果你只想获取一个不需要结果的查询,可以使用prefetchQuery方法。如果查询存在,且数据不是无效的,也不是比给定staleTime早,那么将返回来自缓存的数据。否则,它将尝试获取最新的数据。
try {
  const data = await queryClient.fetchQuery({ queryKey, queryFn })
} catch (error) {
  console.log(error)
}
// 指定一个staleTime,当缓存过期的时候才会去获取数据
try {
  const data = await queryClient.fetchQuery({
    queryKey,
    queryFn,
    staleTime: 10000,
  })
} catch (error) {
  console.log(error)
}
//fetchQuery的options和useQuery的配置一样,除去这些配置: enabled, refetchInterval, refetchIntervalInBackground, refetchOnWindowFocus, refetchOnReconnect, refetchOnMount, notifyOnChangeProps, throwOnError, select, suspense, placeholderData
  • queryClient.fetchInfiniteQuery: 该方法与fetchQuery一样,但可用于获取缓存无限查询;
  • queryClient.prefetchQuery({queryKey,queryFn,options?}):是一种异步方法,可以在需要查询之前预获取查询;该方法的工作原理和fetchQuery相同,只是它不抛出和返回任何数据。其中,配置和fetchQuery一样。
  • queryClient.prefetchInfiniteQuery:该方法与prefetchQuery一样,但可用于获取缓存无限查询;
  • queryClient.getQueryData(queryKey):是一个同步函数,根据指定的查询键从缓存中获取数据;如果查询不存在,则返回undefined
  • queryClient.ensureQueryData({queryKey,queryFn}): 是一个异步函数,可用于获取对应键的缓存数据,如果查询不存在,那将会触发queryClient.fetchQuery方法并返回结果;
  • queryClient.setQueryData(queryKey,data,options?): 将指定数据存储到缓存中,并触发相应更新;
  • queryClient.invalidateQueries(queryKey,options?): 使特定查询失效,下次访问时会重新获取最新数据;
  • queryClient.invalidateQueries((key)=>key.startsWith('examplae)): 通过函数形式批量使多个查询失效
  • queryClient.refetchQueries(queryKeyOrPredicateFn,options?): 手动触发重新获取指定或匹配条件的查询数据。
  • queryClient.cancelQueries(queryKey,options?):取消正在进行中的查询请求,避免不必要的网络请求
  • queryClient.removeQueries
  • queryClient.resetQueries
  • queryClient.clear():清除整个queryClient内容,包括所有的queries和mutations数据 …

useMutation

除了获取数据,很多时候还需要处理数据的修改,比如说最简单的todolist例子,除了拉取数据列表,还需要增删改数据,而这个时候除了需要发送接口,还需要修改本地的数据,React-Query提供了useMutation来帮我们完成这些事情。 用法:

const {
  data,
  error,
  isError,
  isIdle,
  isLoading,
  isPaused,
  isSuccess,
  failureCount,
  failureReason,
  mutate,
  mutateAsync,
  reset,
  status,
} = useMutation({
  mutationFn,
  cacheTime,
  mutationKey,
  networkMode,
  onError,
  onMutate,
  onSettled,
  onSuccess,
  retry,
  retryDelay,
  useErrorBoundary,
  meta,
})

mutate(variables, {
  onError,
  onSettled,
  onSuccess,
})

比如,我们已经拉取了data,现在我们想新增一条数据,那我们可以:

const {isLoading,isError,isSuccess,mutate} = useMutation({
    mutationFn: async (newData) => insertNewData(newData),
    onSuccess: (data: TData,variables: TVariables,context?: TContext) => {
      queryClient.invalidateQueries({ queryKey: ['repoData'] });
    },
    onError: (error) => {
      console.log(error)
    }
  });

其中:

  • mutationFn:必传;代表元数据的方法
  • onSuccess:可选;成功的回调函数;接口调用成功后的回调,其可接受三个参数,分别为:
    • 第一个参数data:为接口成功后返回的值;
    • 第二个参数variables:是发起这个请求时携带的参数,因此在成功回调函数里不仅可以获得后端返回的结果,还可以访问到发起请求携带的参数;
    • 第三个参数context:表示上下文数据,它是一个可选参数,通常用于在进行mutation请求时传递一些额外的上下文信息或数据【比如用户登录状态,认证信息等】。这样可以在成功回调函数中访问并使用这些上下文数据。
  • onError:可选;失败的回调; 返回的数据和useQuery基本相同,这里的mutate则是触发更改的方法,如果我们想执行useMutation中传入的方法,我们只需要调用mutate即可,传给mutate的参数都会被传到useMutation的构造方法中。
const updateDate = async (newData)=>{
    mutate(newData);
}
//传入上下文信息
const updateDate = async (newData)=>{
const contextData = { auth: true }; // 定义上下文数据
mutate(newData,
    {context: contextData});// 传递上下文数据
}

其他详见:tanstack.com/query/v4/do…

React-Query除了以上的最核心的对服务端数据进行增删改查的功能,除此之外,React-Query还有很多其他的能力。

useQueries

useQueries是一个用于同时发起多个查询的React Hook。它允许在一个组件中同时发起多个查询,并返回一个包含多个查询结果的数组,这个Hook对于需要一次性获取多个数据的情况非常有用。其特点和主要作用有:

  • 并行查询:useQueries允许在同一时间点发起多个并行查询,而不需要按顺序等待每一个查询完成后再进行下一个查询。
  • 单独控制每个查询:每个查询都可以单独控制,包括设置缓存策略,触发重新获取,出错处理等。
  • 返回值:useQueries返回一个数组,数组中包含了所有并行查询的结果。可以通过遍历这个数组来访问每一个查询的结果。
  • 处理loading状态:可以方便地处理并显示组件加载状态,例如显示加载指示器直到所有并行查询完成。
  • 使用方式:通常使用useQueries结合queryKeyqueryFn来定义多个同时执行的异步操作。如下所示:
const queries = useQueries([
{queryKey:'key1',queryFn:fetchData},
{queryKey:'key2',queryFn:fetchData},
{queryKey:'key3',queryFn:fetchData},
])

//处理每个查询结果
queries.forEach((query)=>{
    if(query.loading){
     return <div>Loading....</div>;
    }
    
    if(query.error){
     return <div>Error: {query.error.message}</div>;
    }
    
    return <div>data:{query.data}</div>
})

如果需要传参,可以这样写:

// 并行获取初始数据
export const useQuerysViewInitalData = (params: {
  queryViewBaseInfoParams: QueryViewBaseInfoParams;
  queryViewFilterConfigParams: QueryViewFilterConfigParams;
  queryViewFilterConditionParams: QueryViewFilterConditionParams;
}) => {
  return useQueries({
    queries: [
      {
        queryKey: QUERYKEY.viewBaseInfo(params.queryViewBaseInfoParams),
        queryFn: () => queryViewBaseInfo(params.queryViewBaseInfoParams),
        staleTime: 0,
      },
      {
        queryKey: QUERYKEY.viewFilterConfig(params.queryViewFilterConfigParams),
        queryFn: () => queryViewFilterConfig(params.queryViewFilterConfigParams),
        staleTime: 0,
      },
      {
        queryKey: QUERYKEY.supportOpertor(),
        queryFn: () => querySupportOperator(),
        staleTime: 0,
      },
    ],
  });
};

数据预获取

有时候我们不需要整个页面loading来等待数据加载,我们更希望在用户操作之前就完成数据拉取,比如hover详情链接,而不是点击详情的时候;则可以使用queryClient的prefetchQuery方法,提前拉取到用户可能会访问的数据,并加入缓存中,由于不需要监听服务端状态,所以这个方法回避useQuery高效许多。

onMouseEnter={async ()=>{
  await queryClient.prefetchQuery({
      queryKey: ['character',char.id],
      queryFn:()=>getCharacter(char.id),
      staleTime: 10*1000//只有超过10s之后才开始重新获取
  })
}}

分页缓存

通过动态设置queryKey,并将keepPreviousDate设置为true,我们可以很轻松的缓存之前页码的数据

const {status, data,error,isFetching,isPreviousData} = useQuery({
    queryKey:['projects',page],
    queryFn:()=>fetchProjects(page),
    keepPreviousData:true,
    staleTime: 5000
})

滚动列表渲染

使用useInfiniteQuery定义拉取获取数据的方法,以及上下页的逻辑,然后返回更新页面数据的状态,以及触发更新的方法;

const {status,data,error,isFetchingNextPage,isFetchingPreviousPage,fetchNextPage,fetchPreviousPage,hasNextPage,hasPreviousPage} useInfiniteQuery(['projects'],async ({pageParam = 0})=>{
    const res = await axios.get('/api/projects?cursor='+pageParam)
    return res.data
},{
    getPreviousPageParam: ( firstPage ) => firstPage.previousId ?? undefined,
    getNextPageParam: ( lastPage ) => lastPage.nextId ?? undefined
})

devTools配套开发工具

导入开发工具

import {ReactQueryDevtools} from 'react-query/devtools'

<ReactQueryDevtools initialOpen={false}>

默认情况下,当process.env.NODE_ENV==='production时开启Devtools,不必担心构建时需要排除他们; 浮动模式下开启,会将devtools作为固定的浮动元素安装在开发的应用程序中,并在屏幕一角提供一个切换按钮以显示和隐藏devtools,在devtools中我们可以直观的看到已经缓存下来的数据和整个项目的配置,以及各个接口的状态等