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

使用React 18的Suspense特性与Apollo Client

"Suspense"通常用来指一种使用React 18引入的并发渲染引擎构建React应用的新方式。它也是一个具体的React API,<Suspense />,一个组件,可以让你展示一个替代内容,直到它的子元素加载完成。

本指南探索了Apollo Client在3.8版本中引入的数据获取钩子,这些钩子利用了React强大的Suspense特性。

要跟随下面的示例,请在CodeSandbox上打开我们的Suspense演示。

使用Suspense获取数据

useSuspenseQuery钩子发起一个网络请求,并使调用它的组件在请求进行时暂停。你可以将它视为useQuery的替代品,它让你在渲染期间获取数据时利用React的Suspense特性。

让我们看一个示例:

jsxCopy code
import { Suspense } from 'react';
import {
  gql,
  TypedDocumentNode,
  useSuspenseQuery
} from '@apollo/client';

interface Data {
  dog: {
    id: string;
    name: string;
  };
}

interface Variables {
  id: string;
}

interface DogProps {
  id: string
}

const GET_DOG_QUERY: TypedDocumentNode<Data, Variables> = gql`
  query GetDog($id: String) {
    dog(id: $id) {
      id
      name
      breed
    }
  }
`;

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Dog id="3" />
    </Suspense>
  );
}

function Dog({ id }: DogProps) {
  const { data } = useSuspenseQuery(GET_DOG_QUERY, {
    variables: { id },
  });

  return <>Name: {data.dog.name}</>;
}

ⓘ 注意

此示例手动为Data和Variables以及GET_DOG_QUERY的类型定义了TypeScript接口,使用TypedDocumentNode。GraphQL Code Generator是一种流行的工具,它会为你自动生成这些类型定义。有关与Apollo Client结合使用TypeScript的更多信息,请参阅使用TypeScript的参考资料。

在这个示例中,我们的App组件渲染了一个Dog组件,该组件通过useSuspenseQuery获取单只狗的记录。当React首次尝试渲染Dog时,缓存无法满足对GetDog查询的请求,因此useSuspenseQuery发起了一个网络请求。Dog在网络请求进行时暂停,触发了App中暂停组件上方最近的Suspense边界,它渲染了我们的"Loading..."替代内容。一旦网络请求完成,Dog使用新缓存的Mozzarella the Corgi的名字进行渲染。

你可能已经注意到,useSuspenseQuery没有返回loading布尔值。这是因为调用useSuspenseQuery的组件在获取数据时总是暂停。一个推论是,当它渲染时,数据总是已定义的!在Suspense范式中,存在于暂停组件之外的替代内容取代了组件之前负责渲染自身的加载状态。

ⓘ 注意

对于TypeScript用户:由于GET_DOG_QUERY是我们通过Data泛型类型参数指定了结果类型的TypedDocumentNode,useSuspenseQuery返回的data类型反映了这一点!这意味着当Dog渲染时,data保证已定义,并且data.dog的形状为{id: string; name: string; breed: string;}。

更改变量

在前面的示例中,我们通过向useSuspenseQuery传递一个硬编码的id变量来获取单只狗的记录。现在,假设我们想用动态值获取另一只狗的记录。我们将获取我们狗列表的name和id,一旦用户选择了一只狗,我们就获取更多细节,包括它们的品种。

让我们更新我们的示例:

jsxCopy code
export const GET_DOG_QUERY: TypedDocumentNode<
  DogData,
  Variables
> = gql`
  query GetDog($id: String) {
    dog(id: $id) {
      id
      name
      breed
    }
  }
`;

export const GET_DOGS_QUERY: TypedDocumentNode<
  DogsData,
  Variables
> = gql`
  query GetDogs {
    dogs {
      id
      name
    }
  }
`;

function App() {
  const { data } = useSuspenseQuery(GET_DOGS_QUERY);
  const [selectedDog, setSelectedDog] = useState(
    data.dogs[0].id
  );

  return (
    <>
      <select
        onChange={(e) => setSelectedDog(e.target.value)}
      >
        {data.dogs.map(({ id, name }) => (
          <option key={id} value={id}>{name}</option>
        ))}
      </select>
      <Suspense fallback={<div>Loading...</div>}>
        <Dog id={selectedDog} />
      </Suspense>
    </>
  );
}

function Dog({ id }: DogProps) {
  const { data } = useSuspenseQuery(GET_DOG_QUERY, {
    variables: { id },
  });

  return (
    <>
      <div>Name: {data.dog.name}</div>
      <div>Breed: {data.dog.breed}</div>
    </>
  );
}

通过下拉菜单更改狗的选择会导致每次我们选择尚未存在于缓存中的狗时,组件都会暂停。一旦我们在缓存中加载了给定狗的记录,再次从下拉菜单中选择该狗不会导致组件重新暂停,因为在默认的cache-first获取策略下,Apollo Client在缓存命中后不会进行网络请求。

不暂停时更新状态

有时我们可能希望避免在等待网络请求的响应时显示加载UI,而是更愿意继续显示之前的渲染。为此,我们可以使用过渡来标记我们的更新为非紧急的。这告诉React在新数据加载完成之前保持现有UI。

要将状态更新标记为过渡,我们使用来自React的startTransition函数。

让我们修改我们的示例,以便在过渡中获取下一只狗时,之前显示的狗保持在屏幕上:

jsxCopy code
import { useState, Suspense, startTransition } from "react";

function App() {
  const { data } = useSuspenseQuery(GET_DOGS_QUERY);
  const [selectedDog, setSelectedDog] = useState(
    data.dogs[0].id
  );

  return (
    <>
      <select
        onChange={(e) => {
          startTransition(() => {
            setSelectedDog(e.target.value);
          });
        }}
      >
        {data.dogs.map(({ id, name }) => (
          <option key={id} value={id}>{name}</option>
        ))}
      </select>
      <Suspense fallback={<div>Loading...</div>}>
        <Dog id={selectedDog} />
      </Suspense>
    </>
  );
}

通过将我们的setSelectedDog状态更新包装在React的startTransition函数中,我们在选择新狗时不再看到Suspense替代内容!相反,直到下一只狗的记录加载完成,之前的狗仍然在屏幕上。

过渡期间显示待处理UI

在上一个示例中,当选择新狗时没有视觉提示表明正在进行获取。为了提供良好的视觉反馈,让我们更新我们的示例,使用React的useTransition钩子,它给你一个isPending布尔值来确定何时发生过渡。

让我们在过渡发生时将下拉菜单变暗:

jsxCopy code
import { useState, Suspense, useTransition } from "react";

function App() {
  const [isPending, startTransition] = useTransition();
  const { data } = useSuspenseQuery(GET_DOGS_QUERY);
  const [selectedDog, setSelectedDog] = useState(
    data.dogs[0].id
  );

  return (
    <>
      <select
        style={{ opacity: isPending ? 0.5 : 1 }}
        onChange={(e) => {
          startTransition(() => {
            setSelectedDog(e.target.value);
          });
        }}
      >
        {data.dogs.map(({ id, name }) => (
          <option key={id} value={id}>{name}</option>
        ))}
      </select>
      <Suspense fallback={<div>Loading...</div>}>
        <Dog id={selectedDog} />
      </Suspense>
    </>
  );
}

渲染部分数据

当缓存包含部分数据时,您可能更愿意立即渲染这些数据,而不是暂停。为此,请使用returnPartialData选项。

ⓘ 注意

此选项仅在与cache-first(默认)或cache-and-network获取策略结合时有效。cache-only目前不受useSuspenseQuery支持。有关这些获取策略的详细信息,请参见设置获取策略。

让我们更新我们的示例以使用部分缓存数据并立即渲染:

jsxCopy code
interface PartialData {
  dog: {
    id: string;
    name: string;
  };
}

const PARTIAL_GET_DOG_QUERY: TypedDocumentNode<
  PartialData,
  Variables
> = gql`
  query GetDog($id: String) {
    dog(id: $id) {
      id
      name
    }
  }
`;

// 将部分数据写入Buck的缓存
// 以便Dog渲染时可以使用
client.writeQuery({
  query: PARTIAL_GET_DOG_QUERY,
  variables: { id: "1" },
  data: { dog: { id: "1", name: "Buck" } },
});

function App() {
  const client = useApolloClient();

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Dog id="1" />
    </Suspense>
  );
}

function Dog({ id }: DogProps) {
  const { data } = useSuspenseQuery(GET_DOG_QUERY, {
    variables: { id },
    returnPartialData: true,
  });

  return (
    <>
      <div>Name: {data?.dog?.name}</div>
      <div>Breed: {data?.dog?.breed}</div>
    </>
  );
}

在此示例中,我们为Buck写入部分数据到缓存中,以展示当无法完全从缓存中满足查询时的行为。我们通过将returnPartialData选项设置为true告诉useSuspenseQuery,我们可以接受渲染部分数据。当Dog首次渲染时,它不会暂停并立即使用部分数据。Apollo Client随后在后台从网络获取缺失的查询数据。

在首次渲染时,在Name标签后显示Buck的名字,紧接着是Breed标签没有值。一旦缺失的字段加载完毕,useSuspenseQuery触发重新渲染,Buck的品种显示出来。

ⓘ 注意

对于TypeScript用户:将returnPartialData设置为true时,返回的data属性的类型将查询类型中的所有字段标记为可选。Apollo Client在返回部分数据时无法准确确定缓存中哪些字段存在。

错误处理

默认情况下,useSuspenseQuery会抛出网络错误和GraphQL错误。这些错误被最近的错误边界捕获并显示。

ⓘ 注意

错误边界是实现了static getDerivedStateFromError的类组件。有关使用错误边界捕获渲染错误的更多信息,请参阅React文档。

让我们创建一个基本的错误边界,在我们的查询抛出错误时渲染错误UI:

jsxCopy code
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }

    return this.props.children;
  }
}

ⓘ 注意

在真实应用中,您的错误边界可能需要更健壮的实现。当您需要高度灵活性和可重用性时,请考虑使用类似react-error-boundary的库。

当Dog组件中的GET_DOG_QUERY返回GraphQL错误或网络错误时,useSuspenseQuery抛出错误,最近的错误边界渲染其提供的替代组件。

我们的示例还没有错误边界 - 让我们添加一个!

jsxCopy code
function App() {
  const { data } = useSuspenseQuery(GET_DOGS_QUERY);
  const [selectedDog, setSelectedDog] = useState(
    data.dogs[0].id
  );

  return (
    <>
      <select
        onChange={(e) => setSelectedDog(e.target.value)}
      >
        {data.dogs.map(({ id, name }) => (
          <option key={id} value={id}>
            {name}
          </option>
        ))}
      </select>
      <ErrorBoundary
        fallback={<div>Something went wrong</div>}
      >
        <Suspense fallback={<div>Loading...</div>}>
          <Dog id={selectedDog} />
        </Suspense>
      </ErrorBoundary>
    </>
  );
}

在这里,我们使用了我们的ErrorBoundary组件,并将其放在Dog组件外部。现在,当Dog组件中的useSuspenseQuery钩子抛出错误时,ErrorBoundary捕获它并显示我们提供的替代元素。

ⓘ 注意

在许多React框架中,即使有错误边界,当组件中抛出错误时,您可能会在开发模式中看到一个错误对话框覆盖层。这样做是为了避免开发者错过错误。

在错误旁边渲染部分数据

在某些情况下,您可能希望在错误旁边渲染部分数据。为此,请将errorPolicy选项设置为all。通过设置此选项,useSuspenseQuery避免抛出错误,而是设置了钩子返回的错误属性。要完全忽略错误,请将errorPolicy设置为ignore。有关更多信息,请参见errorPolicy文档。

避免请求瀑布

由于useSuspenseQuery在数据获取时会暂停,因此全部使用useSuspenseQuery的组件树可能会导致“瀑布”,其中每个对useSuspenseQuery的调用都依赖于之前的完成才能开始获取。这可以通过使用useBackgroundQuery进行获取,并使用useReadQuery读取数据来避免。

useBackgroundQuery在父组件中启动数据请求,并返回一个queryRef,该queryRef传递给useReadQuery,在子组件中读取数据。当子组件在数据加载完成前渲染时,子组件会暂停。

让我们更新我们的示例以利用useBackgroundQuery:

jsxCopy code
import {
  useBackgroundQuery,
  useReadQuery,
  useSuspenseQuery,
} from '@apollo/client';

function App() {
  // 即使`Dog`暂停并且数据由孙子组件读取,我们也可以在这里开始请求。
  const [queryRef] = useBackgroundQuery(GET_BREEDS_QUERY);

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Dog id="3" queryRef={queryRef} />
    </Suspense>
  );
}

function Dog({ id, queryRef }: DogProps) {
  const { data } = useSuspenseQuery(GET_DOG_QUERY, {
    variables: { id },
  });

  return (
    <>
      Name: {data.dog.name}
      <Suspense fallback={<div>Loading breeds...</div>}>
        <Breeds queryRef={queryRef} />
      </Suspense>
    </>
  );
}

interface BreedsProps {
  queryRef: QueryReference<BreedData>;
}

function Breeds({ queryRef }: BreedsProps) {
  const { data } = useReadQuery(queryRef);

  return data.breeds.map(({ characteristics }) =>
    characteristics.map((characteristic) => (
      <div key={characteristic}>{characteristic}</div>
    ))
  );
}

我们在App组件渲染时开始获取GET_BREEDS_QUERY。网络请求在后台进行,而React渲染我们的其余组件树。当Dog组件渲染时,它获取我们的GET_DOG_QUERY并暂停。

当GET_DOG_QUERY的网络请求完成时,Dog组件停止暂停并继续渲染,达到Breeds组件。由于我们的GET_BREEDS_QUERY请求是在组件树更高处使用useBackgroundQuery启动的,GET_BREEDS_QUERY的网络请求已经完成!当Breeds组件使用useBackgroundQuery提供的queryRef读取数据时,它避免暂停并立即用获取到的数据渲染。

ⓘ 注意

当GET_BREEDS_QUERY的获取时间比我们的GET_DOG_QUERY长时,useReadQuery会暂停,而Dog组件内的Suspense替代内容会被渲染。

关于性能的说明

在父组件中使用的useBackgroundQuery钩子负责启动获取,但不处理读取或渲染数据。这被委派给子组件中使用的useReadQuery钩子。这种分离关注点提供了一个很好的性能优势,因为缓存更新被useReadQuery观察,并且只重新渲染子组件。当缓存数据更改时,您可能会发现这是一个有用的工具,用于优化组件结构,避免不必要地重新渲染父组件。

根据用户交互获取数据自3.9.0起

useSuspenseQuery和useBackgroundQuery钩子让我们在调用钩子的组件挂载时立即加载数据。但是,如何在响应用户交互时加载查询?例如,我们可能希望在用户悬停在链接上时开始加载一些数据。

自Apollo Client 3.9.0起,useLoadableQuery钩子响应用户交互发起网络请求。

useLoadableQuery返回一个执行函数和一个queryRef。执行函数在使用提供的变量调用时发起网络请求。与useBackgroundQuery一样,将queryRef传递给子组件中的useReadQuery在查询完成之前暂停子组件。

ⓘ 注意

直到第一次调用执行函数时,queryRef才不为null。因此,任何尝试使用queryRef读取数据的子组件都应该有条件地渲染。

让我们更新我们的示例,以便通过下拉菜单选择狗的结果开始加载狗的细节。

jsxCopy code
import {
  // ...
  useLoadableQuery
} from '@apollo/client';

function App() {
  const { data } = useSuspenseQuery(GET_DOGS_QUERY);
  const [loadDog, queryRef] = useLoadableQuery(GET_DOG_QUERY);

  return (
    <>
      <select
        onChange={(e) => loadDog({ id: e.target.value })}
      >
        {data.dogs.map(({ id, name }) => (
          <option key={id} value={id}>
            {name}
          </option>
        ))}
      </select>
      <Suspense fallback={<div>Loading...</div>}>
        {queryRef && <Dog queryRef={queryRef} />}
      </Suspense>
    </>
  );
}

function Dog({ queryRef }: DogProps) {
  const { data } = useReadQuery(queryRef)

  return (
    <>
      <div>Name: {data.dog.name}</div>
      <div>Breed: {data.dog.breed}</div>
    </>
  );
}

我们通过在onChange处理函数中调用loadDog函数开始获取GET_DOG_QUERY,当选择一只狗时启动网络请求。一旦网络请求启动,queryRef就不再为null,渲染Dog组件。

useReadQuery在网络请求完成时暂停Dog组件,然后将数据返回给组件。由于这种改变,我们也消除了跟踪所选狗id组件状态的需要。

在React之外启动查询自3.9.0起

此功能在3.9.0版本中处于alpha阶段,并且在3.10.0之前可能会有变化。我们认为此功能已经准备好投入生产,但可能会根据反馈而改变。如果您想在3.10.0稳定之前提供反馈,请在#11519上发表评论。

从Apollo Client 3.9.0开始,在React之外可以启动查询。这允许您的应用在React渲染您的组件之前开始获取数据,可以提供性能优势。

要预加载查询,您首先需要使用createQueryPreloader创建一个预加载函数。createQueryPreloader以ApolloClient实例作为参数,并返回一个函数,当调用时,发起网络请求。

💡 提示

考虑将您的预加载函数与您的ApolloClient实例一起导出。这允许您直接导入该函数,而无需每次预加载查询时都传递您的ApolloClient实例。

预加载函数返回一个queryRef,该queryRef传递给useReadQuery以读取查询数据并在加载时暂停组件。useReadQuery确保您的组件与预加载查询的缓存更新保持同步。

让我们更新我们的示例,以便在渲染我们的组件之前开始加载GET_DOGS_QUERY。

jsxCopy code
import {
  // ...
  createQueryPreloader
} from '@apollo/client';

// 这个`preloadQuery`函数不需要在您每次需要预加载新查询时都创建。您可能更愿意将此函数与您的客户端一起导出。
const preloadQuery = createQueryPreloader(client);
const preloadedQueryRef = preloadQuery(GET_DOGS_QUERY);

function App() {
  const { data } = useReadQuery(preloadedQueryRef);
  const [queryRef, loadDog] = useLoadableQuery(GET_DOG_QUERY)

  return (
    <>
      <select
        onChange={(e) => loadDog({ id: e.target.value })}
      >
        {data.dogs.map(({ id, name }) => (
          <option key={id} value={id}>{name}</option>
        ))}
      </select>
      <Suspense fallback={<div>Loading...</div>}>
        <Dog queryRef={queryRef} />
      </Suspense>
    </>
  );
}

const root = createRoot(document.getElementById('app'));

root.render(
  <ApolloProvider client={client}>
    <Suspense fallback={<div>Loading...</div>}>
      <App />
    </Suspense>
  </ApolloProvider>
);

我们在调用preloadQuery函数时开始加载数据,而不是等待React渲染我们的组件。因为preloadedQueryRef被传递给我们App组件中的useReadQuery,我们仍然获得暂停和缓存更新的好处。

ⓘ 注意

卸载包含预加载查询的组件是安全的,并且会处理掉queryRef。当组件重新挂载时,useReadQuery会自动重新订阅queryRef,并且在此期间发生的任何缓存更新会立即读取,就好像预加载查询从未被卸载一样。

与数据加载路由器的使用

诸如React Router和TanStack Router等流行路由器提供了API,在渲染路由组件之前加载数据。一个例子是React Router的loader函数。

在嵌套路由中,数据加载是并行化的,所以在渲染路由组件之前加载数据特别有用。它可以防止父路由组件可能会暂停并为子路由组件创建请求瀑布的情况。

preloadQuery与这些路由器API非常搭配,因为它让您在不牺牲路由组件中重新渲染缓存更新的能力的情况下利用这些优化。

让我们使用React Router的loader函数更新我们的示例,以便在转换到我们的路由时开始加载数据。

jsxCopy code
import { useLoaderData } from 'react-router-dom';

export function loader() {
  return preloadQuery(GET_DOGS_QUERY);
}

export function RouteComponent() {
  const queryRef = useLoaderData();
  const { data } = useReadQuery(queryRef);

  return (
    // ...
  );
}

React Router v6.4及以上版本提供了loader函数。

React Router调用loader函数,我们用它来通过调用preloadQuery函数开始加载GET_DOG_QUERY查询。由preloadQuery创建的queryRef从loader函数返回,使其在路由组件中可访问。

当路由组件渲染时,我们通过useLoaderData钩子访问queryRef,然后将其传递给useReadQuery。我们在路由生命周期早期加载我们的数据,并且路由组件保持了与缓存更新一起重新渲染的能力。

ⓘ 注意

preloadQuery函数仅适用于客户端路由。从preloadQuery返回的queryRef不是可串行化的,因此不适用于在服务器上获取的路由器,如Remix。

防止查询加载完成之前的路由转换

默认情况下,preloadQuery的工作方式类似于延迟加载器:路由立即转换,并且尝试通过useReadQuery读取数据的传入页面在网络请求完成之前暂停。

但是,如果我们希望在数据完全加载之前阻止路由转换怎么办?queryRef的toPromise方法提供了访问一个promise,该promise在网络请求完成时解析。这个promise用queryRef本身解析,使其与useLoaderData等钩子一起使用变得容易。

以下是一个示例:

jsxCopy code
export async function loader() {
  const queryRef = await preloadQuery(GET_DOGS_QUERY).toPromise();

  return queryRef;
}

// 您还可以直接从loader返回promise。
// 这与上面是等价的。
export async function loader() {
  return preloadQuery(GET_DOGS_QUERY).toPromise();
}

export function RouteComponent() {
  const queryRef = useLoaderData();
  const { data } = useReadQuery(queryRef);

  // ...
}

这指示React Router等待查询完成加载,然后路由转换。当promise解析后,路由转换后,数据会立即渲染,而无需在路由组件中显示加载替代内容。

queryRef.toPromise在3.9.0版本中是实验性的,并且在3.10.0之前可能会有破坏性更改。如果您希望在3.10.0稳定之前为此功能提供反馈,请在#11519上发表评论。

为什么在toPromise中阻止访问数据?

您可能想知道为什么我们用queryRef本身解析toPromise,而不是从查询加载的数据。我们希望鼓励您利用useReadQuery以避免错过查询的缓存更新。如果数据可用,那么在loader函数中使用它并将其暴露给路由组件是很有诱惑力的。这样做意味着错过了缓存更新。

如果您需要在loader函数中访问原始查询数据,请直接使用client.query()。

重新获取和分页

Apollo的Suspense数据获取钩子通过refetch函数返回函数,用于重新获取查询数据,以及通过fetchMore函数获取额外的数据页。

让我们通过添加重新获取品种的能力来更新我们的示例。我们从useBackgroundQuery返回的元组的第二项中解构refetch函数。我们将通过使用React的useTransition钩子来确保向用户显示待处理状态,让用户知道数据正在重新获取:

jsxCopy code
import { Suspense, useTransition } from "react";
import {
  useSuspenseQuery,
  useBackgroundQuery,
  useReadQuery,
  gql,
  TypedDocumentNode,
  QueryReference,
} from "@apollo/client";

function App() {
  const [isPending, startTransition] = useTransition();
  const [queryRef, { refetch }] = useBackgroundQuery(
    GET_BREEDS_QUERY
  );

  function handleRefetch() {
    startTransition(() => {
      refetch();
    });
  };

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Dog
        id="3"
        queryRef={queryRef}
        isPending={isPending}
        onRefetch={handleRefetch}
      />
    </Suspense>
  );
}

function Dog({
  id,
  queryRef,
  isPending,
  onRefetch,
}: DogProps) {
  const { data } = useSuspenseQuery(GET_DOG_QUERY, {
    variables: { id },
  });

  return (
    <>
      Name: {data.dog.name}
      <Suspense fallback={<div>Loading breeds...</div>}>
        <Breeds isPending={isPending} queryRef={queryRef} />
      </Suspense>
      <button onClick={onRefetch}>Refetch!</button>
    </>
  );
}

function Breeds({ queryRef, isPending }: BreedsProps) {
  const { data } = useReadQuery(queryRef);

  return data.breeds.map(({ characteristics }) =>
    characteristics.map((characteristic) => (
      <div
        style={{ opacity: isPending ? 0.5 : 1 }}
        key={characteristic}
      >
        {characteristic}
      </div>
    ))
  );
}

在这个示例中,我们的App组件渲染了一个Dog组件,该组件通过useSuspenseQuery获取单只狗的记录。当React首次尝试渲染Dog时,缓存无法满足对GetDog查询的请求,因此useSuspenseQuery发起了一个网络请求。Dog在网络请求进行时暂停,触发了App中暂停组件上方最近的Suspense边界,它渲染了我们的"Loading..."替代内容。一旦网络请求完成,Dog使用新缓存的名字为id="3"的狗渲染:Mozzarella the Corgi。

与查询预加载的使用

在React之外加载查询时,preloadQuery函数返回的queryRef没有访问refetch或fetchMore函数的能力。当您需要重新获取或分页预加载的查询时,这可能会带来挑战。

您可以通过使用useQueryRefHandlers钩子获得访问refetch和fetchMore函数的能力。这个钩子与React过渡集成,使您能够重新获取或分页而不显示加载替代内容。

让我们更新我们的示例,以在React之外预加载我们的GET_BREEDS_QUERY,并使用useQueryRefHandlers钩子重新获取我们的查询。

jsxCopy code
// ...
import {
  // ...
  useQueryRefHandlers,
} from "@apollo/client";

const queryRef = preloadQuery(GET_BREEDS_QUERY);

function App() {
  const [isPending, startTransition] = useTransition();
  const { refetch } = useQueryRefHandlers(queryRef);

  function handleRefetch() {
    startTransition(() => {
      refetch();
    });
  };

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Dog
        id="3"
        isPending={isPending}
        onRefetch={handleRefetch}
      />
    </Suspense>
  );
}

// ...

我们在React之外使用preloadQuery函数开始加载我们的GET_BREEDS_QUERY。我们将preloadQuery返回的queryRef传递给useQueryRefHandlers钩子,它为我们提供了一个refetch函数,我们可以在点击按钮时用它重新获取查询。

使用其他Suspense钩子产生的queryRef时使用useQueryRefHandlers

useQueryRefHandlers也可以与返回queryRef的任何钩子结合使用,如useBackgroundQuery或useLoadableQuery。当您需要在深层传递queryRef的组件中访问refetch和fetchMore函数时,这很有用。

让我们再次使用useBackgroundQuery更新我们的示例,并看看如何在Dog组件中使用useQueryRefHandlers访问refetch函数:

jsxCopy code
function App() {
  const [queryRef] = useBackgroundQuery(GET_BREEDS_QUERY);

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Dog id="3" queryRef={queryRef} />
    </Suspense>
  );
}

function Dog({ id, queryRef }: DogProps) {
  const { data } = useSuspenseQuery(GET_DOG_QUERY, {
    variables: { id },
  });
  const [isPending, startTransition] = useTransition();
  const { refetch } = useQueryRefHandlers(queryRef);

  function handleRefetch() {
    startTransition(() => {
      refetch();
    });
  };

  return (
    <>
     ```
   Name: {data.dog.name}
      <Suspense fallback={<div>Loading breeds...</div>}>
        <Breeds queryRef={queryRef} isPending={isPending} />
      </Suspense>
      <button onClick={handleRefetch}>Refetch!</button>
    </>
  );
}

使用 useQueryRefHandlers 返回的处理程序并不会阻止你使用由查询引用钩子产生的处理程序。你可以在两个位置使用这些处理程序,无论是否使用 React 过渡,以产生所需的结果。

区分查询的关键是 queryKey Apollo 客户端使用查询和变量的组合来唯一标识每个查询,当使用 Apollo 的 Suspense 数据获取钩子时。

如果你的应用程序渲染多个使用相同查询和变量的组件,这可能会出现问题:多个钩子发出的查询共享相同的标识,导致它们同时挂起,而不管是哪个组件发起或重新发起网络请求。

你可以使用 queryKey 选项来预防这种情况,以确保每个钩子具有独特的身份。当提供了 queryKey 时,Apollo 客户端会将其作为钩子身份的一部分,除了它的查询和变量。

更多信息,请参阅 useSuspenseQueryuseBackgroundQuery API 文档。

跳过 Suspense 钩子 虽然 useSuspenseQueryuseBackgroundQuery 都有一个 skip 选项,但该选项只存在于从 useQuery 迁移时尽可能少地更改代码。长期来看,不应该使用它。

相反,你应该使用 skipToken:

建议使用 skipTokenuseSuspenseQuery

javascriptCopy code
import { skipToken, useSuspenseQuery } from '@apollo/client';
const { data } = useSuspenseQuery(
  query,
  id ? { variables: { id } } : skipToken
);

建议使用 skipTokenuseBackgroundQuery

javascriptCopy code
import { skipToken, useBackgroundQuery } from '@apollo/client';
const [queryRef] = useBackgroundQuery(
  query,
  id ? { variables: { id } } : skipToken
);

React 服务端组件 (RSC) 与 Next.js 13 应用路由器的使用 在 Next.js v13 中,Next.js 的新应用路由器为 React 社区带来了第一个完全支持 React 服务端组件 (RSC) 和流式服务端渲染 (Streaming SSR) 的框架,将 Suspense 作为一个从应用程序的路由层一直延伸下来的一等概念。

为了与这些特性集成,我们的 Apollo 客户端团队发布了一个实验性包 @apollo/experimental-nextjs-app-support,它允许 Apollo 客户端与 RSC 和流式 SSR 无缝使用,是数据获取库中第一个此类的。更多详情,请查看其 README 和我们的介绍性博客文章。

在流式服务端渲染期间使用 useBackgroundQuery 进行流式传输 在客户端渲染的应用程序中,可以使用 useBackgroundQuery 避免请求瀑布,但在使用流式服务端渲染的应用程序中,它的影响可能更加明显。这是因为服务器可以开始向客户端流式传输内容,带来更大的性能优势。

错误处理 在纯客户端渲染的应用程序中,组件中抛出的错误总是被最近的错误边界捕获并显示。

在使用流式服务端渲染 API 时,服务器上抛出的错误处理方式有所不同。有关更多信息,请参阅 React 文档。