Next.js:Apollo Client の構築手順
Next.js での Apollo Client の構築手順を整理します。
前提
こちらの手順に従い、開発環境を構築していることを前提とします。
GraphQL API はこちらの手順で構築したものを前提とします。
0. TypeScript のセットアップ
Next.js に TypeScript を導入します。
touch project-front/tsconfig.json
docker-compose run frontend yarn add --dev typescript @types/react @types/node
docker-compose run frontend yarn dev
既存の js
ファイルを ts
もしくは tsx
に置き換えます。
types.ts
を作成し、API から取得するリソースの型を定義します。
export interface Post {
id: string;
title: string;
body: string;
}
1. Apollo Client のセットアップ
インストール
docker-compose run frontend yarn add @apollo/client graphql
_app.tsx
_app.tsx
で ApolloClient
を初期化し、コンポーネントを <ApolloProvider>
でラップします。
import '../styles/globals.css';
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
import { AppProps } from 'next/dist/next-server/lib/router/router';
const cache = new InMemoryCache();
const client = new ApolloClient({
uri: `${process.env.NEXT_PUBLIC_BACKEND_URL}/graphql`,
cache,
});
function MyApp({ Component, pageProps }: AppProps) {
return (
<ApolloProvider client={client}>
<Component {...pageProps} />
</ApolloProvider>
);
}
export default MyApp;
2. GraphQL クエリ
GraphQL API からデータを取得する処理を書いていきます。
POSTS_QUERY
Post
の一覧を取得するクエリです。
posts.query.ts
クエリと取得できるデータの型を定義します。
import { gql } from '@apollo/client';
import { Post } from '../../types';
export const POSTS_QUERY = gql`
query {
posts {
id
title
}
}
`;
export interface PostsData {
posts: Post[];
}
PostsList.tsx
Post
一覧を取得・描画するコンポーネントです。
import Link from 'next/link';
import { useQuery } from '@apollo/client';
import { POSTS_QUERY, PostsData } from '../graphql/queries/posts.query';
import { NextPage } from 'next';
interface PostsListProps {}
const PostsList: NextPage<PostsListProps> = () => {
const { loading, error, data } = useQuery<PostsData>(POSTS_QUERY);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {JSON.stringify(error)}</p>;
const { posts } = data;
if (!posts) return null;
return (
<ul>
{posts.map((post, index) => {
return (
<li key={index}>
{post.title}{' '}
<Link href={`/posts/${post.id}`}>
<a>[Detail]</a>
</Link>
</li>
);
})}
</ul>
);
};
export default PostsList;
- コンテナ(HOC)でも実装できるが、ここでは
useQuery
フックを利用
index.tsx
ページレベルのコンポーネントです。
import { NextPage } from 'next';
import PostsList from '../components/PostsList';
interface HomeProps {}
const Home: NextPage<HomeProps> = () => {
return (
<div>
<h1>POSTS</h1>
<PostsList />
</div>
);
};
export default Home;
POST_QUERY
Post
1件を取得するクエリです。
post.query.ts
クエリと取得データの型を定義します。
import { gql } from '@apollo/client';
import { Post } from '../../types';
export const POST_QUERY = gql`
query($id: ID!) {
post(id: $id) {
id
title
body
}
}
`;
export interface PostData {
post: Post;
}
PostItem.tsx
Post
1件を取得・描画するコンポーネントです。
import { useQuery } from '@apollo/client';
import { NextPage } from 'next';
import { POST_QUERY, PostData } from '../graphql/queries/post.query';
interface PostItemProps {
id: string;
}
const PostItem: NextPage<PostItemProps> = ({ id }) => {
const { loading, error, data } = useQuery<PostData>(POST_QUERY, {
variables: { id: parseInt(id) },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {JSON.stringify(error)}</p>;
const { post } = data;
if (!post) return null;
return (
<div>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
);
};
export default PostItem;
[id].tsx
ページレベルのコンポーネントです。
import { NextPage, NextPageContext } from 'next';
import Link from 'next/link';
import PostItem from '../../components/PostItem';
interface PostPageProps {
id: string;
}
const PostPage: NextPage<PostPageProps> = ({ id }) => (
<div>
<h1>POST</h1>
<PostItem id={id} />
<hr />
<Link href="/">
<a>Go back</a>
</Link>
</div>
);
PostPage.getInitialProps = (ctx: NextPageContext) => {
const { id } = ctx.query;
return { id: typeof id === 'string' ? id : id[0] };
};
export default PostPage;
動作確認
POSTS_QUERY
POST_QUERY
問題なく表示されます。
3. GraphQL ミューテーション
GraphQL API でデータを更新する処理を書いていきます。
UPDATE_POST
Post
1件を更新するミューテーションです。
update-post.mutation.ts
クエリと取得データの型、インプットデータの型を定義します。
import { gql } from '@apollo/client';
import { Post } from '../../types';
export const UPDATE_POST = gql`
mutation($params: PostInputType!) {
updatePost(input: { params: $params }) {
id
title
body
}
}
`;
export interface UpdatePostData {
post: Post;
}
export interface PostInputType {
params: {
id: number;
title?: string;
body?: string;
};
}
EditPostItem.tsx
Post
1件を取得・描画し、編集・更新するコンポーネントです。
import { useQuery, useMutation } from '@apollo/client';
import { NextPage } from 'next';
import { useState } from 'react';
import {
UPDATE_POST,
UpdatePostData,
PostInputType,
} from '../graphql/mutations/update-post.mutation';
import { PostData, POST_QUERY } from '../graphql/queries/post.query';
import { Post } from '../types';
interface EditPostItemProps {
id: string;
}
const EditPostItem: NextPage<EditPostItemProps> = ({ id }) => {
const [post, setPost] = useState<Post>(null);
const [message, setMessage] = useState<string>('');
const [updatePost, mutationResult] = useMutation<
UpdatePostData,
PostInputType
>(UPDATE_POST);
const mutationError = mutationResult.error;
const { loading, error, data } = useQuery<PostData>(POST_QUERY, {
variables: { id: parseInt(id) },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {JSON.stringify(error)}</p>;
if (!post) setPost(data.post);
if (!post) return null;
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setMessage('');
const { name, value } = e.target;
setPost({
...post,
[name]: value,
});
};
const handleSave = async () => {
await updatePost({
variables: {
params: {
id: parseInt(post.id),
title: post.title,
body: post.body,
},
},
});
mutationError
? alert(`Error: ${JSON.stringify(mutationError)}`)
: setMessage('Successfully saved');
};
return (
<div>
<div>
<button onClick={handleSave}>Save</button>
{message ? <span className="message">{message}</span> : null}
</div>
<span className="form-field">
<input name="title" value={post.title} onChange={handleChange} />
</span>
<span className="form-field">
<textarea
name="body"
value={post.body}
rows={10}
onChange={handleChange}
/>
</span>
<style jsx>
{`
input,
textarea {
width: 100%;
}
span.form-field {
display: block;
overflow: hidden;
padding: 0 5px 0 0;
margin: 10px auto;
}
button {
margin-right: 5px;
}
span.message {
font-size: 0.8rem;
color: #0018f9;
}
`}
</style>
</div>
);
};
export default EditPostItem;
- ミューテーションもコンテナ(HOC)で実装できるが、ここでは
useMutation
フックを利用
edit.tsx
ページレベルのコンポーネントです。
import { NextPage, NextPageContext } from 'next';
import Link from 'next/link';
import EditPostItem from '../../../components/EditPostItem';
interface EditPostPageProps {
id: string;
}
const EditPostPage: NextPage<EditPostPageProps> = ({ id }) => (
<div>
<h1>POST</h1>
<EditPostItem id={id} />
<hr />
<Link href={`/posts/${id}`}>
<a>Go back</a>
</Link>
</div>
);
EditPostPage.getInitialProps = (ctx: NextPageContext) => {
const { id } = ctx.query;
return { id: typeof id === 'string' ? id : id[0] };
};
export default EditPostPage;
[id].tsx
Post
詳細から編集ページに遷移できるようにします。
import { NextPage, NextPageContext } from 'next';
import Link from 'next/link';
import PostItem from '../../components/PostItem';
interface PostPageProps {
id: string;
}
const PostPage: NextPage<PostPageProps> = ({ id }) => (
<div>
<h1>POST</h1>
+ <Link href={`/posts/${id}/edit`}>
+ <button>Edit</button>
+ </Link>
<PostItem id={id} />
<hr />
<Link href="/">
<a>Go back</a>
</Link>
</div>
);
PostPage.getInitialProps = (ctx: NextPageContext) => {
const { id } = ctx.query;
return { id: typeof id === 'string' ? id : id[0] };
};
export default PostPage;
-
Apollo Client
のキャッシュが効くので、詳細ページ → 編集ページに遷移する場合、POST_QUERY
の再取得は行われません✨
動作確認
詳細ページ
編集ページ
更新処理
問題なく「Title updated from GraphQL」から「Title updated from Apollo Client」に更新できました。
4. Apollo Client キャッシュの利用
グローバルステート(ローカルデータ)を Apollo Client
のキャッシュに保存して更新できるようにします。
resolvers
, typeDefs
のセットアップ
Post
一覧の「表示/非表示」を listHidden: boolean
としてキャッシュに持たせます。(性質的にはローカルステートですが、グローバルステートとみなして扱います。)
list-hidden.query.ts
クエリと取得データの型を定義します。
import { gql } from '@apollo/client';
const LIST_HIDDEN_QUERY = gql`
{
listHidden @client
}
`;
export interface ListHiddenData {
listHidden: boolean;
}
export default LIST_HIDDEN_QUERY;
-
@client
ディレクティブでクライアントサイドのフィールドにアクセス
toggle-list-hidden.mutation.ts
クエリと取得データの型を定義します。
import { gql } from '@apollo/client';
export const TOGGLE_LIST_HIDDEN = gql`
mutation {
toggleListHidden @client
}
`;
export interface ToggleListHiddenData {
listHidden: boolean;
}
resolvers.ts
typeDefs
でサーバーサイドのスキーマをベースにクライアントサイドのスキーマを定義します。また、resolvers
でクライアントサイドの更新処理を定義します。
import { gql, InMemoryCache, Resolvers } from '@apollo/client';
import { LIST_HIDDEN_QUERY, ListHiddenData } from './queries/list-hedden.query';
export const typeDefs = gql`
extend type Mutation {
ToggleListHidden: Boolean!
}
`;
export const resolvers: Resolvers = {
Mutation: {
toggleListHidden: (_root, _args, ctx, _info) => {
const cache: InMemoryCache = ctx.cache;
const { listHidden } = cache.readQuery<ListHiddenData, null>({
query: LIST_HIDDEN_QUERY,
});
cache.writeQuery<ListHiddenData, null>({
query: LIST_HIDDEN_QUERY,
data: { listHidden: !listHidden },
});
return !listHidden;
},
},
};
_app.tsx
ApolloClient
に typeDefs
と resolvers
を渡します。また、キャッシュのデフォルト値を cache.writeQuery
でセットしてます。
import '../styles/globals.css';
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
+ import { resolvers, typeDefs } from '../graphql/resolvers';
+ import { LIST_HIDDEN_QUERY } from '../graphql/queries/list-hedden.query';
import { AppProps } from 'next/dist/next-server/lib/router/router';
const cache = new InMemoryCache();
+ cache.writeQuery<ListHiddenData, null>({
+ query: LIST_HIDDEN_QUERY,
+ data: {
+ listHidden: true,
+ },
+ });
const client = new ApolloClient({
uri: `${process.env.NEXT_PUBLIC_BACKEND_URL}/graphql`,
cache,
+ typeDefs,
+ resolvers,
});
function MyApp({ Component, pageProps }: AppProps) {
return (
<ApolloProvider client={client}>
<Component {...pageProps} />
</ApolloProvider>
);
}
export default MyApp;
cache
の取得・更新
ToggleButton.tsx
listHidden
の true/false
を切り替えるボタンです。
import { useMutation, useQuery } from '@apollo/client';
import {
LIST_HIDDEN_QUERY,
ListHiddenData,
} from '../graphql/queries/list-hedden.query';
import {
TOGGLE_LIST_HIDDEN,
ToggleListHiddenData,
} from '../graphql/mutations/toggle-list-hidden.mutation';
import { NextPage } from 'next';
interface ToggleButtonProps {}
const ToggleButton: NextPage<ToggleButtonProps> = () => {
const {
data: { listHidden },
} = useQuery<ListHiddenData>(LIST_HIDDEN_QUERY);
const [toggleListHidden] = useMutation<ToggleListHiddenData>(
TOGGLE_LIST_HIDDEN
);
return (
<button
onClick={() => {
toggleListHidden();
}}
>
{listHidden ? 'Show' : 'Hide'}
</button>
);
};
export default ToggleButton;
index.tsx
Post
一覧に「表示/非表示」ボタンを導入します。
import { useQuery } from '@apollo/client';
import { NextPage } from 'next';
import PostsList from '../components/PostsList';
+ import ToggleButton from '../components/ToggleButton';
+ import { LIST_HIDDEN_QUERY, ListHiddenData } from '../graphql/queries/list-hedden.query';
interface HomeProps {}
const Home: NextPage<HomeProps> = () => {
+ const {
+ data: { listHidden },
+ } = useQuery<ListHiddenData>(LIST_HIDDEN_QUERY);
+
return (
<div>
+ <ToggleButton />
<h1>POSTS</h1>
{listHidden ? null : <PostsList />}
</div>
);
};
export default Home;
動作確認
非表示
表示
ボタンクリックでグローバルステートを切り替えれるようになりました。
Discussion