Apollo ClientはReactで使える状態管理ライブラリです。ローカルとリモートのデータをGraphQLで扱えます。本稿はuseMutationフックでGraphQLのデータをどう書き替えるかについての解説です。Apollo Clientでクエリを使うための基礎はすでに学んだことが前提となります(まだの方は先に「React + TypeScript: Apollo ClientのGraphQLクエリを使ってみる」をお読みください)。公式サイトの「Mutations in Apollo Client」で紹介された作例に、TypeScriptを採り入れ、アプリケーションは簡単にモジュール分けしてつくります。
はじめの一歩のアプリケーションを準備する
お題とする公式作例は、CodeSandboxに公開されている「Mutations > Example app final」です。モジュールはsrc/index.jsひとつで、TypeScriptも使われていません。本稿では、Reactアプリケーションのひな形をCreate React Appでつくりましょう。やり方については、「React + TypeScript: Apollo ClientのGraphQLクエリを使ってみる」の「Apollo Clientアプリケーションをつくる準備」をお読みください。
はじめの一歩は、以下のふたつのモジュール(src/App.tsxとsrc/AddTodo.tsx)です(コード001)。ApolloClientのインスタンスをつくり、コンポーネントツリーはApolloProviderで包んだだけです。GraphQLクエリはまだ使っておらず、フォーム(<form>)にonSubmitイベントハンドラも加えられていません。ご参考までに、ふたつのモジュールでつくられたサンプル001をCodeSandboxに公開しました。
コード001■はじめの一歩のふたつのモジュール
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;
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
useMutationフックを使う
Apollo ClientのGraphQLデータを書き替えるフックがuseMutationです。フックに渡す変更のGraphQLドキュメント(ADD_TODO)は、ルートモジュール(src/App.tsx)にgqlテンプレートリテラルでつぎのように定めます。オペレーション名の頭に添えるキーワードはmutationです(「Operation name」参照)。そして、項目追加の子コンポーネント(AddTodo)にプロパティとして与えます。
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フックを呼び出すのは子のモジュール(src/AddTodo.tsx)です(TypeScriptの型づけは「useMutation」参照)。フックに変更のGraphQLドキュメントを渡すと、変更関数と変更結果オブジェクトのタプルが返されます(変更結果オブジェクトはuseQueryの戻り値と類似)。useQueryフックと異なるのは、描画時に自動的に変更が実行されるのではなく、ユーザー操作などにもとづいて変更関数を呼び出さなければならないことです。
-
変更関数 - UIからGraphQLデータの変更を実行する関数。オプションが第2引数に加えられる(後述)。
-
変更結果オブジェクト - データの変更状況を示すフィールドが含まれたオブジェクト(以下は一部を抜粋)。
-
data: TData- 変更により返されたデータ。 -
loading: boolean- まだ変更中のときはtrue。 -
error: ApolloError- 変更に問題が起こると、graphQLErrorsの配列かnetworkErrorが返される。それ以外の場合はundefined。
-
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)に組み込みます。
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のデータを読み込む」をお読みください。併せて、ルートモジュール(src/App.tsx)の記述も掲げました。
コード002■項目をリスト表示できるようにする
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>
);
});
};
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
update関数で項目データをリストに書き加える
変更のGraphQLドキュメント(ADD_TODO)をuseMutationフックに渡して呼び出すと、たとえばつぎのような変更結果のオブジェクト(Todo)が返されます。__typenameは、Apollo Clientが自動的に加えるフィールドです。このフィールドにidを添えた"Todo:5"をキーとして、Apollo Clientは変更されたオブジェクトをキャッシュします(「Updating the cache directly」参照)。
{
"__typename": "Todo",
"id": "5",
"type": "Buy grapes 🍇"
}
そのキーのオブジェクトがすでにキャッシュにあるときは、Apollo Clientは変更結果に含まれるフィールドを新たな値で上書きします(それ以外のフィールドは変わらず)。けれど、新たにキャッシュに加わったオブジェクトは、そのままではリストフィールドに含められません。この場合には、update関数を定めなければならないのです(「The update function」参照)。引数には、cacheと変更結果オブジェクトを受け取ります。戻り値はありません。
変更のGraphQLドキュメント(ADD_TODO)をuseMutationフックで実行すると、addTodoオブジェクト("__typename": "Todo"のTodoオブジェクト)1は、update関数が呼び出される前にただちにキャッシュに収められます。ただし、クエリGET_TODOSが監視している項目リスト(ROOT_QUERY.todos)のキャッシュはそのままでは書き替わらないのです。つまり、新たなTodoオブジェクトはGET_TODOSに知らされず、項目リストには表示されません。
こういうとき、キャッシュから項目を手動で追加(あるいは削除)するのがcache.modifyメソッドです。引数オブジェクトには書き替え関数(modifier function)を収めて渡します(「Using cache.modify」参照)。
クエリGET_TODOSの結果が収められるのは、キャッシュのROOT_QUERY.todosの配列です。したがって、書き替え関数のフィールド(fields)にはtodosを用い、新たに加えられたTodoの参照が配列のキャッシュに含まれるように書き替えます。追加するTodoの内部的な参照を得るのがcache.writeFragmentメソッドです。その参照をROOT_QUERY.todosの配列要素として与えます。メソッドの引数オブジェクトに定めるのは、つぎのふたつのオプションです。
-
data: Object- キャッシュに書き込むデータ(必須)。このオブジェクトのフィールドは、fragmentで定められた形状にしたがわなければならならい。 -
fragment: DocumentNode-gqlテンプレートリテラルでつくられたGraphQLドキュメント(必須)。書き込むfragmentを含む(GraphQL構文については、「Fragments」および「Fragments」参照)。
なお、writeFragmentメソッドでキャッシュに加えた変更は、GraphQLサーバーには送られません。環境を再読み込みすると、変更は失われますのでご注意ください。
Any changes you make to cached data with
writeFragmentare not pushed to your GraphQL server. If you reload your environment, these changes will disappear.
(「writeFragment」)
書き替え関数todosは、現行のキャッシュの配列を引数(existingTodos)に受け取ります。その配列に、新たに加えられたTodoの参照(newTodoRef)を加えて返せば項目リストのデータが改められるのです。update関数で書き替えたデータは、それを監視するクエリに自動的に伝わります。したがって、項目リストの表示も再描画されるでしょう。
// 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■新たな項目をリストデータに書き加える
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
項目リストにあるデータを書き替える
項目リストのコンポーネント(Todos)には、テキスト入力フィールドとボタンを加えてありました。リストに加えられた項目のテキスト(type)を変更できるようにするためです。そこで、変更のGraphQLドキュメント(UPDATE_TODO)をルートモジュール(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から得た変数とかぶるので変更しました。
// 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■リスト項目のテキストを書き替える
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>
);
});
};
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
関連記事
「React + TypeScript: Apollo ClientのuseQueryフックでGraphQLのデータを読み込む」
-
idとtypeを備えた項目(Todo)オブジェクトで、同名の変更関数addTodoとは異なることにご注意ください。 ↩