1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[GraphQL] Relay.jsのMutate後のストア更新を3つ試してみた

Posted at

概要

  • Relay.jsでミューテーション後のストアのデータ更新する方法を3つ紹介
  • refetchもあるが、Relay.jsはミューテーションの戻り値としてクエリが利用可能であり、これを使うとより効率的
  • 戻り値を使ってIDがあれば自動的に、IDがなくても明示的にストアデータの更新が可能

特に断りのない限り、より一般的な意味での"ミューテーション"および"クエリ"と、型定義のMutationおよびQueryは区別して利用する。

対象とする読者

  • Relay, React, GraphQLの基本的な操作ができる人
  • Relay.jsを使っており、ミューテーションの戻り値を使ってクエリを効率的に再取得したい人

実装例

選択肢

データの再取得の際に考えられる方法はいくつかあるが、今回は代表的なもの3つを試す。

  1. refetchを使う方法
  2. IDを利用する方法
  3. updaterを使う方法

① refetchを使う方法

これはrefetch QueryやuseRefetchableFragmentにより容易に実現可能である。また、ネットワークリクエストが2回飛ぶことになりあまりお勧めしたくない。ここでは紹介に留めておく。

IDを利用する方法

まずは以下のようなスキーマを作る。ここでのポイントは、更新対象となるUserid: 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ストアの更新を明示的に行う必要はなく、以下のような仕組みで勝手にアップデートしてくれる。

  1. ミューテーションの戻り値のQueryにより、Relayストアのuser.id=1user.nameが更新される
  2. 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の戻り値のuseruseLazyLoadQueryuserとで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の方が健全かなと思う。

今回のサンプルコードはこちらに

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?