概要
- Relay.jsでミューテーション後のストアのデータ更新する方法を3つ紹介
- refetchもあるが、Relay.jsはミューテーションの戻り値としてクエリが利用可能であり、これを使うとより効率的
- 戻り値を使って
IDがあれば自動的に、IDがなくても明示的にストアデータの更新が可能
特に断りのない限り、より一般的な意味での"ミューテーション"および"クエリ"と、型定義のMutationおよびQueryは区別して利用する。
対象とする読者
- Relay, React, GraphQLの基本的な操作ができる人
- Relay.jsを使っており、ミューテーションの戻り値を使ってクエリを効率的に再取得したい人
実装例
選択肢
データの再取得の際に考えられる方法はいくつかあるが、今回は代表的なもの3つを試す。
- refetchを使う方法
-
IDを利用する方法 -
updaterを使う方法
① refetchを使う方法
これはrefetch QueryやuseRefetchableFragmentにより容易に実現可能である。また、ネットワークリクエストが2回飛ぶことになりあまりお勧めしたくない。ここでは紹介に留めておく。
② IDを利用する方法
まずは以下のようなスキーマを作る。ここでのポイントは、更新対象となるUserにid: ID!を持たせるのと、ミューテーションの戻り値としてquery: Queryを返すことである。
type Query {
user: User!
}
type Mutation {
updateUser(name: String!): UpdateSettingsPayload!
}
type User {
id: ID! # ポイント - IDを返すこと
name: String!
}
type UpdateSettingsPayload {
query: Query! # ポイント - Mutationの戻り値としてQueryを返す
}
これに対応するResolverは以下のように書く。
let userName = "initialUserName";
const resolvers = {
Query: {
user: () => {
return {
id: "1",
name: userName,
};
},
},
Mutation: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
updateUser: (_: any, { name }: any) => {
userName = name;
return {
query: {}, // Query Resolverが呼ばれるので空ObjectでOK
};
},
},
};
ここで補足しておくと、クエリリゾルバがよしなに解決してくれるので、Mutationのqueryは空で構わない。
また、Relay.js側は以下のようなコードにする。
const query = graphql`
query pageHomeQuery {
user {
name
}
}
`;
const updateUserMutation = graphql`
mutation pageHomeupdateUserMutation($name: String!) {
updateUser(name: $name) {
query {
user {
name
}
}
}
}
`;
export default function Home() {
// ②userId=1の情報が更新されるのでre-renderが走る
const data = useLazyLoadQuery<pageHomeQuery>(query, {});
const [userName, setUserName] = useState("");
const [updateUser] =
useMutation<pageHomeupdateUserMutation>(updateUserMutation);
const onClicUpdateUser = () => {
updateUser({
variables: { name: userName },
onCompleted: () => {} // ① RelayのストアのuserId=1のuserが更新される
});
};
return (
<div>
<div>user: {data.user.name}</div>
<div>
<input
type="text"
value={userName}
onChange={(e) => setUserName(e.target.value)}
></input>
<button type="button" onClick={onClicUpdateUser}>
変更
</button>
</div>
</div>
);
}
特にRelayストアの更新を明示的に行う必要はなく、以下のような仕組みで勝手にアップデートしてくれる。
- ミューテーションの戻り値の
Queryにより、Relayストアのuser.id=1なuser.nameが更新される -
user.id=1を持っているため、useLazyLoadQueryフックが発火し、user.nameが更新された状態で表示される
と言う仕組みである。こうすることでミューテーションの結果を使ってRelayストアを更新できるので、1回のネットワークリクエストで済み、refetchよりも効率的である。
③ updaterを利用する方法
なんらかの理由によりIDを持たせたくない場合、またはIDを持てない場合はupdaterが利用できる。参考ページを以下に示す。
まずは先ほどと同じようにIDが存在しないUserWithoutIdと、関連するクエリとミューテーションを作成する。
type Query {
userWithoutId: UserWithoutId!
}
type Mutation {
updateUserWithoutId(name: String!): UpdateSettingsPayload!
}
type UserWithoutId {
name: String!
}
type UpdateSettingsPayload {
query: Query!
}
let userWithoutIdName = "initialUserWithoutIdName";
const resolvers = {
Query: {
userWithoutId: () => {
return {
name: userWithoutIdName,
};
},
},
Mutation: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
updateUserWithoutId: (_: any, { name }: any) => {
userWithoutIdName = name;
return {
query: {},
};
},
},
};
const query = graphql`
query pageUserWithoutIdQuery {
userWithoutId {
name
}
}
`;
const updateUserWithoutIdMutation = graphql`
mutation pageUpdateUserWithoutIdMutation($name: String!) {
updateUserWithoutId(name: $name) {
query {
userWithoutId {
name
}
}
}
}
`;
export default function Page() {
const data = useLazyLoadQuery<pageUserWithoutIdQuery>(query, {});
const [userWithoutIdName, setUserWithoutIdName] = useState("");
const [updateUserWithoutId] = useMutation<pageUpdateUserWithoutIdMutation>(updateUserWithoutIdMutation);
const onClicUpdateUserWithoutId = () => {
updateUserWithoutId({
variables: { name: userNameWithoutId },
});
};
return (
<div>
<div>user: {data.userWithoutId.name}</div>
<div>
<input
type="text"
value={userName}
onChange={(e) => setUserWithoutIdName(e.target.value)}
></input>
<button type="button" onClick={onClicUpdateUserWithoutId}>
変更
</button>
</div>
</div>
);
}
ところが先ほどと同様にやってもうまくいかないことがわかる。先ほどとは異なり、Mutationの戻り値のuserとuseLazyLoadQueryのuserとでIDの一致が取れないので、更新ができないからである。この問題を解決するためには、明示的にuseLazyLoadQueryで使っているRelayストアのクエリデータを更新する必要がある。
そこで、はじめに@updatableなクエリを用意する。
const updatableQuery = graphql`
query pageUserWithoutIdUpdatableQuery @updatable {
userWithoutId {
name
}
}
`;
そして、Mutationのupdaterの中であれば、Relayストア更新のためのreadUpdatableQuery APIとMutationのresponseが手に入るので、Relayストアのクエリをresponseで明示的に更新する処理を書く。
updateUserWithoutId({
variables: { name: userWithoutIdName },
updater: (store, response) => {
const { updatableData } =
store.readUpdatableQuery<pageUserWithoutIdUpdatableQuery>(
updatableQuery, // 上で用意したupdtableなクエリ
{}
);
if (response) {
// @updatableにしているので、直接ストア内のクエリににアクセスできる
// ストアのクエリをresponseのクエリで上書き
updatableData.userWithoutId.name =
response.updateUserWithoutId.query.userWithoutId.name;
}
},
});
これによりストアのクエリデータが上書きされるので、useLazyLoadQueryフックもストアのデータ更新に伴って発火する。
最後に
ネットワークリクエストが1回で済むことから、2か3をお勧めしたい。2の方がシンプルだとは思うが、IDを持たせたくないようなケースは普通にあると思うし、RefetchableにするためだけにID固定値化するくらいなら3の方が健全かなと思う。
今回のサンプルコードはこちらに