🐙

Apollo ClientでState管理をやってみる

2021/11/20に公開

はじめに

最近、graphqlを触るようになりApollo Clientを使い始めるようになりました。
せっかくだからState管理もApolloでできたらいいなと思って、試行錯誤してる最中を言語化してみたものです。
ちゃんと理解できてない自信があり参考になるか全くわかりませんが、Apollo ClientでState管理興味あるぜ!って方はぜひ読んでみてください。
もし認識間違っているところあれば教えていただきたいです!

Appで管理が必要なStateを5つに分類

まず、Apolloの公式ブログを参考にアプリで管理が必要なStateを5つに分類します。

Local state

  • 1つのコンポーネント内で完結するstate
  • 例: ui関連(tooltip/modalの開閉), form関連(input/selectbox等のvalue)
  • useStateを使用

Global state

  • 複数のコンポーネントで横断的に使用するstate
  • 例: ui関連(tooltip/modalの開閉), form関連(input/selectbox等のvalue)
  • Reactive variablesを使用

Remote state

  • APIから取得するstate
  • 例: ドメインデータ
  • useQueryを使用

Meta state

Router state

  • 現在のURL
  • 例: /login, s=keyword
  • useRouterを使用

実装方法

次に上記で上げた5つのStateを具体的にどのように実装していくかのサンプルコードを考えてみました。

※Apollo Clientはv3以上、Next.js/typescriptの環境を前提にしてます。

Local state

同一コンポーネントでしか使用しないstateはuseStateを使用します。

定義

useStateはカスタムフックに切り出す。

import { useState } from "react";

type Hooks = {
  state: boolean;
  handleToggle: () => void;
};
export const useHooks = (): Hooks => {
  const [state, setState] = useState<boolean>(false);

  const handleToggle = () => {
    setState((state) => !state);
  };
  return { state, handleToggle };
};

使用

カスタムフックをコンテナコンポーネントで呼び出し、プレゼンテーショナルコンポーネントに渡す。

// Container
import type { VFC } from "react";
import { useHooks } from "./hooks";
import { Presenter } from "./presenter";

export const Sample: VFC = () => {
  const { state, handleToggle } = useHooks();

  return <Presenter state={state} handleToggle={handleToggle} />;
};

// Presentational
import type { VFC } from "react";

export type Props = {
  state: boolean;
  handleToggle: () => void;
};
export const Presenter: VFC<Props> = ({ state, handleToggle }) => {
  return (
    <div>
      <h1>Sample Component</h1>
      <div>state: {state}</div>
      <button onClick={handleToggle}>toggle</button>
    </div>
  );
};

Global state

複数のコンポーネントで使用するstateはApollo Clientが提供するReactive variablesを使用します。

ヘルパー関数

まずヘルパー関数を作成、Reactive Variblesの依存関係を二つの関数に閉じ込めます。

ReactiveVarを作成する関数

Apollo Clientが提供するmakeVarを使用

import { makeVar, ReactiveVar } from '@apollo/client';

export const createReactiveVar = <T>(initialValue: T): ReactiveVar<T> => makeVar<T>(initialValue);

useStateライクにreactive variablesを使用できるカスタムフックを作成する関数

Apollo Clientが提供するuseReactiveVarを使用

typePoliciesを使用している例ばかりなので、この実装方法に大きな不安を感じている。

// apollo/useReactiveVarHooks.ts
import { ReactiveVar, useReactiveVar } from '@apollo/client';

export type ReactiveVarHooks<T> = [T, (payload: T) => void];

export const useReactiveVarHooks = <T>(reactiveVar: ReactiveVar<T>): ReactiveVarHooks<T> => {
  const value = useReactiveVar(reactiveVar);
  const setValue = (payload: T) => {
    reactiveVar(payload);
  };
  return [value, setValue];
};

定義

// apollo/variables/isLoggedInVar.ts
import { createReactiveVar } from '../createReactiveVar';
import { ReactiveVarHooks, useReactiveVarHooks } from '../useReactiveVarHooks';

const initialValue = false;

// 変数名の接尾辞に Var を付与
export const isLoggedInVar = createReactiveVar(initialValue);

// 変数の値と変数を更新する関数を返すカスタムフック
export const useIsLoggedIn = (): ReactiveVarHooks<boolean> => useReactiveVarHooks(isLoggedInVar);

使用

値の参照

変数を使用する場合はカスタムフックの返り値の一つ目のみを受け取る

// useHooks.ts
import { useIsLoggedIn } from "@/apollo/variables/isLoggedInVar";

export const useHooks = () => {
  const [isLoggedIn] = useIsLoggedIn();

  return { isLoggedIn };
}

値の変更

関数を使用する場合はカスタムフックの返り値の二つ目のみを受け取る

// useHooks.ts
import { useIsLoggedIn } from "@/apollo/variables/isLoggedInVar";

export const useHooks = () => {
  const [, setIsLoggedIn] = useIsLoggedIn()
  const login = () => {
    setIsLoggedIn(true);
  }
  const logout = () => {
    setIsLoggedIn(false);
  }

  return { login, logout };
}

Remote state

APIから取得してくるデータを格納するstateはApollo Clientが提供するuseQueryを使用します。

import { gql, useQuery } from "@apollo/client";
import type { VFC } from "react";
import { Presenter } from "./presenter";

const GET_ALL_TODOS = gql`
  query GetAllTodos {
    todos {
      id
      title
    }
  }
`;

export const TodoList: VFC = () => {
  const { data } = useQuery<{ todos: TodoItem[] }>(GET_ALL_TODOS);

  if(!data) return null;

  return <Presenter todos={data.todos} />
}

import type { VFC } from "react";

export type Props = {
  todos: TodoItem[];
};
export const Presenter: VFC<Props> = ({ todos }) => {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
};

Meta state

APIでデータを取得する際に発生するloadingやerrorといったstateはuseQuery, useMutationの返り値であるloading, errorを使用します。

useQuery

import { gql, useQuery } from "@apollo/client";
import type { VFC } from "react";
import { Presenter } from "./presenter";

const GET_ALL_TODOS = gql`
  query GetAllTodos {
    todos {
      id
      title
    }
  }
`;

export const TodoList: VFC = () => {
  const { data, loading, error } = useQuery<{ todos: TodoItem[] }>(GET_ALL_TODOS);

  if(loading) return <Loading />
  if(error) return <ErrorDialog error={error} />
  if(!data) return null;

  return <Presenter todos={data.todos} />
}

useMutation

import { gql, useMutation } from "@apollo/client";
import type { VFC } from "react";
import { Presenter } from "./presenter";

const ADD_TODO = gql`
  mutation AddTodo($text: String!) {
    addTodo(text: $text) {
      id
      text
    }
  }
`;

export const TodoForm: VFC = () => {
  const [addTodo, { loading, error }] = useMutation(ADD_TODO);

  if(loading) return <Loading />
  if(error) return <ErrorDialog error={error} />

  return <Presenter addTodo={addTodo} />
}

Router state

Next.jsが提供しているuseRouterを使用

import { useRouter } from 'next/router'
import type { VFC } from "react";

export const Sample: VFC = () => {
  const router = useRouter();
  // URL が <https://localhost:3000/todo/hogefuga123> の時
  const path = router.asPath;
  // '/todo/hogefuga123'
  const query = router.query;
  // { id: 'hogefuga123' }
  return null;
};

おわりに

いかがでしたでしょうか。
使ってみるとわかりますが、Apollo Clientめっちゃ良いんですよ。ぜひ多くの方に使ってもらいたいと思う技術です!

Remote stateMeta stateで紹介した実装方法ですが、GraphQL Code Generatorを使ってuseQueryやuseMutationをラップしたカスタムフックを自動生成できるので、実案件ではこっちを使った方が良いかもです。

GraphQL Summitの動画もアップされたので引き続きApollo Clientの理解を深めていきたいと思います。

最後まで読んでいただきありがとうございました!

References

Discussion