使用 React 的 Hook:useQuery
Apollo 入门引导 - 目录:
- 介绍
- 构建 schema
- 连接数据源
- 编写查询解析器
- 编写变更解析器
- 连接 Apollo Studio
- 创建 Apollo 客户端
- 通过查询获取数据
- 通过变更修改数据
- 管理本地状态
完成时间:20 分钟
前一节已经设置了 Apollo 客户端,现在可以将其集成到我们的 React 应用中了。可以使用React Hooks将 GraphQL 查询的结果直接绑定到 UI。
与 React 集成
为了将 Apollo 客户端连接到 React,需要将应用程序封装在 @apollo/client
包中的 ApolloProvider
组件中。我们通过 client
属性将客户端实例传递给 ApolloProvider
组件。
打开 src/index.tsx
,更新以下内容:
import {
ApolloClient,
NormalizedCacheObject,
ApolloProvider,
} from '@apollo/client';
import { cache } from './cache';
import React from 'react';
import ReactDOM from 'react-dom';
import Pages from './pages';
import injectStyles from './styles';
// 初始化Apollo 客户端
const client: ApolloClient = new ApolloClient({
cache,
uri: 'http://localhost:4000/graphql',
});
injectStyles();// 传递ApolloClient的实例给ApolloProvider组件
ReactDOM.render(
</ApolloProvider>,
document.getElementById('root')
);
ApolloProvider
组件类似于 React 的上下文提供器:它封装了 React 应用,并将 client
放置在上下文中,所以可以从组件树中的任何位置访问它。现在,准备构建执行 GraphQL 查询的 React 组件。
显示发射列表
接下来在应用中构建页面,该页面显示可获得的 SpaceX 发射的列表。打开 src / pages / launches.tsx
,该文件如下所示:
import React, { Fragment, useState } from 'react';
import { RouteComponentProps } from '@reach/router';
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 extends RouteComponentProps {}
const Launches: React.FC = () => {return
};export default Launches;
定义查询
首先定义查询的格式,该查询将用于获取发射的分页列表。将以下内容粘贴到 LAUNCH_TILE_DATA
声明的下方:
export const GET_LAUNCHES = gql`
query GetLaunchList($after: String) {
launches(after: $after) {
cursor
hasMore
launches {
...LaunchTile
}
}
}${LAUNCH_TILE_DATA}
`;
使用片段
注意,在定义查询时,在其上方插入了 LAUNCH_TILE_DATA
的定义。LAUNCH_TILE_DATA
定义了一个 GraphQL 片段(fragment),名为 LaunchTile
。片段对于定义一组字段时很有帮助,无需重写就可以将这组字段包含在多个查询中。
在上面的查询中,通过 ...
加 LaunchTile
方式引入了片段,类似于 JavaScript spread 语法。
分页详细信息
注意,除了获取 launches
列表之外,查询还获取 hasMore
和 cursor
字段。这是因为 launches
查询返回 分页结果 :
hasMore
字段指示服务返回的列表后面是否还有其他的发射信息。cursor
字段指示客户端在发射列表中的当前位置。可以再次执行查询,并提供最新的cursor
作为$after
变量的值,以获取列表中的 下一批 发射的集合。
使用 useQuery
hook
我们将使用 Apollo 客户端的 useQuery
React Hook在 Launches
组件中执行新查询。Hook 的结果对象提供的属性可以在查询执行时填充和呈现组件。
- 修改
@apollo/client
,导入useQuery
,再导入一些预定义的组件以呈现页面:
import { gql, useQuery } from '@apollo/client';
import { LaunchTile, Header, Button, Loading } from '../components';
如果使用的是 TypeScript,需要从服务的 schema 定义中导入必需的类型:
import * as GetLaunchListTypes from './__generated__/GetLaunchList';
- 将伪声明
const Launches
替换为以下内容:
const Launches: React.FC = () => {const { data, loading, error } = useQuery<
GetLaunchListTypes.GetLaunchList,
GetLaunchListTypes.GetLaunchListVariables
>(GET_LAUNCHES);if (loading) return ;if (error) return
ERROR</p>;
if (!data) return
Not found
p>;return (
{data.launches &&
data.launches.launches &&
data.launches.launches.map((launch: any) => (
))}
</Fragment>
);
};
该组件将 GET_LAUNCHES
查询传给 useQuery
,并从结果中获取 data
、 loading
和 error
属性。根据这些属性的状态来展现发射列表,加载状态和错误信息。
使用 npm start
启动服务和客户端,并访问 localhost:3000
。如果一切都配置正确,将会展现应用主页,并列出 20 次 SpaceX 发射!
但是有一个问题:总共的 SpaceX 发射数超过了 20 个。服务会分页显示其结果,并在一次响应中最多包含 20 次发射。
为了能够获取并存储 全部 启动,需要修改代码以使用查询中包含的 cursor
和 hasMore
字段。接下来学习如何做分页支持。
添加分页支持
本教程中没有展现 Apollo client 3 为基于偏移和Relay 风格的分页助手函数(pagination helper function)。
Apollo 客户端提供了一个 fetchMore
助手函数来协助分页查询。可以用不同值的变量(例如当前游标)来执行相同的查询。
从 useQuery
结果对象中解构的对象列表中添加 fetchMore
,并定义一个 isLoadingMore
状态变量:
const Launches: React.FC = () => {const {
data,
loading,
error,
fetchMore, // highlight-line
} = useQuery<
GetLaunchListTypes.GetLaunchList,
GetLaunchListTypes.GetLaunchListVariables
>(GET_LAUNCHES);const [isLoadingMore, setIsLoadingMore] = useState(false); //highlight-line// ...
};
现在将 fetchMore
连接到 Launches
组件中的按钮上,单击该按钮可获取其他发射。
将此代码直接粘贴在 Launches
组件的结束 Fragment>
标签上方:
{
data.launches &&
data.launches.hasMore &&
(isLoadingMore ? (
) : (
onClick={async () => {
setIsLoadingMore(true);
await fetchMore({
variables: {
after: data.launches.cursor,
},
});
setIsLoadingMore(false);
}}
>
Load More
</Button>
));
}
//Fragment>
单击按钮时,它将调用 fetchMore
(将当前的 cursor
的值传给after
变量),直到查询返回结果前一直显示加载中的状态。
启动所有内容,然后再次访问 localhost:3000
。现在已有的 20 个发射下方会出现一个 Load More 按钮,点击它。查询返回后, 没有其他发射出现。?
如果检查浏览器的网络活动,会发现该按钮确实向服务发送了后续查询,并且服务确实响应了新的发射列表。但是,Apollo 客户端将这些列表分隔开,因为它们表示带有 不同变量值 (本例为 after
的值)的查询结果。
我们需要 Apollo 客户端来将 fetchMore
查询中的发射与 之前 查询中的发射进行 合并 。接下来配置该行为。
合并缓存的结果
Apollo 客户端将查询结果存储在内存缓存中。缓存可以智能高效地处理大多数操作,但是并不能自动知道我们是否要合并两个不同的发射列表。为了解决这个问题,为 schema 中的分页字段定义一个 合并函数(merge
function)。
打开 src/cache.ts
,现在初始化的是默认 InMemoryCache
:
import { InMemoryCache, Reference } from '@apollo/client';
export const cache: InMemoryCache = new InMemoryCache({});
服务中分页的 schema 字段是 launches
列表。修改 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
方法接受我们现有的缓存发射(existing
)和传入的发射(incoming
),并将它们组合成一个列表并返回。缓存存储了此组合列表,并将其返回给所有使用 launches
字段的查询。
此示例展示字段策略的用法,这是针对 schema 中各个字段的缓存配置选项。
如果现在尝试单击 Load More 按钮,则 UI 将成功将其他发射附加到列表中!
显示单次发射的详情
我们希望能够单击列表中的发射以查看其完整详情。打开 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}
`;
该查询包含了所有详情。注意,代码复用了已经在 launches.tsx
中定义的 LAUNCH_TILE_DATA
片段。
再一次将查询传递给 useQuery
hook。这次还需要将对应发射的 launchId
作为变量传给查询。launchId
'的值可用路由来传递。
现在,将 launch.tsx
的内容替换为以下内容:
import React, { Fragment } from 'react'; // preserve-line
import { gql, useQuery } from '@apollo/client'; // preserve-line
import { LAUNCH_TILE_DATA } from './launches';
import { Loading, Header, LaunchDetail } from '../components'; // preserve-line
import { ActionButton } from '../containers'; // preserve-line
import { RouteComponentProps } from '@reach/router';
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 extends RouteComponentProps {
launchId?: any;
}
const Launch: React.FC = ({ launchId }) => {const { data, loading, error } = useQuery<
LaunchDetailsTypes.LaunchDetails,
LaunchDetailsTypes.LaunchDetailsVariables
>(GET_LAUNCH_DETAILS, { variables: { launchId } });if (loading) return ;if (error) return
ERROR: {error.message}</p>;
if (!data) return
Not found
p>;return ( image={
data.launch && data.launch.mission && data.launch.mission.missionPatch
}
>
{data && data.launch && data.launch.mission && data.launch.mission.name}
</Header>>
</Fragment>
);
};
export default Launch;
像以前一样,正在查询时呈现 loading
或 error
状态,在查询完成后呈现数据。
回到应用中,单击列表中的发射以查看详情页面。
显示个人资料页面
我们希望用户的个人资料页面显示其已预订的发射列表。打开 src/pages/profile.tsx
并将其内容替换为以下内容:
import React, { Fragment } from 'react'; // preserve-line
import { gql, useQuery } from '@apollo/client'; // preserve-line
import { Loading, Header, LaunchTile } from '../components'; // preserve-line
import { LAUNCH_TILE_DATA } from './launches'; // preserve-line
import { RouteComponentProps } from '@reach/router';
import * as GetMyTripsTypes from './__generated__/GetMyTrips';
export const GET_MY_TRIPS = gql`
query GetMyTrips {
me {
id
email
trips {
...LaunchTile
}
}
}${LAUNCH_TILE_DATA}
`;
interface ProfileProps extends RouteComponentProps {}
const Profile: React.FC = () => {const { data, loading, error } = useQuery(
GET_MY_TRIPS,
{ fetchPolicy: 'network-only' } // highlight-line
);if (loading) return ;if (error) return
ERROR: {error.message}</p>;
if (data === undefined) return
ERROR
p>;return (My Trips</Header>
{data.me && data.me.trips.length ? (
data.me.trips.map((launch: any) => (>
))
) : (
You haven't booked any trips
)}
);
};
export default Profile;
你应该从已经完成的页面中找到上述代码中的所有概念,只有一个例外:正在设置的 fetchPolicy
。
自定义 fetch 策略
如前所述,Apollo 客户端将查询结果存储在其缓存中。如果查询缓存中已存在的数据,则 Apollo 客户端会直接返回该数据,而无需通过网络获取。
但是,缓存的数据可能会过时。在大部分情况下,稍微过时的数据是可以接受的,但是用户的预订行程列表应当是时刻保持最新的。为了解决这个问题,专门为 GET_MY_TRIPS
查询指定了fetch 策略。
fetch 策略定义了 Apollo 客户端如何将缓存用于特定查询。默认策略是 cache-first
(缓存优先),这意味着 Apollo 客户端在发出网络请求之前会检查缓存,查看结果是否存在。如果存在结果,则不会发生网络请求。
通过将此查询的 fetch 策略设置为 network-only
(仅限网络来源),就能保证 Apollo 客户端 始终 是从服务中获取用户的最新预订行程列表。
有关所有支持的 fetch 策略的列表,请参阅支持的 fetch 策略。
如果访问应用中的个人资料页面,则会发现查询返回 null。这是因为还未实现登录功能。将在下一节中解决这个问题!
前端记事本,不定期更新,欢迎关注!
- 微信公众号:林景宜的记事本
- 博客:林景宜的记事本
- 掘金专栏:林景宜的记事本
- 知乎专栏:林景宜的记事本
- Github: MageeLin