【React Query】终极教程03: 深入理解和使用 useQuery

在上一章中,介绍了 QueryClientReact Query Devtools,接下来将结合实际工作场景来讲述 React Query 的最佳实践和一些思考~
长文预警!!useQuery 作为 React Query 最核心的 API 之一,必须要好好深究一番!

本章代码放在我的 github repoian-kevin126/react-query-best-practices

在 React Query 中,可以使用 useQuery 来获取、缓存和处理服务端状态。为了缓存数据,React Query 构建了一个叫 queryKey 的东西,结合其他的一些配置,React Query 可以将服务端状态管理提升到一个新的水平。还可以使用它在某些情况下重新获取数据和解决相互依赖的查询。

useQuery

在 React Query 文档 中,查询是这样定义的:查询是对异步数据源的声明式依赖,并且它与唯一 key 绑定。

可以这样使用:

jsx 复制代码
import { useQuery } from "@tanstack/react-query";

const values = useQuery({
   queryKey: <insertQueryKey>,
   queryFn: <insertQueryFunction>,
});
  • queryKey:用于标识查询的唯一 key,老版本可以设置为字符串,v4 之后推荐用数组;
  • queryFn:返回 Promise 的函数;

queryKey

queryKey 是 React Query 用来标识查询的唯一值。此外,React Query 还会使用 queryKey 将数据缓存到查询缓存(QueryCache)中,还能通过它手动处理查询。

queryKey 需要是一个数组(为了兼容性,依然还可以设置成字符串,但是推荐还是数组吧,说不定以后不支持字符串方式了 ),其中可以只包含一个字符串,也可以包含其他值,例如对象。需要注意的是,queryKey 数组中的值需要是可序列化的。

PS:在 React Query v4 之前,queryKey 并不一定需要是一个数组。它可以只是一个字符串,因为 React Query 会在内部将其转换为数组。所以,如果你在网上看到一些字符串作为 queryKey 的示例,不要觉得奇怪。

下面是一些 queryKey 的示例:

jsx 复制代码
useQuery({ queryKey: ['users']  })
useQuery({ queryKey: ['users', 10] })
useQuery({ queryKey: ['users', 10, { isVisible: true }] })
useQuery({ queryKey: ['users', page, filters] })

为了使你的 queryKey 唯一,并且 useQuery 钩子更可读,建议将查询的所有参数作为 queryKey 的一部分添加进去。就像 useEffect 上的依赖关系数组类似的那种心智模型,因为 queryKey 还允许 React Query 在查询的参数发生变化时,可以自动重新发起查询

需要注意的一点是,queryKey 是会被确定性散列化的,也就是说数组内项的顺序很重要,可以在官网源码中找到。那什么是确定性散列?其实散列也叫 hash(哈希)。

  • 确定性哈希 对于数据完整性验证很重要,通过对数据应用哈希函数,可以生成哈希值,该值充当该数据的数字指纹,具备唯一性。

比如,下面是一些查询, queryKey 在确定性散列化时将被视为是同一个查询:

jsx 复制代码
useQuery({ queryKey: ['users', 10, { page, filters }] })
useQuery({ queryKey: ['users', 10, { filters, page }] })
useQuery({ queryKey: ['users', 10, { page, random: undefined, filters }] })

这些示例都是同一个查询 ------ 因为在这三个示例中,queryKey 中数组的顺序是不变的。

你可能想知道这怎么可能?因为 page 和 filter 的位置每次都会改变,而且在最后一个示例中,还有第三个叫 random 的属性。

但事实上,它们仍在同一个对象内,而该对象在 queryKey 数组中的位置没有改变。此外, random 属性是 undefined 的,因此在进行散列(hashing)时,它会被忽略。

而下面这些散列化时就不是同一个查询:

jsx 复制代码
useQuery({ queryKey: ['users', 10, undefined, { page, filters }] })
useQuery({ queryKey: ['users', { page, filters }, 10] })
useQuery({ queryKey: ['users', 10, { page, filters }] })

你可能想知道为什么第一个示例与最后一个示例不一样,放在对象内和对象外区别这么大吗?因为顺序很重要,散列后,这个 undefined 将转化为散列键内的空值。

对于一些比较规范的 CRUD 接口请求方法,我们可以像下面这样将 queryKey 封装起来:

jsx 复制代码
const todoKeys = {
  all: ['todos'] as const,
  lists: () => [...todoKeys.all, 'list'] as const,
  list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
  details: () => [...todoKeys.all, 'detail'] as const,
  detail: (id: number) => [...todoKeys.details(), id] as const,
}

这样可以带来很好的灵活性,比如:

jsx 复制代码
queryClient.removeQueries({ queryKey: todoKeys.all })

queryClient.invalidateQueries({ queryKey: todoKeys.lists() })

queryClient.prefetchQueries({
  queryKey: todoKeys.detail(id),
  queryFn: () => fetchTodo(id),
})

queryFn

queryFn 需要是一个返回 Promise 的函数,这使得 React Query 变得更加强大,因为 queryFn 可以支持任何能够执行异步数据获取的 client:比如 RESTGraphQL

比如在 GraphQL 中:

jsx 复制代码
import { useQuery } from "@tanstack/react-query";
import { request, gql } from "graphql-request";

const customQuery = gql`
  query {
    posts {
      data {
        id
        title
      }
    }
  }
`;

const fetchGQL = async () => {
  const { posts: { data }, } = await request('xxx/sss', customQuery);
  
  return data;
};

// ...

useQuery({
  queryKey: ["posts"],
  queryFn: fetchGQL
});

RESTful API 中:

jsx 复制代码
import axios from "axios";

const fetchData = async () => {
  const { data } = await axios.get('https://xxx.aaa.com/api/sss');
  return data;
};

// ...

useQuery({
    queryKey: ["api"],
    queryFn: fetchData,
});

为了让 React Query 可以正确处理错误场景,在使用这些 client 时需要注意的一点是,它们是否会在请求失败时自动抛出错误。如果它们不抛出错误,你就必须自己抛出错误。

你可以在使用 fetch 的 queryFn 中这样做:

jsx 复制代码
const fetchDataWithFetch = async () => {
  const response = await fetch('https://xxx.aaa.com/api/sss')
  
  // 检查响应是否成功。如果无效,我们会抛出一个错误
  if (!response.ok) throw new Error('Something failed')
  
  return response.json()
}

在封装构建查询函数中,也可以将 queryKey 传递给 queryFn ,因为 queryKey 中包含了一些我们传的查询参数,传给 queryFn 之后,React Query 会在这些参数变化时自动取重新请求数据。

官方给了两种方式:

  • 内联函数(Inline function
  • 查询函数Context(QueryFunctionContext

内联函数

当查询键中需要传递给查询函数的参数不多时,就可以利用这种模式。来看一个示例:

jsx 复制代码
import axios from "axios";
import { useQuery } from "@tanstack/react-query";

const fetchData = async (nameOrId) => {
  const { data } = await axios.get(
    `https://dummyjson.com/products/${nameOrId}`
  );

  return nameOrId ? { products: [data] } : data;
};

export const useFetchData = (nameOrId) => {
  return useQuery({
    queryKey: ["api", nameOrId],
    queryFn: () => fetchData(nameOrId),
  });
};

然后在 App.js 中使用:

js 复制代码
import { useState } from "react";
import { useFetchData } from "./api";
import { Button, Table, Input } from "antd";

function App() {
  const [nameOrId, setNameOrId] = useState("");

  const { data } = useFetchData(nameOrId);

  const handleOnChangeName = (e) => {
    setNameOrId(e?.target?.value);
  };

  const columns = [
    {
      title: "名 称",
      dataIndex: "title",
      key: "title",
    },
    {
      title: "价 格",
      dataIndex: "price",
      key: "price",
    },
    {
      title: "品 牌",
      dataIndex: "brand",
      key: "brand",
    },
  ];

  return (
    <div className="App">
      <div style={{ display: "flex", alignItems: "center" }}>
        <Input value={nameOrId} onChange={handleOnChangeName} />
        <Button type="primary">搜索</Button>
      </div>
      <Table
        dataSource={data?.products ?? []}
        columns={columns}
        rowKey={"id"}
      />
    </div>
  );
}

export default App;

测试一下,打开页面,在不输入任何参数的情况下,会请求全量的数据:

在输入框中输入一个id,你就会发现触发了接口的重新请求了对应id的数据:

随意改变id的值,后台都会自动取请求接口数据,无需再去点击一次搜索按钮!是不是很方便?不需要我们手动再去做了,这在一些管理后台的列表页中非常好用~

QueryFunctionContext

在参数比较少的时候,上面这种模式非常适合。但是,如果你有十几个参数,而所有参数都需要在查询函数中使用。这种做法也不是不行,但会影响代码的可读性。为了避免这种情况,你可以使用 QueryFunctionContext 对象。

每次调用查询函数时,React Query 都会自动将 queryKey 作为 QueryFunctionContext 对象传递给 queryFn

下面是一个使用 QueryFunctionContext 模式的示例:

jsx 复制代码
import axios from "axios";
import { useQuery } from "@tanstack/react-query";
import qs from "qs";

const fetchData = async ({ queryKey }) => {
  const [_queryKeyIdentifier, params] = queryKey;
  console.log("🚀 ~ file: index.js:14 ~ fetchData ~ params:", params);

  const { data } = await axios.get(
    `https://dummyjson.com/products?${qs.stringify(params)}`
  );

  return data;
};

export const useFetchData = (params) => {
  return useQuery({
    queryKey: ["api", params],
    queryFn: fetchData,
  });
};

我们首先创建了 fetchData 函数。该函数将接收 QueryFunctionContext 作为参数,因此我们可以立即从该对象中解构 queryKey,进而解构到我们传入的参数。

然后将参数拼接到用于获取数据的 URL,这样,我们只需改变查询参数,后台就会自动发起接口请求,无需手动。

这种模式要求将查询的所有参数项添加到 queryKey 中,有一个缺点,那就是由于参数太多,你必须记住将它们添加到 queryKey 的顺序,才能在 queryFn 中使用它们。解决这个问题的方法之一是发送一个包含 queryFn 中所需全部参数的对象。这样就无需记住数组元素的顺序了。

具体方法如下:

jsx 复制代码
useQuery({
   queryKey: [{queryIdentifier: "api", someVariable}],
   queryFn: fetchData,
});

通过传递一个对象作为 queryKey ,该对象将作为 QueryFunctionContext 对象发送到 queryFn。然后,在接口请求函数中,你只需:

jsx 复制代码
const fetchData = async ({ queryKey }) => {
  const { someVariable } = queryKey[0];
  // ...
};

我们从 QueryFunctionContext 对象中重组了 queryKey 。然后,由于我们的对象将位于 queryKey 的第一个位置,我们可以从我们的对象中解构我们需要的值。

useQuery 返回值

使用 useQuery 时,它会返回一些值,我们可以从钩子的返回值中解构这些值来做一些逻辑处理:

jsx 复制代码
const values = useQuery(...);
const { data, error, status, fetchStatus }= useQuery(...);

主要有这几个:

  • data
  • error
  • status
  • fetchStatus

data

data 是 queryFn 返回的最后一次成功解析的数据:

jsx 复制代码
const App = () => {
  const { data } = useQuery({
    queryKey: ["api"],
    queryFn: fetchData,
  });
  
  return (
    <div>
       {data ? data.username : null}
    </div>
  );
};

error

通过 error 变量,可以访问查询函数失败后返回的错误对象。下面是使用 error 变量的方法:

jsx 复制代码
const App = () => {
  const { error } = useQuery({
    queryKey: ["api"],
    queryFn: fetchData,
  });
  
  return (
    <div>
       {error ? error.message : null}
    </div>
  );
};

status

在执行查询时,查询可能会经历几种状态。这些状态有助于向用户提供更多反馈。以下是 status 可能具有的状态:

  • loading:尚未完成查询,也没有缓存数据。
  • error:执行查询时出错,error 属性会收到查询函数返回的错误信息。
  • success:查询成功,并已返回数据,data 属性会接收查询函数返回的成功数据。

使用方法:

jsx 复制代码
const App = () => {
  const { status, error, data } = useQuery({
    queryKey: ["api"],
    queryFn: fetchData,
  });
  
  if(status === "loading" ) {
    return <div>Loading...</div>
  }
  
  if(status === "error" ) {
    return <div>error: {error.message}</div>
  }
  
  return (
    <div>
       {data.hello}
    </div>
  );
};

为了方便起见,React Query 还引入了一些布尔值的变量来帮助我们识别每种状态。它们如下

  • isLoading:status 处于 loading 状态
  • isError:status 处于 error 状态
  • isSuccess:status 处于 success 状态

让我们重写之前的代码:

jsx 复制代码
const App = () => {
  const {  isLoading, isError, error, data } = useQuery({
    queryKey: ["api"],
    queryFn: fetchData,
  });
  
  if(isLoading) {
    return <div>Loading...</div>
  }
  
  if(isError) {
    return <div>error: {error.message}</div>
  }
  
  return (
    <div>
       {data.hello}
    </div>
  );
};

fetchStatus

React Query v3 中,发现在处理用户离线的场景时存在一个问题:如果用户触发了查询,但由于某种原因在请求过程中失去了连接,状态变量将一直处于加载状态,直到用户恢复连接并自动重试查询。

为了解决这类问题,React Query v4 引入了一个名为 networkMode 的新属性。该属性可以有三种状态,但默认情况下会使用 online 状态。这种模式可以使用 fetchStatus 变量。

fetchStatus 变量提供了查询函数的相关状态信息。以下是该变量可能具有的状态:

  • fetching:当前正在执行查询函数。这意味着当前正在获取数据。
  • paused:查询想要获取数据,但由于连接中断,现在已停止执行。这表示当前已暂停。
  • idle:查询目前没有任何操作。这表示当前处于空闲状态。

可以这样使用:

jsx 复制代码
const App = () => {
  const {  fetchStatus, data } = useQuery({
    queryKey: ["api"],
    queryFn: fetchData,
  });
  
  if(fetchStatus === "paused" ) {
    return <div>Waiting for your connection to return...</div>
  }
  
  if(fetchStatus === "fetching" ) {
    return <div>Fetching...</div>
  }
  
  return (
    <div>
       {data.hello}
    </div>
  );
};

与 status 一样,React Query 也引入了一些布尔值的变量来帮助识别其中的两种状态。它们如下

  • isFetching:fetchStatus 变量处于获取状态
  • isPaused:fetchStatus 变量处于暂停状态

重写之前的代码:

jsx 复制代码
const App = () => {
  const { isFetching, isPaused, data } = useQuery({
    queryKey" [""pi"],
    queryFn: fetchData,
  });
  
  if(isPaused) {
    return <div>Waiting for your connection to return...</div>
  }
  
  if(isFetching) {
    return <div>Fetching...</div>
  }
  
  return (
    <div>
       {data.hello}
    </div>
  );
};

常用 options 说明

使用 useQuery 时,除了 queryKeyqueryFn 外,还可以向其中传递更多 options。这些options可帮助我们实现更好的开发体验和用户体验。主要有以下的几个:

  • staleTime
  • cacheTime
  • retry
  • retryDelay
  • enabled
  • onSuccess
  • onError

staleTime

staleTime 是查询数据的过期时间(以毫秒为单位)。当设定的时间过去后,这个查询就会被认为过期了。

当查询是未过期的时候,我们将从缓存中直接抓取数据,而不会触发更新缓存的新请求。当查询被标记为过期时,数据仍会从缓存中提取,但会先触发查询的自动重新获取。

默认情况下,所有查询都使用设置为 0 的 staleTime,这意味着所有缓存数据默认都被视为过期数据。

配置 staleTime 选项:

jsx 复制代码
useQuery({
  // 查询数据在一分钟内都是最新的
  staleTime: 60000,
});

cacheTime

cacheTime 是指缓存中不活跃的数据在内存中保留的时间(以毫秒为单位)。一旦超过这个时间,数据将被垃圾回收。

默认情况下,当查询没有 useQuery 的活动实例时,查询会被标记为不活跃的。此时,查询数据将在缓存中保留 5 分钟。5 分钟后,这些数据将被垃圾回收。

配置 cacheTime 选项:

jsx 复制代码
useQuery({
  // 在查询停止 1 分钟后,数据将被垃圾回收
  cacheTime: 60000,
});

retry

retry 表示查询失败时是否重试。为 true 时,将重试直至成功。如果为 false,则不会重试。该属性也可以是一个数字。当它是一个数字时,查询将重试指定的次数。

默认情况下,所有失败的查询都会重试 3 次。

使用方法:

jsx 复制代码
useQuery({
  retry: false,
  // 如果无法获取查询,那么它会重试 1 次
  // retry: 1,
});

retryDelay

retryDelay 选项是下次重试前的延迟时间(单位为毫秒)。

默认情况下,React Query 使用指数回退延迟算法(exponential backoff delay algorithm)来定义两次重试之间的重试时间。

配置 retryDelay 选项:

jsx 复制代码
useQuery({
  retryDelay: (attempt) => attempt * 2000,
});

上面定义了一个线性延迟函数作为 retryDelay 选项。每次重试时,该函数都会接收尝试次数并乘以 2000,意思就是每次重试的间隔时间将延长 2 秒。

enabled

enabled 表示何时可以执行查询。默认情况下,该值为 true,即查询都回立即执行。

我们可以这样使用 enabled 选项:

jsx 复制代码
useQuery({
  enabled: arrayVariable.length > 0
});

意思是只有在 arrayVariable 的大于 0 时才触发查询,否则不会执行查询。

onSuccess

onSuccess 选项是一个函数,当查询成功时会触发该函数。可以这样使用:

jsx 复制代码
useQuery({
  onSuccess: (data) => console.log("查询成功", data),
});

onError

onError 也是一个函数,会在查询失败时触发。可以这样使用:

jsx 复制代码
useQuery({
  onError: (error) => console.log("查询失败", error.message),
});

重新请求数据(Refetching)

有时候,你需要更新数据,因为可能数据已经过期,或者只是因为你有一段时间没有打开页面。无论是手动还是自动,React Query 都支持重新获取数据。

自动重请求

React Query 提供了几个 options,可以让你更轻松地保持服务端状态的新鲜度。为此,它会在某些情况下自动重新获取数据。

queryKey

queryKey 用于标识查询。在之前有提到,建议将所有查询函数的参数作为 queryKey 的一部分。为什么要这样做呢?

因为每当其中一些参数发生变化时,你的 queryKey 也会随之变化,而当你的 queryKey 发生变化时,你的查询也会自动重新执行。

让我们看看下面的例子:

jsx 复制代码
const [someVariable, setSomeVariable] = useState(0)

useQuery({
    queryKey: ["api", someVariable],
    queryFn: fetchData,
  });
  
return <button onClick={() => setSomeVariable(someVariable + 1)}>Click me</button>

我们定义了一个以 someVariable 作为 queryKey 的 useQuery。该查询将像往常一样在初始渲染时获取,但当我们点击按钮时,someVariable 的值将发生变化。queryKey 也会发生变化,这将触发查询自动重新请求,从而获得新数据。

Refetching options

在上面 "常用 options 说明" 那部分,实际上还有几个选项没说,它们是默认启用的,通常,只有在它们影响你的业务逻辑的情况下关闭,一般最好还是保持开启状态。

以下是 useQuery 默认启用的与数据重新获取相关的选项:

  • refetchOnWindowFocus:每当你聚焦到当前窗口时,就会触发一次重新获取。例如,如果你返回应用程序时切换了标签页,React Query 就会触发数据重新获取。
  • refetchOnMount:每当新组件挂载时,此选项都会触发一次重新获取。例如,当一个使用你定义的钩子的新组件挂载时,React Query 就会触发数据的重新获取。
  • refetchOnReconnect:当失去网络连接时,就会触发一次重新获取。

需要注意的一点是 ,这些选项默认只会在数据被标记为过期时重新获取数据。即使是过期数据也可以重新获取,因为所有这些选项(布尔值除外)都支持接收一个值为 "always " 的字符串。当这些选项的值为 "always" 时,即使数据没有过期,也会始终触发重新获取。

配置如下:

jsx 复制代码
useQuery({
    refetchOnMount: "always",
    refetchOnReconnect: true,
    refetchOnWindowFocus: false
});

在上面的代码中:

  • 对于 refetchOnMount,我们总是希望钩子在任何使用它的组件挂载时重新获取数据,即使缓存的数据不是过期的也重新获取
  • 对于 refetchOnReconnect,我们希望钩子在离线后重新获得连接时重新获取数据,但仅限于数据过期的情况。
  • 对于 refetchOnWindowFocus,我们不希望钩子在窗口聚焦时重新获取数据

可能会想到一个问题:是否有办法强制每隔几秒钟重新获取一次数据,即使数据并没有过期。即使你没有想到,React Query 也帮你做了。React Query 添加了另一个与重新获取相关的选项,叫做 refetchInterval。该选项允许你以毫秒为单位指定查询重新获取数据的频率。

我们可以这样使用它:

jsx 复制代码
useQuery({
    refetchInterval: 2000,
    refetchIntervalInBackground: true
});

在上面的代码中,我们将钩子配置为始终每 2 秒重新获取一次。我们还添加了另一个名为 refetchIntervalInBackground 的选项,其值为 true。即使窗口或标签页在后台运行,该选项也会允许查询继续重新获取。这样,自动重新获取就完成了。

下面,让我们看看如何在代码中触发手动重新获取。

手动重请求

手动触发查询重取有两种方法。可以使用 QueryClient 或从钩子中获取 refetch 函数。

使用 QueryClient

可以利用 QueryClient 在需要时强制重新获取数据。可以这样做:

jsx 复制代码
const queryClient = useQueryClient();
queryClient.refetchQueries({ queryKey: ["api"] })

通过使用 QueryClient,我们调用了它提供的一个 refetchQueries 的函数。通过该函数,可以触发重新获取与给定 queryKey 匹配的所有查询。在本代码中,我们将触发对所有具有["api"] queryKey 的查询。

使用 refetch

useQuery 的返回值提供了一个 refetch 函数。通过该函数,可以触发对该查询的重新获取。可以这样做:

jsx 复制代码
const { refetch } = useQuery({
    queryKey: ["api"],
    queryFn: fetchData,
});
// 使用
refetch()

从 useQuery 返回值中解构 refetch 函数。然后,只要我们想强制重新获取查询,就可以调用该函数。

依赖查询

在开发过程中,有时一个查询的执行依赖于之前的一个查询。在这种情况下,我们需要使用所谓的依赖查询。通过 enabled 选项,React Query 可以让查询依赖于其他查询。

你可以这样做:

jsx 复制代码
const App = () => {
  const { data: firstQueryData } = useQuery({
    queryKey: ["api"],
    queryFn: fetchData,
  });
  
  const canThisDependentQueryFetch = firstQueryData?.hello !== undefined;
  
  const { data: dependentData } = useQuery({
    queryKey: ["dependentApi", firstQueryData?.hello],
    queryFn: fetchDependentData,
    enabled: canThisDependentQueryFetch,
  });
  
  // ...
}

页面:

解释一下:

  1. 创建一个以 ["api"] 作为 queryKey、以 fetchData 函数作为查询函数的查询。
  2. 创建一个 canThisDependentQueryFetch 的变量,用于检查之前的查询是否包含我们需要的数据。这个布尔变量将帮助我们决定下一个查询是否可以获取数据。
  3. 然后以["dependentAPI", firstQueryData?.hello]作为 queryKey,以 fetchDependentData 函数作为 queryFn,以 canThisDependentQueryFetch 作为 enabled,创建第二个查询。
  4. 当上一个查询完成数据获取后,canThisDependentQueryFetch 将被置为 true,并执行第二个查询。

try 一 try

先准备一个接口请求的方法:

jsx 复制代码
export const fetchData = async ({ queryKey }) => {
  const { apiName } = queryKey[0];

  const response = await fetch(
    `https://danieljcafonso.builtwithdark.com/${apiName}`
  );

  if (!response.ok) throw new Error("Something failed in your request");

  return response.json();
};

export const apiA = "react-query-api";
export const apiB = "react-query-api-two";

然后定义三个组件。

第一个组件 ComponentA

jsx 复制代码
import { useQuery } from "@tanstack/react-query";
import { fetchData, apiA } from "../api";
import ComponentB from "./ComponentB";

const ComponentA = () => {
  const { data, error, isLoading, isError, isFetching } = useQuery({
    queryKey: [{ queryIdentifier: "api", apiName: apiA }],
    queryFn: fetchData,
    retry: 1,
  });

  if (isLoading) return <div> Loading data... </div>;

  if (isError) return <div>error: {error.message}</div>;

  return (
    <div>
      <p>ComponentA: {isFetching ? "Fetching Component A..." : data.hello}</p>
      <ComponentB />
    </div>
  );
};

export default ComponentA;

组件 ComponentB

jsx 复制代码
import { useQuery } from "@tanstack/react-query";
import { fetchData, apiB } from "../api";
import ComponentC from "./ComponentC";

const ComponentB = () => {
  const { data } = useQuery({
    queryKey: [{ queryIdentifier: "api", apiName: apiB }],
    queryFn: fetchData,
    onSuccess: (data) => console.log("Component B fetched data", data),
  });

  return (
    <div>
      <span>ComponentB: {data?.hello}</span>
      <ComponentC parentData={data} />
    </div>
  );
};

export default ComponentB;

组件 ComponentC

jsx 复制代码
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { fetchData, apiA } from "../api";

const ComponentC = ({ parentData }) => {
  const { data, isFetching } = useQuery({
    queryKey: [{ queryIdentifier: "api", apiName: apiA }],
    queryFn: fetchData,
    enabled: parentData !== undefined,
  });

  const queryClient = useQueryClient();

  const handleOnClick = () => {
    queryClient.refetchQueries({
      queryKey: [{ queryIdentifier: "api", apiName: apiA }],
    });
  };

  return (
    <div>
      <p>ComponentC: {isFetching ? "Fetching Component C..." : data.hello} </p>
      <button onClick={handleOnClick}>Refetch Parent Data</button>
    </div>
  );
};

export default ComponentC;

大致梳理一下逻辑:

  • 当组件 A 渲染时:

    • 使用 [{ queryIdentifier: "api", apiName: apiA }] queryKey 的 useQuery 实例挂载:
      • 由于这是第一次挂载,没有缓存也没有之前的请求,因此会立即请求接口数据,并且查询函数将接收 queryKey 作为 QueryFunctionContext 的一部分。
      • 数据获取成功后,数据将缓存在 [{ queryIdentifier: "api", apiName: apiA }]queryKey 下。
      • 由于我们假设默认的 staleTime 为 0,因此钩子会将其数据标记为过期数据。
  • 当组件 A 渲染组件 B 时:

    • 一个带有 [{ queryIdentifier: "api", apiName: apiB }] queryKey 的 useQuery 实例被挂载:
      • 由于这是第一次挂载,既没有缓存也没有之前的请求,因此会立即请求数据。
      • 数据获取成功后,数据将缓存在 [{ queryIdentifier: "api", apiName: apiB }] queryKey 下,钩子将调用 onSuccess 函数。
      • 由于我们假设默认的 staleTime 为 0,因此钩子会将其数据标记为过期数据。
  • 当组件 B 渲染组件 C 时:

    • 带有 [{ queryIdentifier: "api", apiName: apiA }] queryKey 的 useQuery 实例挂载:
      • 由于该钩子与组件 A 中的钩子具有相同的 queryKey,钩子下已经有缓存数据,因此可以立即访问数据。
      • 由于该查询在上次取回后被标记为过期,因此该钩子需要重新取回,但它需要先等待该查询的 enabled 条件变为 true。
      • 一旦 enabled 为 true,查询就会触发重新获取。这使得组件 A 和组件 C 上的 isFetching 都为 true。
      • 一旦获取请求成功,数据将被缓存在 [{ queryIdentifier: "api", apiName: apiA }] 查询键下,并且查询再次被标记为过期。
  • 再来看一下组件 A 卸载的情况:

    • 由于不再有任何使用 [{ queryIdentifier: "api", apiName: apiA }] queryKey 的查询实例处于活跃状态,默认的缓存超时时间为 5 分钟,5 分钟后,该查询下的数据就会被删除并被垃圾回收。

    • 由于不再有任何使用 [{ queryIdentifier: "api", apiName: apiB }] queryKey 的查询实例处于活跃状态,默认缓存超时时间为 5 分钟,5 分钟后,该查询下的数据就会被删除并被垃圾回收。

如果你能在查询使用过程中跟踪前面的过程和查询的生命周期,那么恭喜你:你已经基本解了 useQuery 的工作原理!


相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang1 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
小牛itbull3 小时前
ReactPress:构建高效、灵活、可扩展的开源发布平台
react.js·开源·reactpress
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js