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
writeFragment
are 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
とは異なることにご注意ください。 ↩