与useQuery
React Hook一起工作
现在我们已经
与React集成
import {ApolloClient,NormalizedCacheObject,ApolloProvider,} from "@apollo/client";import { cache } from "./cache";import React from "react";import ReactDOM from "react-dom/client";import Pages from "./pages";import injectStyles from "./styles";// Initialize ApolloClientconst client: ApolloClient<NormalizedCacheObject> = new ApolloClient({cache,uri: "https://127.0.0.1:4000/graphql",});injectStyles();// Find our rootElement or throw and error if it doesn't existconst rootElement = document.getElementById("root");if (!rootElement) throw new Error("Failed to find the root element");const root = ReactDOM.createRoot(rootElement);// Pass the ApolloClient instance to the ApolloProvider componentroot.render(<ApolloProvider client={client}><Pages /></ApolloProvider>);
显示发射列表
在我们的应用中构建展示可用SpaceX发射列表的页面。打开src/pages/launches.tsx
。目前,文件看起来像这样:
import React from "react";import { gql } from "@apollo/client";export const LAUNCH_TILE_DATA = gql`fragment LaunchTile on Launch {__typenameidisBookedrocket {idname}mission {namemissionPatch}}`;interface LaunchesProps {}const Launches: React.FC<LaunchesProps> = () => {return <div />;};export default Launches;
定义查询
首先,我们将定义用于获取分页列表的查询的形状。将其粘贴到LAUNCH_TILE_DATA
声明下方:
export const GET_LAUNCHES = gql`query GetLaunchList($after: String) {launches(after: $after) {cursorhasMorelaunches {...LaunchTile}}}${LAUNCH_TILE_DATA}`;
使用LazyQuery
使用片段
注意,我们的查询定义引用了上面的LAUNCH_TILE_DATA
定义。LAUNCH_TILE_DATA
定义了一个名为LaunchTile
的GraphQL片段。片段非常有用,可以定义一组字段,在多个查询中无需重写即可使用。
在上面的查询中,我们通过使用...
前缀包含LaunchTile
片段,类似于JavaScript的扩展运算符语法。
分页详细信息
注意,除了获取launches
列表之外,我们的查询还在获取hasMore
和cursor
字段。这是因为launches
查询返回的是分页结果:
- The
hasMore
字段表示是否存在服务器返回的列表之外的额外launches。 - “
光标
”字段指示客户端在启动列表中的当前位置。我们可以再次执行查询,并将我们最新的光标
作为$after
变量的值来获取列表中下一组启动。
应用 useQuery
钩子
我们将在Apollo Client的useQuery
反应钩子的帮助下执行我们的新查询在Launches
组件内。钩子返回的结果对象提供属性,这些属性帮助我们在整个查询执行过程中填充和渲染组件。
- 将你的
@apollo/client
导入修改为包含useQuery
,以及导入一些用于渲染页面的预定义组件:
import { gql, useQuery } from "@apollo/client";import { LaunchTile, Header, Button, Loading } from "../components";
由于我们使用TypeScript,我们还将导入从服务器模式定义生成的必要类型
import * as GetLaunchListTypes from "./__generated__/GetLaunchList";
最后,确保从React导入Fragment
。
import React, { Fragment } from "react";
- 使用以下内容替换对“Launches”的虚拟声明:
const Launches: React.FC<LaunchesProps> = () => {const { data, loading, error } = useQuery<GetLaunchListTypes.GetLaunchList,GetLaunchListTypes.GetLaunchListVariables>(GET_LAUNCHES);if (loading) return <Loading />;if (error) return <p>ERROR</p>;if (!data) return <p>Not found</p>;return (<Fragment><Header />{data.launches &&data.launches.launches &&data.launches.launches.map((launch: any) => (<LaunchTile key={launch.id} launch={launch} />))}</Fragment>);};
该组件将我们的GET_LAUNCHES
查询传递给useQuery
,并从结果中获取数据
、加载状态
和错误信息.
根据这些属性的状态,我们渲染包含启动的列表、加载指示器或错误消息。
使用npm start
启动你的服务器和客户端,并访问localhost:3000
。如果一切配置正确,我们的应用主页面就会出现,并列出20次SpaceX启动!
尽管如此,我们遇到了一个问题:总共有 超过20次 SpaceX 的发射。我们的服务器对结果进行分页,并在一个单一的响应中包括最多20次发射。
为了能够获取和存储 所有 发射,我们需要修改我们的代码以使用包含在我们 查询
中的 指针
和 hasMore
字段。让我们来看看如何操作。
添加分页支持
Apollo Client提供了一个fetchMore
辅助函数来帮助处理分页查询。它允许你使用不同的变量(如当前的指针)执行相同的查询。
将fetchMore
添加到从useQuery
结果对象析构的对象列表中,并定义一个isLoadingMore
状态变量:
在此阶段,你需要从React导入useState
。在launches.tsx
文件顶部,添加useState
:
import React, { Fragment, useState } from "react";
const Launches: React.FC<LaunchesProps> = () => {const { data, loading, error, fetchMore } = useQuery<GetLaunchListTypes.GetLaunchList,GetLaunchListTypes.GetLaunchListVariables>(GET_LAUNCHES);const [isLoadingMore, setIsLoadingMore] = useState(false);};
现在,我们可以将fetchMore
连接到Launches
组件中的按钮,该按钮在点击时获取额外的发射。
将此代码直接粘贴到</Fragment>
标签之上,在Launches
组件中:
{data.launches &&data.launches.hasMore &&(isLoadingMore ? (<Loading />) : (<ButtononClick={async () => {setIsLoadingMore(true);await fetchMore({variables: {after: data.launches.cursor,},});setIsLoadingMore(false);}}>Load More</Button>));}//</Fragment>
当我们的新按钮点击时,它调用fetchMore
(将当前的指针
作为after
变量的值传递)并显示一个正在加载数据的通知,直到查询返回结果。
让我们测试一下我们的按钮。启动所有内容并再次访问 localhost:3000
。现在在下面我们的 20 个 启动旁边出现了一个 更多加载 按钮。点击它。在 查询 返回后,不再出现额外的启动。 🤔
如果你检查浏览器的网络活动,你会发现该按钮确实向服务器发送了后续的 查询,并且服务器确实响应了一个 启动 列表。 然而, Apollo Client 将这些列表分开存储,因为它们代表具有 不同变量值 的查询结果(在这个例子中,是 after
的值)。
我们需要 Apollo Client 将来自我们的 fetchMore
查询 的 合并 与原始 查询 的 启动 列表。让我们配置这种行为。
合并缓存结果
Apollo Client 将你的 查询 结果存储在其 内存缓存 中。缓存智能而高效地处理大多数 操作,但它不知道我们想要合并我们两个不同的 启动 列表。要解决这个问题,我们将定义一个针对模式中分页 merge
函数。
打开 src/cache.ts
,在那里我们初始化了默认的 InMemoryCache
:
import { InMemoryCache, Reference } from "@apollo/client";export const cache: InMemoryCache = new InMemoryCache({});
我们服务器分页的方案字段是启动列表。修改 cache
的初始化以添加 launches 字段的一个 merge
函数,如下所示:
export const cache: InMemoryCache = new InMemoryCache({typePolicies: {Query: {fields: {launches: {keyArgs: false,merge(existing, incoming) {let launches: Reference[] = [];if (existing && existing.launches) {launches = launches.concat(existing.launches);}if (incoming && incoming.launches) {launches = launches.concat(incoming.launches);}return {...incoming,launches,};},},},},},});
这个 merge
函数接收我们的 现有缓存
启动缓存launches 和 传入
launches 并将它们合并为一个单一列表,然后返回。缓存存储这个合并后的列表,并将其返回给所有使用 launches
字段 的查询。
这个示例演示了 字段策略 的应用,字段策略是缓存配置选项,针对您模式中特定的 字段。
如果您现在尝试点击 加载更多 按钮,界面将成功将额外的 launches 添加到列表中!
显示单个启动的详细信息
我们想要能够点击列表中的 launch 查看其完整详情。打开 src/pages/launch.tsx
并替换它的内容为以下:
import { gql } from "@apollo/client";import { LAUNCH_TILE_DATA } from "./launches";export const GET_LAUNCH_DETAILS = gql`query LaunchDetails($launchId: ID!) {launch(id: $launchId) {siterocket {type}...LaunchTile}}${LAUNCH_TILE_DATA}`;
这个 query 包含了页面所需的所有详情。注意,我们正在重用 LAUNCH_TILE_DATA
fragment,它已经在 launches.tsx
中定义了。
再次,我们将我们的 query 传递给 useQuery
钩子。这次,我们还需要将对应的 launch's launchId
作为 variable 传递给 queryReact Router 的 useParams
钩子 来访问当前 URL 中的 launchId
。
现在,将 launch.tsx
的内容替换为以下:
import React, { Fragment } from "react";import { gql, useQuery } from "@apollo/client";import { LAUNCH_TILE_DATA } from "./launches";import { Loading, Header, LaunchDetail } from "../components";import { ActionButton } from "../containers";import { useParams } from "react-router-dom";import * as LaunchDetailsTypes from "./__generated__/LaunchDetails";export const GET_LAUNCH_DETAILS = gql`query LaunchDetails($launchId: ID!) {launch(id: $launchId) {siterocket {type}...LaunchTile}}${LAUNCH_TILE_DATA}`;interface LaunchProps {}const Launch: React.FC<LaunchProps> = () => {let { launchId } = useParams();// This ensures we pass a string, even if useParams returns `undefined`launchId ??= "";const { data, loading, error } = useQuery<LaunchDetailsTypes.LaunchDetails,LaunchDetailsTypes.LaunchDetailsVariables>(GET_LAUNCH_DETAILS, { variables: { launchId } });if (loading) return <Loading />;if (error) return <p>ERROR: {error.message}</p>;if (!data) return <p>Not found</p>;return (<Fragment><Headerimage={data.launch && data.launch.mission && data.launch.mission.missionPatch}>{data && data.launch && data.launch.mission && data.launch.mission.name}</Header><LaunchDetail {...data.launch} /><ActionButton {...data.launch} /></Fragment>);};export default Launch;
就像之前一样,我们使用 查询 的状态来渲染 正在加载
或 错误
状态,或者当 查询 完成
回到您的应用并点击列表中的启动,以查看其详细信息页面。
显示个人资料页面
我们希望用户的个人资料页面显示一个他们预订座位的 启动 列表。打开 src/pages/profile.tsx
并将其内容替换为以下内容:
import React, { Fragment } from "react";import { gql, useQuery } from "@apollo/client";import { Loading, Header, LaunchTile } from "../components";import { LAUNCH_TILE_DATA } from "./launches";import * as GetMyTripsTypes from "./__generated__/GetMyTrips";export const GET_MY_TRIPS = gql`query GetMyTrips {me {idtrips {...LaunchTile}}}${LAUNCH_TILE_DATA}`;interface ProfileProps {}const Profile: React.FC<ProfileProps> = () => {const { data, loading, error } = useQuery<GetMyTripsTypes.GetMyTrips>(GET_MY_TRIPS,{ fetchPolicy: "network-only" });if (loading) return <Loading />;if (error) return <p>ERROR: {error.message}</p>;if (data === undefined) return <p>ERROR</p>;return (<Fragment><Header>My Trips</Header>{data.me && data.me.trips.length ? (data.me.trips.map((launch: any) => (<LaunchTile key={launch.id} launch={launch} />))) : (<p>You haven't booked any trips</p>)}</Fragment>);};export default Profile;
您应该熟悉此代码中的所有概念,因为我们已经在先前页面中使用过了,但有几点需要注意:我们设置了 fetchPolicy
。
自定义获取策略
如前所述,Apollo Client 会将其缓存的 查询 结果存储在本地。如果您查询的数据已经存在于缓存在中,Apollo Client 可以在不需要从网络上获取数据的情况下返回该数据。
然而,缓存的数据可能会过时。轻微过时的数据在很多情况下是可以接受的,但我们确实希望用户的已预订行程列表是最新的。为了处理这个问题,我们已经为我们的 获取策略 指定了 GET_MY_TRIPS
查询。
获取策略定义了 Apollo Client 对于特定 查询 如何使用缓存。默认策略是 cache-first
,这意味着 Apollo Client 在进行网络请求前会检查缓存,看结果是否存在。如果结果存在,则不会发生网络请求。
通过将此 查询'的 fetchPolicy 设置为'network-only,我们确保 Apollo Client'总是查询服务器以获取用户最新预订的行程列表。
有关所有支持的获取策略的列表,请参阅 支持的获取策略。
如果您访问您的应用中的个人资料页面,您会注意到 查询 返回 null。这是因为我们还需要实现登录功能。我们将在下一节中解决这个问题!