掘金 后端 ( ) • 2024-04-07 13:54

查询:使用useQuery钩子获取数据

本文展示了如何在React中使用useQuery钩子获取GraphQL数据,并将结果附加到您的UI上。您还将了解Apollo Client如何通过为您跟踪错误和加载状态来简化数据管理代码。

先决条件

本文假设您熟悉构建基本的GraphQL查询。如果需要复习,请参考这个指南。您也可以对Apollo的全栈教程服务器构建示例查询。

本文还假设您已经设置了Apollo Client,并且已经用ApolloProvider组件包裹了您的React应用。更多信息,请参见入门指南。

要跟随下面的示例,请在CodeSandbox上打开我们的入门项目和示例GraphQL服务器。您可以在这里查看完成的应用。

执行查询

useQuery React钩子是在Apollo应用程序中执行查询的主要API。要在React组件中运行查询,请调用useQuery并传递一个GraphQL查询字符串。当您的组件渲染时,useQuery返回一个来自Apollo Client的包含loadingerrordata属性的对象,您可以使用这些属性来渲染您的UI。

注意:在Apollo Client >= 3.8中,暂停(Suspense)数据获取钩子可用于在React 18的新并发渲染模型中使用<Suspense />边界查询数据。更多信息请参见Apollo Client的暂停文档。

让我们看一个示例。首先,我们将创建一个名为GET_DOGS的GraphQL查询。记得用gql函数包裹查询字符串来解析它们为查询文档:

jsxCopy code
// index.js
import { gql, useQuery } from '@apollo/client';

const GET_DOGS = gql`
  query GetDogs {
    dogs {
      id
      breed
    }
  }
`;

接下来,我们将创建一个名为Dogs的组件。在其中,我们将把我们的GET_DOGS查询传递给useQuery钩子:

jsxCopy code
// index.js
function Dogs({ onDogSelected }) {
  const { loading, error, data } = useQuery(GET_DOGS);

  if (loading) return 'Loading...';
  if (error) return `Error! ${error.message}`;

  return (
    <select name='dog' onChange={onDogSelected}>
      {data.dogs.map((dog) => (
        <option key={dog.id} value={dog.breed}>
          {dog.breed}
        </option>
      ))}
    </select>
  );
}

随着查询的执行和loadingerrordata的值的变化,Dogs组件可以智能地根据查询的状态渲染不同的UI元素:

  • 只要loading为true(表示查询仍在进行中),组件呈现一个Loading...通知。
  • loading为false且没有错误时,查询已完成。组件呈现一个下拉菜单,菜单中填充了服务器返回的狗品种列表。
  • 当用户从下拉菜单中选择一个狗品种时,通过提供的onDogSelected函数将选择发送到父组件。

在下一步中,我们将把下拉菜单与一个更复杂的查询关联,该查询使用GraphQL变量。

缓存查询结果

每当Apollo Client从您的服务器获取查询结果时,它会自动在本地缓存这些结果。这使得同一查询的后续执行变得非常快速。

为了看到这种缓存的实际效果,让我们构建一个名为DogPhoto的新组件。DogPhoto接受一个名为breed的属性,该属性反映了我们Dogs组件中下拉菜单的当前值:

jsxCopy code
// index.js
const GET_DOG_PHOTO = gql`
  query Dog($breed: String!) {
    dog(breed: $breed) {
      id
      displayImage
    }
  }
`;

function DogPhoto({ breed }) {
  const { loading, error, data } = useQuery(GET_DOG_PHOTO, {
    variables: { breed },
  });

  if (loading) return null;
  if (error) return `Error! ${error}`;

  return (
    <img src={data.dog.displayImage} style={{ height: 100, width: 100 }} />
  );
}

请注意,这次我们向useQuery钩子提供了一个配置选项(variables)。variables选项是一个包含所有我们想要传递给GraphQL查询的变量的对象。在这种情况下,我们想要传递下拉菜单中当前选择的品种。

从下拉菜单中选择bulldog以查看它的照片。然后切换到另一个品种,然后再切换回bulldog。您会注意到第二次bulldog照片立即加载。这就是缓存在起作用!

接下来,让我们学习一些确保我们的缓存数据保持最新的技术。

更新缓存的查询结果

有时,您希望确保您的查询的缓存数据与您的服务器的数据保持最新。Apollo Client支持两种策略来实现这一点:轮询和重新获取。

轮询

轮询通过在指定间隔定期执行您的查询,提供与服务器几乎实时的同步。要为查询启用轮询,请向useQuery钩子传递一个pollInterval配置选项,并以毫秒为单位指定间隔:

jsxCopy code
// index.js
function DogPhoto({ breed }) {
  const { loading, error, data } = useQuery(GET_DOG_PHOTO, {
    variables: { breed },
    pollInterval: 500,
  });

  if (loading) return null;
  if (error) return `Error! ${error}`;

  return (
    <img src={data.dog.displayImage} style={{ height: 100, width: 100 }} />
  );
}

通过将pollInterval设置为500,我们每0.5秒从服务器获取当前品种的图片。注意,如果您将pollInterval设置为0,则查询不进行轮询。

您还可以使用useQuery钩子返回的startPollingstopPolling函数来动态地开始和停止轮询。使用这些函数时,将pollInterval配置选项作为startPolling函数的参数设置。

重新获取

重新获取使您能够响应特定用户操作来刷新查询结果,而不是使用固定间隔。

让我们在DogPhoto组件中添加一个按钮,每当点击时调用查询的refetch函数。

您还可以向refetch函数提供一个新的variables对象。如果您只使用refetch()而不传递variables对象,查询将使用其先前执行中使用的相同变量。

jsxCopy code
// index.js
function DogPhoto({ breed }) {
  const { loading, error, data, refetch } = useQuery(GET_DOG_PHOTO, {
    variables: { breed },
  });

  if (loading) return null;
  if (error) return `Error! ${error}`;

  return (
    <div>
      <img src={data.dog.displayImage} style={{ height: 100, width: 100 }} />
      <button onClick={() => refetch()}>
        Refetch new breed!
      </button>
    </div>
  );
}

点击按钮并注意到UI用一张新的狗照片更新。重新获取是确保数据最新的绝佳方式,但它在加载状态方面引入了一些复杂性。在下一节中,我们将介绍处理复杂加载和错误状态的策略。

向重新获取提供新变量(Refetching)

您可以像这样调用refetch并提供一组新变量:

jsxCopy code
<button
  onClick={() =>
    refetch({
      breed: 'dalmatian', // 总是重新获取达尔马提亚品种而不是原始品种
    })
  }
>
  Refetch!
</button>

如果您为初始查询的变量中的一些变量提供了新值,但没有提供所有变量,则refetch使用每个省略变量的原始值。

检查加载状态(loading)

我们已经看到useQuery钩子如何公开我们查询的当前加载状态。这在查询首次加载时很有帮助,但当我们重新获取或轮询时,加载状态会发生什么?

让我们回到前一节的重新获取示例。如果您点击重新获取按钮,您会看到组件在新数据到达之前不会重新渲染。如果我们想告诉用户我们正在重新获取照片,该怎么办?

useQuery钩子的结果对象通过networkStatus属性提供了有关查询关联请求当前网络状态的细节信息。要利用这些信息,我们需要将notifyOnNetworkStatusChange选项设置为true,以便我们的查询组件在重新获取进行中时重新渲染:

jsxCopy code
// index.js
import { NetworkStatus } from '@apollo/client';

function DogPhoto({ breed }) {
  const { loading, error, data, refetch, networkStatus } = useQuery(
    GET_DOG_PHOTO,
    {
      variables: { breed },
      notifyOnNetworkStatusChange: true,
    }
  );

  if (networkStatus === NetworkStatus.refetch) return 'Refetching!';
  if (loading) return null;
  if (error) return `Error! ${error}`;

  return (
    <div>
      <img src={data.dog.displayImage} style={{ height: 100, width: 100 }} />
      <button onClick={() => refetch()}>
        Refetch!
      </button>
    </div>
  );
}

启用这个选项也确保了loading的值相应地更新,即使您不想使用networkStatus属性提供的更细粒度的信息。

networkStatus属性是表示查询关联请求网络状态的不同加载状态的NetworkStatus枚举。重新获取由NetworkStatus.refetch表示,还有轮询和分页的值。有关所有可能的加载状态的完整列表,请查看源代码。

要查看我们刚刚构建的应用的完整版本,请查看CodeSandbox这里。

检查错误状态(error)

您可以通过向useQuery钩子提供errorPolicy配置选项来自定义查询错误处理。默认值是none,这告诉Apollo Client将所有GraphQL错误视为运行时错误。在这种情况下,Apollo Client丢弃服务器返回的任何查询响应数据,并在useQuery结果对象中设置error属性。

如果您将errorPolicy设置为alluseQuery不会丢弃查询响应数据,允许您呈现部分结果。

更多信息,请参见处理操作错误。

使用useLazyQuery手动执行

当React渲染调用useQuery的组件时,Apollo Client会自动执行相应的查询。但是,如果您希望响应除组件渲染之外的不同事件来执行查询,比如用户点击按钮?

useLazyQuery钩子非常适合在除组件渲染之外的事件响应中执行查询。与useQuery不同,当您调用useLazyQuery时,它不会立即执行其关联的查询。相反,它在其结果元组中返回一个查询函数,您可以在准备好执行查询时调用该函数。

以下是一个示例:

jsxCopy code
// index.js
import React from 'react';
import { useLazyQuery } from '@apollo/client';

function DelayedQuery() {
  const [getDog, { loading, error, data }] = useLazyQuery(GET_DOG_PHOTO);

  if (loading) return <p>Loading ...</p>;
  if (error) return `Error! ${error}`;

  return (
    <div>
      {data?.dog && <img src={data.dog.displayImage} />}
      <button onClick={() => getDog({ variables: { breed: 'bulldog' } })}>
        Click me!
      </button>
    </div>
  );
}

useLazyQuery返回元组的第一项是查询函数,第二项是useQuery返回的相同结果对象。

如上所示,您可以像将它们传递给useLazyQuery本身一样,将选项传递给查询函数。如果您同时向两者传递一个特定选项,传递给查询函数的值优先。这是一种方便的方式,将默认选项传递给useLazyQuery,然后在查询函数中自定义这些选项。

ⓘ 注意

变量通过获取传递给钩子的变量并将它们与传递给查询函数的变量合并来合并。如果您没有向查询函数传递变量,则只在查询执行中使用传递给钩子的变量。

有关支持的选项的完整列表,请参见API参考。

设置获取策略(fetching policy)

默认情况下,useQuery钩子在执行时首先检查Apollo Client缓存,以查看您请求的所有数据是否已经在本地可用。如果所有数据都在本地可用,useQuery返回这些数据,并且不会查询您的GraphQL服务器。这种cache-first策略是Apollo Client的默认获取策略。

您可以为给定查询指定不同的获取策略。要这样做,请在调用useQuery时包含fetchPolicy选项:

jsxCopy code
const { loading, error, data } = useQuery(GET_DOGS, {
  fetchPolicy: 'network-only', // 不检查缓存就发送网络请求
});

nextFetchPolicy自3.1起 您还可以指定查询的nextFetchPolicy。如果您这样做,fetchPolicy用于查询的第一次执行,nextFetchPolicy用于确定如何响应以后的缓存更新:

jsxCopy code
const { loading, error, data } = useQuery(GET_DOGS, {
  fetchPolicy: 'network-only', // 用于第一次执行
  nextFetchPolicy: 'cache-first', // 用于后续执行
});

例如,这在您希望查询始终先进行初始网络请求,但之后您满意从缓存中读取的情况下很有帮助。

nextFetchPolicy函数 如果您希望通过默认应用单个nextFetchPolicy,因为您发现自己手动为大多数查询提供nextFetchPolicy,您可以在创建ApolloClient实例时配置defaultOptions.watchQuery.nextFetchPolicy

jsxCopy code
new ApolloClient({
  link,
  client,
  defaultOptions: {
    watchQuery: {
      nextFetchPolicy: 'cache-only',
    },
  },
});

这种配置适用于所有client.watchQuery调用和未另行配置nextFetchPolicyuseQuery调用。

如果您希望对nextFetchPolicy的行为有更多控制权,可以提供一个函数而不是WatchQueryFetchPolicy字符串:

jsxCopy code
new ApolloClient({
  link,
  client,
  defaultOptions: {
    watchQuery: {
      nextFetchPolicy(currentFetchPolicy) {
        if (
          currentFetchPolicy === 'network-only' ||
          currentFetchPolicy === 'cache-and-network'
        ) {
          // 在第一次请求后将网络策略(除了"no-cache")降级为"cache-first"
          return 'cache-first';
        }
        // 保留所有其他获取策略不变。
        return currentFetchPolicy;
      },
    },
  },
});

这个nextFetchPolicy函数将在每次请求后调用,并使用currentFetchPolicy参数来决定如何修改获取策略。

除了每次请求后调用外,当变量更改时也会调用您的nextFetchPolicy函数,这通常会将options.fetchPolicy重置为其初始值,这对于触发以cache-and-networknetwork-only获取策略开始的查询的新网络请求很重要。

要拦截并处理变量更改的情况,您可以使用作为第二参数传递给nextFetchPolicy函数的NextFetchPolicyContext对象:

jsxCopy code
new ApolloClient({
  link,
  client,
  defaultOptions: {
    watchQuery: {
      nextFetchPolicy(
        currentFetchPolicy,
        {
          // 为"after-fetch"或"variables-changed",表示为何调用`nextFetchPolicy`函数。
          reason,
          // 其余选项(currentFetchPolicy === options.fetchPolicy)。
          options,
          // `options.fetchPolicy`首次应用`nextFetchPolicy`之前的原始值。
          initialPolicy,
          // 与此`client.watchQuery`调用关联的`ObservableQuery`。
          observable,
        }
      ) {
        // 当变量更改时,默认行为是将`options.fetchPolicy`重置为`context.initialPolicy`。如果省略这个逻辑,
        // 您的`nextFetchPolicy`函数可以覆盖此默认行为以防止`options.fetchPolicy`在这种情况下改变。
        if (reason === 'variables-changed') {
          return initialPolicy;
        }

        if (
          currentFetchPolicy === 'network-only' ||
          currentFetchPolicy === 'cache-and-network'
        ) {
          // 在第一次请求后将网络策略(除了"no-cache")降级为"cache-first"。
          return 'cache-first';
        }

        // 保留所有其他获取策略不变。
        return currentFetchPolicy;
      },
    },
  },
});

为了调试这些nextFetchPolicy转换,向函数体中添加console.logdebugger语句会很有用,以了解何时以及为何调用该函数。

支持的获取策略

  • cache-first:

    1. Apollo Client首先对缓存执行查询。如果所有请求的数据都在缓存中,该数据被返回。否则, Apollo Client会对GraphQL服务器执行查询,并在缓存它之后返回该数据。
    2. 优先考虑最小化您的应用程序发送的网络请求数量。
    3. 这是默认的获取策略。
  • cache-only

    1. Apollo Client只对缓存执行查询。在这种情况下,它从不查询服务器。
    2. 如果缓存不包含所有请求字段的数据,则只有缓存查询会抛出错误。
  • cache-and-network

    1. Apollo Client对缓存和GraphQL服务器同时执行完整查询。如果服务器端查询的结果修改了缓存字段,查询会自动更新。
    2. 在提供快速响应的同时,也有助于保持缓存数据与服务器数据一致。
  • network-only

    1. Apollo Client不检查缓存,而是直接对GraphQL服务器执行完整查询。查询结果存储在缓存中。

    2. 优先考虑与服务器数据的一致性,但无法在缓存数据可用时提供几乎即时的响应。

  • no-cache 与network-only类似,但查询结果不存储在缓存中。

  • standby 使用与cache-first相同的逻辑,除了该查询不会在底层字段值更改时自动更新。您仍然可以通过refetch和updateQueries手动更新此查询。