react-query在项目中的架构封装设计(大量实践经验)

前言

本文的定位是 react-query 的进阶教程,我们假定你已经对 react-query 有一定的了解或者熟悉,并能搞清如下问题:

  1. 了解如何配置 react-query
  2. 熟悉 useQuery / useMutation 的使用

如果你连 cache 和 stale 时间都搞不清楚,或者完全对 react-query 小白,可以先阅读入门文章:

《 React Api 请求最佳实践 react-query3 使用教程(比 swr 更好用更强大)》

另外,axios 或者 fetch 的底层封装是属于基础能力,本文不会涉及。

架构设计

useQuery

先聊最重要的 useQuery ,这个东西是查询最常用的。

全局配置最佳实践

首先是全局配置:

import {
  QueryClient,
  QueryClientProvider
} from 'react-query'

// Create a client
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false
    }
  }
})

ReactDOM.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>,
  document.getElementById('root')
);

熟悉 swr 的都知道,重新聚焦的时候不能再去获取一次数据了,所以我们会配一个 revalidateOnFocus: false ,不然很多情况会造成我只切换下浏览器 tab 你就给我把数据换了,或者当前分页和输入内容等丢了,或者被破坏了,在 react-query 中也是如此,所以聚焦重新 refetch 我们需要关掉。

全局配置最佳实践到此结束。

注意:

  1. cacheTime 默认 30s ,其实新鲜时间 staleTime 也是一样的,我们一般情况不需要去主动设定,因为我们想要的数据要保证最新性,极少的情况需要控制缓存,所以即使你真的需要缓存配置,请在 useQuery 时具体进行个别的配置。

  2. 可视化 devtools 的配置这里就不谈了,官方文档 写的很清楚,这个东西实际开发中用到的机会还是比较少的,更多情况可以当做一个 当前在开发环境 的小标识 icon。

封装最佳实践

我们遵循 axios / fetch 作为底层封装基础上

=> 进行 api 封装

=> 进行 useQuery hooks 封装

=> 业务中使用

的三步原则。

每个接口建议遵循如下结构:

第一层:api 封装
// apis/index.ts

export interface IGetSearchResultReq {
  key: any
}

export interface ISearchResult {
  key: any
}

export const getSearchResult = async (params: IGetSearchResultReq) => {
  // const res = await fetch('https://www.baidu.com')
  const res = {
    code: 200,
    data: { key: 'value' }
  }
  return res?.data as ISearchResult | undefined
}

注意:

  1. api 封装建议把 type 和函数写在一个文件,为什么要这么做,因为你的 response type 可能很长,如果你单独维护一份 .d.ts ,一是接口多了这个文件会变的特别大,很难阅读,其次是如果以后迁移或者其他项目需要使用,再抽出来会很麻烦,但是你都写在一个文件里,可以保证最小聚合性,作为一个小单元,随时可以移动到任意地方或者其他项目使用。

  2. 响应直接取 res?.data 即可,但前提是你对异常 code 的处理在 axios 拦截器里做了,所以我们不需要担心 code 的问题,而且我们做了可能 undefined 的处理,假如你后续在应用里写的没问题,即使接口炸了应用也不会炸,如果你真的需要根据 code 判断接口成功性,可以把 res 整体放出来,这个无所谓。

  3. 一般 api 接口文件会放在 src/apis/* 或者 src/services/* 下,放在哪里并不重要,但是你要清楚。

第二层:hooks 封装

先汇总请求 key:

// apis/key.ts

export const RQ_MODULE_PREFIX = '_RQ_module_name'

export const RQ_SEARCH_RESULT = `${RQ_MODULE_PREFIX}_search_result`

汇总请求 key 的意义在于:

  1. 在之前的文章介绍过,调取全局 client 进行重复请求或者阻断、获取缓存都是会模糊匹配 key list 的,所以我们是有必要汇总 key 的,为了以后可能的全局操作提供便利性。

  2. 一个有意义的 key 可以帮助你在 react-query 可视化 devtools 里定位是什么请求。

下一步封装 hooks:

// hooks/useSearchResult.ts

import { useQuery } from 'react-query'
import { isNil, isEmpty } from 'lodash'
import { IGetSearchResultReq, getSearchResult } from '../apis'
import { RQ_SEARCH_RESULT } from '../apis/key'

export const useSearchResult = (params: IGetSearchResultReq) => {

  const result = useQuery(
    [RQ_SEARCH_RESULT, params],
    async () => {
      // 你可能有一些其他固定逻辑在这里
      // ...
      return getSearchResult(params)
    },
    {
      enabled: !isEmpty(params) && !isNil(params?.key),
    }
  )

  return result
}

这里注意几点:

  1. 这个 case 里的入参是请求参数,有时候你的请求可能是一些固定参数,或者没有 api 请求接口需要的参数那么多,所以根据实际情况写死或者写到这里的 hooks 入参即可,这可以最大限度的帮助你减小入参数量,提高易用性。

  2. 对于个别的 useQuery 的 config 配置,按实际情况来即可,一般情况我们的入参都是异步的,所以为了保证安全触发性,必须控制什么时候才去查询,使用 enabled 选项 + lodash 加持入参判断即可!

这里再说几个常用的 config:

{
  // 控制满足什么条件的时候才去请求,保证安全性,一般情况必须指定
  enabled: !isEmpty(params) && !isNil(params?.key),
  
  // 每次 新请求开始 到 获取到新数据 前这段时间保留上一次的旧数据
  // 一般情况分页查询,表格分页切换这种场景会用到,保证换页时不会出现一个空列表
  keepPreviousData: true,

  // 轮询间隔,需要轮询的时候使用
  refetchInterval: 3e3,

  // 获取数据成功时的回调,入参是响应值
  // 有时我们只关注这个响应的部分内容,则可以在这个回调里操作,并不需要 useQuery 返回的 data 等
  onSuccess: (res) => {

  },

  // swr 里有个在一定时间内重复请求无效的选项 dedupingInterval
  // 在 react-query 里把 cache 和 stale 时间指定一致即可
  staleTime: 60 * 1e3,
  cacheTime: 60 * 1e3
}
第三层:业务使用
import React from 'react';
import { Spin } from 'antd'
import { useSearchResult } from './hooks/useSearchResult'

function App() {

  const {
    data: searchResult,
    isLoading: searchResultLoading,
    isFetching: searchResultFetching,
    refetch: searchResultRefetch
  } = useSearchResult({
    key: 1
  })

  return (
    <Spin spinning={searchResultLoading}>{searchResult?.key}</Spin>
  );
}

export default App;

注意:

  1. 为什么不直接使用结构的 data 还要命一个别名 searchResult ,一是为了可读性,二是很多情况你并不只这一个请求,可能是先查一个再拿到结果再去查第二个内容,所以结构后重新命名的意义就存在了。

  2. 关于 isLoadingisFetching 的区别就不在提了,在 上篇文章内 已经介绍过,一般情况下,骨架屏使用 isLoading,因为我们不想骨架屏一直在查询时出现,假如是 Spin 这种 loading 态,建议使用 isFetching,当然使用 isLoading 也并非不可。

  3. refetch 是很常用的,比如你保存新数据后重新获取一次最新数据,当然前提是后端不会有短时间的查询强缓存!

另外,假如你有多个查询,为了 loading 的精准性,可以把多个 isFetchingisLoading 用 useMemo 写在一起:

  const isLoading = useMemo(
    () => xxxIsLoaidng || yyyIsLoading,
    [xxxIsLoaidng, yyyIsLoading]
  )

到此为止,useQuery 的封装部分到此结束。

useMutation

这个 api 我还是不推荐大家经常使用,因为他不强依赖 key 标识,而且也比较麻烦,所以也不推荐封装。

看一个 case:

import React from 'react';
import { message, Button } from 'antd'
import { useMutation } from 'react-query';

function App() {

  const mutation = useMutation(async (
    // ↓ 注意这里只支持第一个位置入参,多了不支持,如果有多个参数,需要写成对象形式
    params: {
      key1: any,
      key2: any
    }
  ) => {
    const res = await requertApi({
      // params...
      key1,
    })
    if (res?.code === 200) {
      message.success('成功')
      return
    }
    message.error('失败,请重试')
  })

  return (
      <Button
        loading={mutation.isLoading} 
        onClick={() => {
          mutation.mutateAsync()
        }}
      >
        按钮
      </Button>
  );
}

export default App;

乍一看,我们的好处是什么,是 loading 态不需要自己管理了!

可问题是什么:

  1. 得到的 mutation 必须通过 mutation.mutate()mutation.mutateAsync() 调用,不能直接赋值给 onClick ,需要多写一步内联函数,否则你就要写一堆 useCallback

  2. mutation 传参只支持第一个参数,也就是只能用第一个位置传参,如果你有多个参数,必须写成对象形式。

总而言之给人的感觉就是麻烦,而且对于使用 useMutation 的 api,都是非查询的 api,所以我们也不需要享受数据一致性的好处,单纯发个请求而已。

所以对于 mutation 这个东西,还是适当使用,适可而止。

总结

可能 useQuery 的上手成本比 swr 稍微高,而且还要封装这么多层是不是太不友好了,要重返 redux + saga 的大量模板代码时代了?

其实不然,封装 hooks 除了减少参数,最重要的是做一些脏活累活,保证你在多处复用时的简便,毕竟在模块化的时代,多处都要查询一个东西是很常见的,所以每个地方都写一次 useQuery 也不友好。

问题在于如何减少重复代码,所以封装的价值也是存在的。

从另一个角度想,假如你有很多项目,都需要使用这个接口,那你直接把封装好的 api 和 hooks 文件拷贝过去就行了。

React-chunked-uploader是一个用于React应用程序的文件分块上传库。要在一个React项目集成React-chunked-uploader,你可以按照以下步骤进行: 1. **安装依赖包**:首先,你需要安装react-chunked-uploader。可以通过npm或yarn命令行工具来安装它。 ```bash npm install react-chunked-uploader # 或者 yarn add react-chunked-uploader ``` 2. **引入组件**:在你的React组件引入`ChunkedUploader`组件。 ```javascript import { ChunkedUploader } from 'react-chunked-uploader'; ``` 3. **配置ChunkedUploader**:在你的组件使用`ChunkedUploader`,并配置必要的属性,比如上传的URL、文件类型限制、是否允许多文件上传等。 ```javascript function App() { const uploadUrl = "http://your-upload-url.com/upload"; const uploadConfig = { chunkSize: 1024 * 1024, // 分块大小为1MB concurrentChunkRequestLimit: 3, // 同时上传的块数 testChunks: true, // 可选,测试所有分块,实际应用通常设为false }; return ( <ChunkedUploader url={uploadUrl} config={uploadConfig} // 其他属性... /> ); } ``` 4. **处理上传事件**:你可以使用`onUploadStart`、`onChunkComplete`、`onUploadSuccess`等事件处理器来处理上传过程的各种事件。 5. **自定义样式和行为**:根据需要,你还可以对ChunkedUploader组件进行样式定制和行为调整,以符合你的应用需求。 6. **测试**:完成集成后,确保进行充分的测试,以验证文件上传功能是否正常工作。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值