React + TypeScript: Apollo ClientのuseMutationフックでGraphQLのデータを書き替える

[Apollo Client](https://www.apollographql.com/docs/react/)はReactで使える状態管理ライブラリです。ローカルとリモートのデータをGraphQLで扱えます。本稿は`useMutation`フックでGraphQLのデータをどう書き替えるかについての解説です。Apollo Clientでクエリを使うための基礎はすでに学んだことが前提となります(まだの方は先に「[React + TypeScript: Apollo ClientのGraphQLクエリを使ってみる](https://qiita.com/FumioNonaka/items/0c6b711627e3443ff73b)」をお読みください)。公式サイトの「[Mutations in Apollo Client](https://www.apollographql.com/docs/react/data/mutations/)」で紹介された[作例](https://codesandbox.io/s/mutations-example-app-final-tjoje)に、TypeScriptを採り入れ、アプリケーションは簡単にモジュール分けしてつくります。

# はじめの一歩のアプリケーションを準備する
お題とする公式作例は、CodeSandboxに公開されている「[Mutations > Example app final](https://codesandbox.io/s/mutations-example-app-final-tjoje)」です。モジュールは`src/index.js`ひとつで、TypeScriptも使われていません。本稿では、Reactアプリケーションのひな形を[Create React App](https://ja.reactjs.org/docs/create-a-new-react-app.html#create-react-app)でつくりましょう。やり方については、「React + TypeScript: Apollo ClientのGraphQLクエリを使ってみる」の「[Apollo Clientアプリケーションをつくる準備](https://qiita.com/FumioNonaka/items/0c6b711627e3443ff73b#apollo-client%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E3%81%A4%E3%81%8F%E3%82%8B%E6%BA%96%E5%82%99)」をお読みください。

はじめの一歩は、以下のふたつのモジュール(`src/App.tsx`と`src/AddTodo.tsx`)です(コード001)。`ApolloClient`のインスタンスをつくり、コンポーネントツリーは`ApolloProvider`で包んだだけです。GraphQLクエリはまだ使っておらず、フォーム(`<form>`)に`onSubmit`イベントハンドラも加えられていません。ご参考までに、ふたつのモジュールでつくられたサンプル001をCodeSandboxに公開しました。

#### コード001■はじめの一歩のふたつのモジュール
```react:src/App.tsx
import {
	ApolloClient,
	InMemoryCache,
	ApolloProvider,
	gql,
} from '@apollo/client';
import { AddTodo } from './AddTodo';

const client = new ApolloClient({
	uri: 'https://sxewr.sse.codesandbox.io/',
	cache: new InMemoryCache(),
});
function App() {
	return (
		<ApolloProvider client={client}>
			<div>
				<h2>Building Mutation components 🚀</h2>
				<AddTodo />
			</div>
		</ApolloProvider>
	);
}
export default App;
```

```react:src/AddTodo.tsx
import { VFC } from 'react';

export const AddTodo: VFC = () => {
	let input: HTMLInputElement | null;
	return (
		<div>
			<form>
				<input
					ref={(node) => {
						input = node;
					}}
				/>
				<button type="submit">Add Todo</button>
			</form>
		</div>
	);
};
```

#### サンプル001■React + TypeScript: Apollo Client Mutatiions 01
https://codesandbox.io/s/react-typescript-apollo-client-mutatiions-01-0eixyi


# `useMutation`フックを使う
Apollo ClientのGraphQLデータを書き替えるフックが`useMutation`です。フックに渡す変更のGraphQLドキュメント(`ADD_TODO`)は、ルートモジュール(`src/App.tsx`)に`gql`テンプレートリテラルでつぎのように定めます。オペレーション名の頭に添えるキーワードは`mutation`です(「[Operation name](https://graphql.org/learn/queries/#operation-name)」参照)。そして、項目追加の子コンポーネント(`AddTodo`)にプロパティとして与えます。

```react:src/App.tsx
const ADD_TODO = gql`
	mutation AddTodo($type: String!) {
		addTodo(type: $type) {
			id
			type
		}
	}
`;
function App() {
	return (
		<ApolloProvider client={client}>
			<div>
				{/* <AddTodo /> */}
				<AddTodo ADD_TODO={ADD_TODO} />
			</div>
		</ApolloProvider>
	);
}
```

[`useMutation`](https://www.apollographql.com/docs/react/api/react/hooks/#usemutation)フックを呼び出すのは子のモジュール(`src/AddTodo.tsx`)です(TypeScriptの型づけは「[useMutation](https://www.apollographql.com/docs/react/development-testing/static-typing/#usemutation)」参照)。フックに変更のGraphQLドキュメントを渡すと、変更関数と変更結果オブジェクトの[タプル](https://www.apollographql.com/docs/react/api/react/hooks/#mutationtupletdata-tvariables-result-tuple)が返されます(変更結果オブジェクトは`useQuery`の戻り値と類似)。`useQuery`フックと異なるのは、描画時に自動的に変更が実行されるのではなく、ユーザー操作などにもとづいて変更関数を呼び出さなければならないことです。

* [変更関数](https://www.apollographql.com/docs/react/api/react/hooks/#mutate) - UIからGraphQLデータの変更を実行する関数。オプションが第2引数に加えられる(後述)。

* 変更結果オブジェクト - データの変更状況を示すフィールドが含まれたオブジェクト(以下は一部を抜粋)。
    - [`data: TData`](https://www.apollographql.com/docs/react/api/react/hooks/#data) - 変更により返されたデータ。
    - [`loading: boolean`](https://www.apollographql.com/docs/react/api/react/hooks/#loading) - まだ変更中のときは`true`。
    - [`error: ApolloError`](https://www.apollographql.com/docs/react/api/react/hooks/#error) - 変更に問題が起こると、`graphQLErrors`の配列か`networkError`が返される。それ以外の場合は`undefined`。

```react:src/AddTodo.tsx
import { DocumentNode, useMutation } from '@apollo/client';

type Props = { ADD_TODO: DocumentNode };
// export const AddTodo: VFC = () => {
export const AddTodo: VFC<Props> = ({ ADD_TODO }) => {

	const [addTodo, { loading, error }] = useMutation(ADD_TODO);
	if (loading) return <p>Submitting...</p>;
	if (error) return <p>Submission error! {error.message}</p>;
	return (
		<div>
			<form
				onSubmit={(event) => {
					event.preventDefault();
					if (!input || !input.value.trim()) return;
					addTodo({ variables: { type: input.value } });
					input.value = '';
				}}
			>

			</form>
		</div>
	);
};
```


# 項目のリスト表示用モジュールを組み込む
前述の項目追加のモジュール(`src/AddTodo.tsx`)が項目データを変更する処理は、まだでき上がっていません。けれど、項目データを表示しなければ、結果が確かめられないでしょう。そこで、項目のリスト表示用コンポーネント(`Todos`)を新たに定めて、ルートモジュール(`src/App.tsx`)に組み込みます。

```react:src/App.tsx
import { Todos } from './Todos';

const GET_TODOS = gql`
	query GetTodos {
		todos {
			id
			type
		}
	}
`;

function App() {
	return (
		<ApolloProvider client={client}>
			<div>

				<Todos GET_TODOS={GET_TODOS} />
			</div>
		</ApolloProvider>
	);
}
```

項目をリスト表示するモジュール(`src/Todos.tsx`)の定めはつぎのとおりです(コード002)。`useQuery`フックの使い方については、前出「React + TypeScript: Apollo ClientのGraphQLクエリを使ってみる」および「[React + TypeScript: Apollo ClientのuseQueryフックでGraphQLのデータを読み込む](https://qiita.com/FumioNonaka/items/eafa971127b1a20438fb)」をお読みください。併せて、ルートモジュール(`src/App.tsx`)の記述も掲げました。

#### コード002■項目をリスト表示できるようにする
```react:src/Todos.tsx
import { DocumentNode, useQuery } from '@apollo/client';
import { VFC } from 'react';

type Props = {
	GET_TODOS: DocumentNode;
};
export const Todos: VFC<Props> = ({ GET_TODOS }) => {
	const { loading, error, data } = useQuery(GET_TODOS);
	if (loading) return <p>Loading...</p>;
	if (error) return <p>Error :(</p>;
	return data.todos.map(({ id, type }: { id: string; type: string }) => {
		let input: HTMLInputElement | null;
		return (
			<div key={id}>
				<p>{type}</p>
				<form
					onSubmit={(event) => {
						event.preventDefault();
						if (!input || !input.value.trim()) {
							return;
						}
						input.value = '';
					}}
				>
					<input
						ref={(node) => {
							input = node;
						}}
					/>
					<button type="submit">Update Todo</button>
				</form>
			</div>
		);
	});
};
```

```react:src/App.tsx
import {
	ApolloClient,
	InMemoryCache,
	ApolloProvider,
	gql,
} from '@apollo/client';
import { AddTodo } from './AddTodo';
import { Todos } from './Todos';

const client = new ApolloClient({
	uri: 'https://sxewr.sse.codesandbox.io/',
	cache: new InMemoryCache(),
});
const GET_TODOS = gql`
	query GetTodos {
		todos {
			id
			type
		}
	}
`;
const ADD_TODO = gql`
	mutation AddTodo($type: String!) {
		addTodo(type: $type) {
			id
			type
		}
	}
`;
function App() {
	return (
		<ApolloProvider client={client}>
			<div>
				<h2>Building Mutation components 🚀</h2>
				<AddTodo ADD_TODO={ADD_TODO} />
				<Todos GET_TODOS={GET_TODOS} />
			</div>
		</ApolloProvider>
	);
}
export default App;
```

これで、項目追加のコンポーネントに入力したテキストはボタンクリックで、GraphQLデータに項目として加わります(サンプル002)。ただし、ページを読み込み直さないと、新たな項目はリストに表示されません。これは、キャッシュの描画が更新されないためです。このあと改めましょう。

#### サンプル002■React + TypeScript: Apollo Client Mutatiions 02
https://codesandbox.io/s/react-typescript-apollo-client-mutations-02-gbm0zb


# `update`関数で項目データをリストに書き加える
変更のGraphQLドキュメント(`ADD_TODO`)を`useMutation`フックに渡して呼び出すと、たとえばつぎのような変更結果のオブジェクト(`Todo`)が返されます。`__typename`は、Apollo Clientが自動的に加えるフィールドです。このフィールドに`id`を添えた"Todo:5"をキーとして、Apollo Clientは変更されたオブジェクトをキャッシュします(「[Updating the cache directly](https://www.apollographql.com/docs/react/data/mutations/#updating-the-cache-directly)」参照)。

```terminal
{
	"__typename": "Todo",
	"id": "5",
	"type": "Buy grapes 🍇"
}
```

そのキーのオブジェクトがすでにキャッシュにあるときは、Apollo Clientは変更結果に含まれるフィールドを新たな値で上書きします(それ以外のフィールドは変わらず)。けれど、新たにキャッシュに加わったオブジェクトは、そのままではリストフィールドに含められません。この場合には、[`update`](https://www.apollographql.com/docs/react/api/react/hooks/#update)関数を定めなければならないのです(「[The update function](https://www.apollographql.com/docs/react/data/mutations/#the-update-function)」参照)。引数には、`cache`と変更結果オブジェクトを受け取ります。戻り値はありません。

変更のGraphQLドキュメント(`ADD_TODO`)を`useMutation`フックで実行すると、`addTodo`オブジェクト(`"__typename": "Todo"`の`Todo`オブジェクト)[^1]は、`update`関数が呼び出される前にただちにキャッシュに収められます。ただし、クエリ`GET_TODOS`が監視している項目リスト(`ROOT_QUERY.todos`)のキャッシュはそのままでは書き替わらないのです。つまり、新たな`Todo`オブジェクトは`GET_TODOS`に知らされず、項目リストには表示されません。

こういうとき、キャッシュから項目を手動で追加(あるいは削除)するのが[`cache.modify`](https://www.apollographql.com/docs/react/api/cache/InMemoryCache/#modify)メソッドです。引数オブジェクトには書き替え関数(modifier function)を収めて渡します(「[Using cache.modify](https://www.apollographql.com/docs/react/caching/cache-interaction/#using-cachemodify)」参照)。

クエリ`GET_TODOS`の結果が収められるのは、キャッシュの`ROOT_QUERY.todos`の配列です。したがって、書き替え関数のフィールド(`fields`)には`todos`を用い、新たに加えられた`Todo`の参照が配列のキャッシュに含まれるように書き替えます。追加する`Todo`の内部的な参照を得るのが[`cache.writeFragment`](https://www.apollographql.com/docs/react/api/cache/InMemoryCache/#writefragment)メソッドです。その参照を`ROOT_QUERY.todos`の配列要素として与えます。メソッドの引数オブジェクトに定めるのは、つぎのふたつの[オプション](https://www.apollographql.com/docs/react/api/cache/InMemoryCache/#options-6)です。

* [`data: Object`](https://www.apollographql.com/docs/react/api/cache/InMemoryCache/#data-1) - キャッシュに書き込むデータ(必須)。このオブジェクトのフィールドは、`fragment`で定められた形状にしたがわなければならならい。

* [`fragment: DocumentNode`](https://www.apollographql.com/docs/react/api/cache/InMemoryCache/#fragment-1) - `gql`テンプレートリテラルでつくられたGraphQLドキュメント(必須)。書き込む`fragment`を含む(GraphQL構文については、「[Fragments](https://www.apollographql.com/docs/react/data/fragments/)」および「[Fragments](https://graphql.org/learn/queries/#fragments)」参照)。

なお、`writeFragment`メソッドでキャッシュに加えた変更は、GraphQLサーバーには送られません。環境を再読み込みすると、変更は失われますのでご注意ください。

> **Any changes you make to cached data with `writeFragment` are not pushed to your GraphQL server**. If you reload your environment, these changes will disappear.
(「[writeFragment](https://www.apollographql.com/docs/react/caching/cache-interaction/#writefragment)」)

書き替え関数`todos`は、現行のキャッシュの配列を引数(`existingTodos`)に受け取ります。その配列に、新たに加えられたTodoの参照(`newTodoRef`)を加えて返せば項目リストのデータが改められるのです。`update`関数で書き替えたデータは、それを監視するクエリに自動的に伝わります。したがって、項目リストの表示も再描画されるでしょう。

[^1]:`id`と`type`を備えた項目(`Todo`)オブジェクトで、同名の変更関数`addTodo`とは異なることにご注意ください。

```react:src/AddTodo.tsx
// import { DocumentNode, useMutation } from '@apollo/client';
import { DocumentNode, gql, useMutation } from '@apollo/client';

export const AddTodo: VFC<Props> = ({ ADD_TODO }) => {

	// const [addTodo, { loading, error }] = useMutation(ADD_TODO);
	const [addTodo, { loading, error }] = useMutation(ADD_TODO, {
		update(cache, { data: { addTodo } }) {
			cache.modify({
				fields: {
					todos(existingTodos = []) {
						const newTodoRef = cache.writeFragment({
							data: addTodo,
							fragment: gql`
								fragment NewTodo on Todo {
									id
									type
								}
							`,
						});
						return [...existingTodos, newTodoRef];
					},
				},
			});
		},
	});

};
```

書き上がった項目追加のモジュール(`src/AddTodo.tsx`)の記述は、つぎのコード003のとおりです。併せて、以下のサンプル003をCodeSandboxに掲げました。

#### コード003■新たな項目をリストデータに書き加える
```react:src/AddTodo.tsx
import { VFC } from 'react';
import { DocumentNode, gql, useMutation } from '@apollo/client';

type Props = { ADD_TODO: DocumentNode };
export const AddTodo: VFC<Props> = ({ ADD_TODO }) => {
	let input: HTMLInputElement | null;
	const [addTodo, { loading, error }] = useMutation(ADD_TODO, {
		update(cache, { data: { addTodo } }) {
			cache.modify({
				fields: {
					todos(existingTodos = []) {
						const newTodoRef = cache.writeFragment({
							data: addTodo,
							fragment: gql`
								fragment NewTodo on Todo {
									id
									type
								}
							`,
						});
						return [...existingTodos, newTodoRef];
					},
				},
			});
		},
	});
	if (loading) return <p>Submitting...</p>;
	if (error) return <p>Submission error! {error.message}</p>;
	return (
		<div>
			<form
				onSubmit={(event) => {
					event.preventDefault();
					if (!input || !input.value.trim()) return;
					addTodo({ variables: { type: input.value } });
					input.value = '';
				}}
			>
				<input
					ref={(node) => {
						input = node;
					}}
				/>
				<button type="submit">Add Todo</button>
			</form>
		</div>
	);
};
```

#### サンプル003■React + TypeScript: Apollo Client Mutations 03
https://codesandbox.io/s/react-typescript-apollo-client-mutations-03-2kutb2


# 項目リストにあるデータを書き替える
項目リストのコンポーネント(`Todos`)には、テキスト入力フィールドとボタンを加えてありました。リストに加えられた項目のテキスト(`type`)を変更できるようにするためです。そこで、変更のGraphQLドキュメント(`UPDATE_TODO`)をルートモジュール(`src/App.tsx`)に定めましょう。この参照はコンポーネントのプロパティとして与えます。

```react:src/App.tsx
const UPDATE_TODO = gql`
	mutation UpdateTodo($id: String!, $type: String!) {
		updateTodo(id: $id, type: $type) {
			id
			type
		}
	}
`;
function App() {
	return (
		<ApolloProvider client={client}>
			<div>

				{/* <Todos GET_TODOS={GET_TODOS} /> */}
				<Todos GET_TODOS={GET_TODOS} UPDATE_TODO={UPDATE_TODO} />
			</div>
		</ApolloProvider>
	);
}
```

項目リスト表示のモジュール(`src/Todos.tsx`)は、つぎのように改めます。すでに、キャッシュの配列(`ROOT_QUERY.todos`)に含まれているデータを書き替えるのですから、`update`関数は要りません。変更関数(`updateTodo`)を呼び出せば済むことです。`useMutation`フックの戻り値から取り出した`loading`と`error`の値も、状態をテキストで示すために用います。ただし、プロパティ名が`useQuery`から得た変数とかぶるので変更しました。

```react:src/Todos.tsx
// import { DocumentNode, useQuery } from '@apollo/client';
import { DocumentNode, useMutation, useQuery } from '@apollo/client';

type Props = {

	UPDATE_TODO: DocumentNode;
};
// export const Todos: VFC<Props> = ({ GET_TODOS }) => {
export const Todos: VFC<Props> = ({ GET_TODOS, UPDATE_TODO }) => {

	const [updateTodo, { loading: mutationLoading, error: mutationError }] =
		useMutation(UPDATE_TODO);

	return data.todos.map(({ id, type }: { id: string; type: string }) => {

		return (
			<div key={id}>

				<form
					onSubmit={(event) => {

						updateTodo({ variables: { id, type: input.value } });
						input.value = '';
					}}
				>

				</form>
				{mutationLoading && <p>Loading...</p>}
				{mutationError && <p>Error :( Please try again</p>}
			</div>
		);
	});
};
```

これで、項目リストのモジュール(`src/Todos.tsx`)は、項目のテキストを書き替えられるようになりました(コード004)。併せて、ルートモジュール(`src/App.tsx`)の記述も掲げました。CodeSandboxに以下のサンプル004を公開しましたので、各モジュールのコードとアプリケーションの動きはこちらでお確かめください。

#### コード004■リスト項目のテキストを書き替える
```react:src/Todos.tsx
import { DocumentNode, useMutation, useQuery } from '@apollo/client';
import { VFC } from 'react';

type Props = {
	GET_TODOS: DocumentNode;
	UPDATE_TODO: DocumentNode;
};
export const Todos: VFC<Props> = ({ GET_TODOS, UPDATE_TODO }) => {
	const { loading, error, data } = useQuery(GET_TODOS);
	const [updateTodo, { loading: mutationLoading, error: mutationError }] =
		useMutation(UPDATE_TODO);
	if (loading) return <p>Loading...</p>;
	if (error) return <p>Error :(</p>;
	return data.todos.map(({ id, type }: { id: string; type: string }) => {
		let input: HTMLInputElement | null;
		return (
			<div key={id}>
				<p>{type}</p>
				<form
					onSubmit={(event) => {
						event.preventDefault();
						if (!input || !input.value.trim()) {
							return;
						}
						updateTodo({ variables: { id, type: input.value } });
						input.value = '';
					}}
				>
					<input
						ref={(node) => {
							input = node;
						}}
					/>
					<button type="submit">Update Todo</button>
				</form>
				{mutationLoading && <p>Loading...</p>}
				{mutationError && <p>Error :( Please try again</p>}
			</div>
		);
	});
};
```


```react:src/App.tsx
import {
	ApolloClient,
	InMemoryCache,
	ApolloProvider,
	gql,
} from '@apollo/client';
import { AddTodo } from './AddTodo';
import { Todos } from './Todos';

const client = new ApolloClient({
	uri: 'https://sxewr.sse.codesandbox.io/',
	cache: new InMemoryCache(),
});
const GET_TODOS = gql`
	query GetTodos {
		todos {
			id
			type
		}
	}
`;
const ADD_TODO = gql`
	mutation AddTodo($type: String!) {
		addTodo(type: $type) {
			id
			type
		}
	}
`;
const UPDATE_TODO = gql`
	mutation UpdateTodo($id: String!, $type: String!) {
		updateTodo(id: $id, type: $type) {
			id
			type
		}
	}
`;
function App() {
	return (
		<ApolloProvider client={client}>
			<div>
				<h2>Building Mutation components 🚀</h2>
				<AddTodo ADD_TODO={ADD_TODO} />
				<Todos GET_TODOS={GET_TODOS} UPDATE_TODO={UPDATE_TODO} />
			</div>
		</ApolloProvider>
	);
}
export default App;
```

#### サンプル004■React + TypeScript: Apollo Client Mutations 04
https://codesandbox.io/s/react-typescript-apollo-client-mutations-04-q9zvs3


### 関連記事
「**[React + TypeScript: Apollo ClientのuseQueryフックでGraphQLのデータを読み込む](https://qiita.com/FumioNonaka/items/eafa971127b1a20438fb)**」