8. 使用查询获取数据
10m

useQuery React Hook一起工作

现在我们已经

与React集成

client/src/index.tsx
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 ApolloClient
const 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 exist
const 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 component
root.render(
<ApolloProvider client={client}>
<Pages />
</ApolloProvider>
);

显示发射列表

在我们的应用中构建展示可用SpaceX的页面。打开src/pages/launches.tsx。目前,文件看起来像这样:

client/src/pages/launches.tsx
import React from "react";
import { gql } from "@apollo/client";
export const LAUNCH_TILE_DATA = gql`
fragment LaunchTile on Launch {
__typename
id
isBooked
rocket {
id
name
}
mission {
name
missionPatch
}
}
`;
interface LaunchesProps {}
const Launches: React.FC<LaunchesProps> = () => {
return <div />;
};
export default Launches;

定义查询

首先,我们将定义用于获取分页列表的的形状。将其粘贴到LAUNCH_TILE_DATA声明下方:

client/src/pages/launches.tsx
export const GET_LAUNCHES = gql`
query GetLaunchList($after: String) {
launches(after: $after) {
cursor
hasMore
launches {
...LaunchTile
}
}
}
${LAUNCH_TILE_DATA}
`;

使用LazyQuery

使用片段

注意,我们的定义引用了上面的LAUNCH_TILE_DATA定义。LAUNCH_TILE_DATA定义了一个名为LaunchTile片段。片段非常有用,可以定义一组,在多个查询中无需重写即可使用。

在上面的中,我们通过使用...前缀包含LaunchTile片段,类似于JavaScript的扩展运算符语法。

分页详细信息

注意,除了获取launches列表之外,我们的还在获取hasMorecursor字段。这是因为launches查询返回的是分页结果

  • ThehasMore字段表示是否存在服务器返回的列表之外的额外
  • 光标字段指示客户端在中的当前位置。我们可以再次执行,并将我们最新的光标作为$after变量的值来获取列表中下一组

应用 useQuery 钩子

我们将在useQuery反应钩子的帮助下执行我们的新Launches组件内。钩子返回的结果对象提供属性,这些属性帮助我们在整个执行过程中填充和渲染组件。

  1. 将你的@apollo/client导入修改为包含useQuery,以及导入一些用于渲染页面的预定义组件:
client/src/pages/launches.tsx
import { gql, useQuery } from "@apollo/client";
import { LaunchTile, Header, Button, Loading } from "../components";

由于我们使用TypeScript,我们还将导入从服务器模式定义生成的必要类型

client/src/pages/launches.tsx
import * as GetLaunchListTypes from "./__generated__/GetLaunchList";

最后,确保从React导入Fragment

client/src/pages/launches.tsx
import React, { Fragment } from "react";
  1. 使用以下内容替换对“Launches”的虚拟声明:
client/src/pages/launches.tsx
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 。让我们来看看如何操作。

添加分页支持

3提供了用于基于偏移量Relay风格的分页的新辅助函数,这些辅助函数尚未在本教程中体现。

提供了一个fetchMore辅助函数来帮助处理分页查询。它允许你使用不同的(如当前的)执行相同的

fetchMore添加到从useQuery结果对象析构的对象列表中,并定义一个isLoadingMore状态:

在此阶段,你需要从React导入useState。在launches.tsx文件顶部,添加useState

client/src/pages/launches.tsx
import React, { Fragment, useState } from "react";
client/src/pages/launches.tsx
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组件中:

client/src/pages/launches.tsx
{
data.launches &&
data.launches.hasMore &&
(isLoadingMore ? (
<Loading />
) : (
<Button
onClick={async () => {
setIsLoadingMore(true);
await fetchMore({
variables: {
after: data.launches.cursor,
},
});
setIsLoadingMore(false);
}}
>
Load More
</Button>
));
}
//</Fragment>

当我们的新按钮点击时,它调用fetchMore(将当前的指针作为after 变量的值传递)并显示一个正在加载数据的通知,直到返回结果。

让我们测试一下我们的按钮。启动所有内容并再次访问 localhost:3000。现在在下面我们的 20 个 旁边出现了一个 更多加载 按钮。点击它。在 返回后,不再出现额外的启动🤔

如果你检查浏览器的网络活动,你会发现该按钮确实向服务器发送了后续的 ,并且服务器确实响应了一个 列表。 然而 将这些列表分开存储,因为它们代表具有 不同变量值 的查询结果(在这个例子中,是 after 的值)。

我们需要 将来自我们的 fetchMore 合并 与原始 启动 列表。让我们配置这种行为。

合并缓存结果

将你的 结果存储在其 内存缓存 中。缓存智能而高效地处理大多数 ,但它不知道我们想要合并我们两个不同的 列表。要解决这个问题,我们将定义一个针对模式中分页 merge 函数

打开 src/cache.ts,在那里我们初始化了默认的 InMemoryCache

client/src/cache.ts
import { InMemoryCache, Reference } from "@apollo/client";
export const cache: InMemoryCache = new InMemoryCache({});

我们服务器分页的方案字段是启动列表。修改 cache 的初始化以添加 字段的一个 merge 函数,如下所示:

client/src/cache.ts
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 的查询。

这个示例演示了 字段策略 的应用,字段策略是缓存配置选项,针对您模式中特定的

如果您现在尝试点击 加载更多 按钮,界面将成功将额外的 添加到列表中!

任务!

显示单个启动的详细信息

我们想要能够点击列表中的 查看其完整详情。打开 src/pages/launch.tsx 并替换它的内容为以下:

client/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) {
site
rocket {
type
}
...LaunchTile
}
}
${LAUNCH_TILE_DATA}
`;

这个 包含了页面所需的所有详情。注意,我们正在重用 LAUNCH_TILE_DATA ,它已经在 launches.tsx 中定义了。

再次,我们将我们的 传递给 useQuery 钩子。这次,我们还需要将对应的 's launchId 作为 传递给 React Router 的 useParams 钩子 来访问当前 URL 中的 launchId

现在,将 launch.tsx 的内容替换为以下:

client/src/pages/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) {
site
rocket {
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>
<Header
image={
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 并将其内容替换为以下内容:

client/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 {
id
email
trips {
...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 可以在不需要从网络上获取数据的情况下返回该数据。

然而,缓存的数据可能会过时。轻微过时的数据在很多情况下是可以接受的,但我们确实希望用户的已预订行程列表是最新的。为了处理这个问题,我们已经为我们的 获取策略 指定了 GET_MY_TRIPS

获取策略定义了 对于特定 如何使用缓存。默认策略是 cache-first,这意味着 在进行网络请求前会检查缓存,看结果是否存在。如果结果存在,则不会发生网络请求。

通过将此 ''network-only,我们确保 '

有关所有支持的获取策略的列表,请参阅 支持的获取策略

如果您访问您的应用中的个人资料页面,您会注意到 返回 null。这是因为我们还需要实现登录功能。我们将在下一节中解决这个问题!

上一页