$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

Apollo platformのチュートリアルをやった

Hatena-Textbook 2018学習日記(5) - GraphQL編 - $shibayu36->blog;のようにHatena-Textbookを用いて最近のモダンなWebアプリケーション開発の学習をしているのだけど、TypeScript + GraphQL + Apollo Client + Reactの部分でそれぞれの技術の基本知識を理解できていなかったので、エラーが起きたときに何から直したらよいかわからない状態になってしまっていた。

前回はGraphQLのクエリについて学んだ - $shibayu36->blog;でGraphQLを少し掘り下げたので、今回はApollo platformについてチュートリアル( https://www.apollographql.com/docs/tutorial/introduction/ )を行いながら学習を進めていった。

Apollo platformとは

JSからGraphQLを使うときにめっちゃ便利に使えるフレームワーク。これだけでGraphQLサーバも書けるし、クライアントからGraphQLサーバへのアクセスも書ける。

特に面白いなと思ったのは、サーバサイドのGraphQLスキーマを、クライアントサイドで拡張して、GraphQLサーバのデータとクライアントサイドのデータを透過的に扱えるところ。これによって状態の取り扱いが非常にやりやすくなりそう。世のフロントエンドエンジニアにApollo Clientを布教したい - Qiitaが分かりやすいので参照のこと。

学習メモ

あとは学習メモ。今はApollo Clientの方を特に学習したかったので、GraphQLのサーバを作る部分はほとんどメモはない。

4. Run your graph in production - Apollo Basics - Apollo GraphQL Docs

  • Graph ManagerというGraphQLのSchemaをアップロードできるクラウドサービスがある

5. Connect your API to a client - Apollo Basics - Apollo GraphQL Docs

  • Apollo Clientを使うことでいい感じにキャッシュをしてくれる
  • またlocalでもremoteでも透過的に扱うことができる

6. Fetch data with queries - Apollo Basics - Apollo GraphQL Docs

  • useQueryはdata, loading, errorを使う形式が最もよく使う。const { data, loading, error } = useQuery(GET_LAUNCHES);
  • useQueryではfetchMoreを取ってきて、ページングすることができる。fetchMoreにわたすupdateQueryの返す値は、これまでのデータをmergeしたものを返す必要がある
        fetchMore({
          variables: {
            after: data.launches.cursor,
          },
          updateQuery: (prev, { fetchMoreResult, …rest }) => {
            if (!fetchMoreResult) return prev;
            return {
              …fetchMoreResult,
              launches: {
                …fetchMoreResult.launches,
                launches: [
                  …prev.launches.launches,
                  …fetchMoreResult.launches.launches,
                ],
              },
            };
          },
        })
  • useQueryにはvariablesを渡せる
  const { data, loading, error } = useQuery(
    GET_LAUNCH_DETAILS,
    { variables: { launchId } }
  );
  • fetchPolicyを用いれば、キャッシュから先に取得するかなどの条件を変更できる。network-onlyを使えば毎回APIから取得してくれる
  const { data, loading, error } = useQuery(
    GET_MY_TRIPS,
    { fetchPolicy: "network-only" }
  );

7. Update data with mutations - Apollo Basics - Apollo GraphQL Docs

  • mutationの処理にはuseMutationを使う。useMutationを使うとmutateのための関数が返ってくるので、それをコンポーネントに引き渡し、利用すると良い
  • また、ログイン処理で返ってきた値をlocalStorageに保存するなどの処理は、onCompletedを使う。
  • Apollo Clientを直接参照したいときはuseApolloClientを使う。これをすることでclientに保持されているデータにアクセスできる
export default function Login() {
  const client = useApolloClient();
  const [login, { loading, error }] = useMutation(
    LOGIN_USER,
    {
      onCompleted({ login }) {
        localStorage.setItem('token', login);
        client.writeData({ data: { isLoggedIn: true } });
      }
    }
  );

  if (loading) return <Loading />;
  if (error) return <p>An error occurred</p>;

  return <LoginForm login={login} />;
}
  • Apollo Clientがリクエストするときに送信するヘッダはnew ApolloClientのときに指定できる。これを使えばログイントークンをAuthorizationヘッダに含めるなどのことができる。
const client = new ApolloClient({
  cache,
  link: new HttpLink({
    uri: 'http://localhost:4000/graphql',
    headers: {
      authorization: localStorage.getItem('token'),
    },
  }),
});

8. Manage local state - Apollo Basics - Apollo GraphQL Docs

  • 内部状態はreduxなど別のものを使わずApollo cacheに統一しておくのが良い
  • 内部状態の取得はスキーマを定義することでGraphQLと同じような形で取得できる。
  • extendを使ってサーバーのスキーマを拡張できる
export const typeDefs = gql`
  extend type Query {
    isLoggedIn: Boolean!
    cartItems: [ID!]!
  }

  extend type Launch {
    isInCart: Boolean!
  }

  extend type Mutation {
    addOrRemoveFromCart(id: ID!): [Launch]
  }
`;
// ↑をnew Apollo Clientにわたす
  • クライアント側のスキーマを使うときは、@client directiveを使うことでアクセスできる。cacheのdataに入っているものは特にclient側のresolversを定義せずとも取得できる?以下のようにしていればclientサイドで作ったisLoggedInやcartItemsは取得できそう
cache.writeData({
  data: {
    isLoggedIn: !!localStorage.getItem(‘token’),
    cartItems: [],
  },
});
  • ↑を取得するためのクエリは@client directiveを使ってこう書く
  query IsUserLoggedIn {
    isLoggedIn @client
  }
  query GetCartItems {
    cartItems @client
  }
  • クライアントサイドでresolverを定義すれば、サーバのGraphQLスキーマに対し、Virtual Fieldを付与することもできる。以下はサーバ側のLaunch typeに対し、isInCartフィールドを追加する例。
export const resolvers = {
  Launch: {
    isInCart: (launch, _, { cache }) => {
      const { cartItems } = cache.readQuery({ query: GET_CART_ITEMS });
      return cartItems.includes(launch.id);
    },
  },
};
  • useMutationのrefetchQueriesを使うことで、更新後のデータの再取得ができる
  • localデータのmutationを定義するときは、cache.writeQueryを使うと便利
export const resolvers = {
  Mutation: {
    addOrRemoveFromCart: (_, { id }, { cache }) => {
      const { cartItems } = cache.readQuery({ query: GET_CART_ITEMS });
      const data = {
        cartItems: cartItems.includes(id)
          ? cartItems.filter(i => i !== id)
          : […cartItems, id],
      };
      cache.writeQuery({ query: GET_CART_ITEMS, data });
      return data.cartItems;
    },
  },
};