【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 的工作原理!


相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax