react-query 探究

1,171 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第17天,点击查看活动详情

react-query是react官方设计的一套请求库,它使 React 应用程序中的获取、缓存、同步和更新服务器状态变得轻而易举。

介绍

文档:react-query.tanstack.com/installatio…

其他人的介绍:zhuanlan.zhihu.com/p/265146038

基于hook,只能管理请求的内容,包括数据、状态、缓存、更新等。

甚至可以替换redux,mobx

  • 基于axios等请求库封装,可以实现请求、轮询、失败重试、无限加载等功能
  • 在网络重连、窗口获取焦点等时机向服务器发送请求同步状态
  • 可以把服务端状态缓存在客户端内存中,让组件获取

安装:

npm i react-query axios --save

使用

  • index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from 'react-query';
import App from './App';
const queryClient = new QueryClient();//创建一个查询客户端的实例
const root = ReactDOM.createRoot(document.getElementById('root')); //react 18 的写法
root.render(
  //负责把查询客户端传递给所有的子组件
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);
  • request.js
import axios, { CancelToken } from 'axios';
axios.interceptors.response.use(response => response.data);
axios.defaults.baseURL = 'http://localhost:8080';
export default axios;
export { CancelToken }
  • App.js
import React from 'react'
import { useQuery } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
import request from './request';
// 会将请求缓存起来,key是 user 
// isIdle指是否启动了,error.message就是错误信息,比如404
const {data,isLoading,isError,isIdle,error} = useQuery('user',()=>request.get('./user'),{
    refetchOnWindowFocus: true,// 请求默认是当鼠标重新聚焦到当前tab页面时重新请求
    refetchOnReconnect: true, // 网络恢复
    staleTime: 5000, // 过期时间,请求在5s内不过期,也就是说刚请求,5s内重新聚焦不会重新请求
    cacheTime: 5000, // 缓存时间,当组件切换,请求过的数据没被使用,5s后会被回收。
    refetchInterval: 1000//轮询时间
    enabled: //为true就代表激活,可以用变量控制
    retry: //重新请求次数
    retryDelay: (attemptIndex)=>Math.min(30000,1000*2**attemptIndex) //重试间隔时间 1、2、4..30
	initialData: initialUser,//初始化数据
})
 return (
    <>
      <ul>
        {
          data?.map(user => <li key={user.id}>{user.name}</li>)
        }
      </ul>
    </>
  )

状态监听


import React, { useEffect, useState } from 'react'
import { useQuery, QueryObserver, useQueryClient } from 'react-query';
function Status() {
  const [data, setData] = useState();
  const queryClient = useQueryClient(); //操控缓存
  useEffect(() => {
      //设置观察 users
    const observer = new QueryObserver(queryClient, { queryKey: 'users' });
      //订阅,当数据变动时,要更新数值。
    return observer.subscribe(result => setData(result.data));
  }, []);
  return (
    <div>
      共计{data?.length}个用户
    </div>
  )
}

请求带参数

这个是一个get请求,带参数 
const { data, isIdle, isError, error } = useQuery(['user', userId], () => {
    const promise = request.get('/user', { params: { userId } })
  },

并发多个请求

react-query.tanstack.com/reference/u…

 const {results1,results2} = useQueries([
   { queryKey: ['post', 1], queryFn: fetchPost },
   { queryKey: ['post', 2], queryFn: fetchPost },
 ])

取消请求

像防抖一样,新请求会取消之前的请求

axios取消请求文档:juejin.cn/post/703954…

react-query文档:react-query.tanstack.com/guides/quer…

However, if you consume the AbortSignal or attach a cancel function to your Promise, the Promise will be cancelled (e.g. aborting the fetch) and therefore, also the Query must be cancelled. Cancelling the query will result in its state being reverted to its previous state

  import request, { CancelToken } from './request';
  const { data, isIdle, isError, error } = useQuery(['user', userId], () => {
    const source = CancelToken.source();
    const promise = request.get('/user', { params: { userId }, cancelToken: source.token }).catch(error => {
      if (request.isCancel(error)) {
        console.log(error.message);
      }
    })
    promise.cancel = () => source.cancel('请求被React Query取消了!')
    return promise;
  }
// query在重复请求时会检查有无cancel方法,会调  promise.cancel ,source.cancel会去取消请求。

子路由使用父级路由的数据

  • 子路由在父级路由请求里查数据
  const queryClient = useQueryClient();
  const userQuery = useQuery(['user', userId],
    () => request.get('/user', { params: { userId } }), {
    initialData: () => queryClient.getQueryData('users').find(user => user.id === userId),
    initialStable: false
  });
  • 预缓存、父级路由请求后 在缓存中添加子路由请求的缓存
 const usersQuery = useQuery('users', () => request.get('/users'), {
    onSuccess(data) {
      const users = queryClient.getQueryData('users');
      users.forEach(user => {
        queryClient.setQueryData(['user', user.id], user);
      });
      console.log('查询成功', data);
    },
    onError(error) {
      console.log('查询失败', error);
    },
    onSettled(data, error) {
      console.log('查询结束 ', data, error);
    }
  });
// 这样子路由请求时,会发现已经有请求过的缓存了。

预查询

提前请求。类似 prefetch

这里是鼠标移到按钮上就去请求放缓存里,不用点击再请求。

 <button
        onMouseEnter={() => {
          queryClient.prefetchQuery('users', () => request.get('/users'));
        }}
        onClick={() => setShow(true)}>显示用户列表</button>

更新

举个例子,输入框输入东西后点保存后数据添加到列表后

import { useQuery, useQueryClient, useMutation } from 'react-query';
function Users({ setUserId }) {
  const { data, isLoading, isFetching } = useQuery('users', () => request.get('/users'));
  if (isLoading) return "加载中...."
  return (
    <>
      <ul>
        {data?.map(user => <li key={user.id}>{user.name}</li>)}
      </ul>
      {isFetching && "更新中..."}
    </>
  )
}
function App() {
  const queryClient = useQueryClient();
  const nameRef = React.useRef();
    // mutate 更新方法,调用时会去请求。 reset 重置状态
  const { mutate: saveUser, isLoading, isError, isSuccess, error, reset } = useMutation(
    (values) => request.post('/users', values), {
    onMutate(values) {
        //更新触发后 直接往缓存中 users 中添加数据,乐观更新,假定成功,不用等请求回调
      const oldUsers = queryClient.getQueryData('users');
      queryClient.setQueryData('users', oldUsers => [...oldUsers, { ...values, id: Date.now() + "" }]);
        //这里返回的会跑到onError的第三个参数上,
      return () => queryClient.setQueryData('users', oldUsers);;
    },
    onSuccess() {
      //在更新成功后让客户端用户列表的缓存失效,会立刻重新发起请求
      queryClient.invalidateQueries('users');
    },
        // 请求出错,回用老数据
    onError(error, values, rollback) {
      rollback();
    },
    onSettled() {
        // 请求结束,清空输入,重置状态
      setTimeout(() => {
        reset();
        nameRef.current.value = '';
      }, 3000);
    }
  });
  const handleSubmit = (event) => {
    event.preventDefault();
    const name = nameRef.current.value;
    const user = { name };
    saveUser(user);
  }
  return (
    <>
      <Users />
      <form onSubmit={handleSubmit}>
        <input ref={nameRef} />
        <input type="submit"
          value={isLoading ? '保存中' : isError ? '保存失败' : isSuccess ? '保存成功' : '保存'} />
      </form>
    </>
  )

分页

function fetchUsers({ queryKey: [_, { pageNumber }] }) {
  return request.get('/users', {
    params: {
      pageNumber
    }
  });
}
function Users() {
  const queryClient = useQueryClient();
  const [pageNumber, setPageNumber] = React.useState(1);
    // pageNumber 变化时 自动请求 
  const { data, isLoading, isFetching } = useQuery(['users', { pageNumber }], fetchUsers, {
      // 预加载下一页,没必要
    onSuccess() {
      queryClient.prefetchQuery(['users', { pageNumber: pageNumber + 1 }], fetchUsers);
    }
  });
  return (
    <>
      <ul>
        {data?.data?.map(user => <li key={user.id}>{user.name}</li>)}
      </ul>
      <button disabled={pageNumber <= 1} onClick={() => setPageNumber(pageNumber => pageNumber - 1)}>上页</button>
      <span>{pageNumber}</span>
      <button disabled={pageNumber >= data?.totalNumber} onClick={() => setPageNumber(pageNumber => pageNumber + 1)}>下页</button>
    </>
  )

点击加载更多

import {  useInfiniteQuery } from 'react-query';
function fetchUsers({ pageParam = 1 }) {
  return request.get('/users', {
    params: {
      pageNumber: pageParam
    }
  });
}
function Users() {
  const { data, hasNextPage, fetchNextPage } = useInfiniteQuery('users', fetchUsers, {
      //一定要给的,获取下一页  pageNumber, hasMore 是返回的数据解构的
    getNextPageParam: ({ pageNumber, hasMore }) => {
      //无限加载的时候 并没有总页数
      return hasMore && pageNumber + 1;
      //return pageNumber < totalNumber ? pageNumber + 1 : false;
    }
  });
  return (
    <>
      <ul>
        {
          data?.pages?.map((page, index) => {
            return (
              <React.Fragment key={index}>
                {page?.data?.map(user => <li key={user.id}>{user.name}</li>)}
              </React.Fragment>
            )
          })
        }
      </ul>
      <button disabled={!hasNextPage} onClick={fetchNextPage}>加载更多</button>
    </>
  )

工具

可以得知合和操控调试当前过期的请求,正在响应的请求等。

import { ReactQueryDevtools } from 'react-query/devtools';
<ReactQueryDevtools initialIsOpen={true} />