技術探し

JavaScriptを中心に記事を書いていきます :;(∩´﹏`∩);:

apollo-link-stateで状態管理を行う

www.apollographql.com

昨日、会社の同僚とreduxにapolloのキャッシュを乗せるかどうかって話をしたので、サンプルを書いてみました。

自分はreduxのアーキテクチャが大変好きですが、Apolloとは相性が悪いためと思っています。
(なんでApolloClient.reducer()がなくなったのかも考えるといいかも)

また、ここでは話しませんがprop drillingを解決するための他の方法として、context apiの使用も考えることが可能です。

サンプルコード

github.com

react-apollo

react-apolloではクエリを渡すことで自動にfetchを行い、コンポーネントレンダリングすることが可能です。

// github/graphqlを叩き、hiroppyの情報を取得し表示する

const GET_AUTHOR = gql`
  query {
    user(login: "hiroppy") {
      name
      avatarUrl
      bio
    }
  }
`;

export const Top = () => (
  <>
    <Query query={GET_AUTHOR}>
      {({ loading, error, data }) => {
        if (loading) return 'Loading...';
        if (error) return `Error! ${error.message}`;

        const { user }: { user: typeof Author } = data;

        return (
          <>
            <h3>Author</h3>
            <p>{user.name}</p>
            <p>{user.bio}</p>
            <Icon src={user.avatarUrl} />
          </>
        );
      }}
    </Query>
    ...
  </>
);

つまり、reduxでいう通信をするだけのmiddlewareがやってほしい部分をすでに吸収しています。
あとは、グローバルな状態管理をどう考えるかだけです。

apollo-link-state

このライブラリはローカルのデータに対して、クエリを投げることを可能とし、storeを構築します。
つまり、状態が管理されている部分にクエリを投げて操作する感じです。

クエリに @client と追加するとローカルに向きます。(複合も可)

ローカルの状態を作成する

reduxでいうreducersのinitialStateです。

export const author = {
  name: '',
  avatarUrl: '',
  bio: '',
  __typename: 'User'
};

export const counter = {
  current: 0,
  __typename: 'Counter'
};

export const initialState = { // まとめたオブジェクトをclientの初期値に設定する
  author,
  counter
};

clientを作成する

今回は、githubのgraphqlのapiを叩くのとローカルデータを変更することのできるクライアントを例として出します。

import { ApolloClient, InMemoryCache, ApolloLink, HttpLink } from 'apollo-boost';
import { withClientState } from 'apollo-link-state';
import { initialState } from './state';
import { resolvers } from './resolvers';

const stateLink = withClientState({ // ローカルのstore
  cache,
  defaults: initialState, // 初期状態を登録する
  resolvers
});
const httpLink = new HttpLink({ // githubのapiを設定
  uri: 'https://api.github.com/graphql',
  headers: {
    authorization: `Bearer ${process.env.TOKEN}`
  }
});

// 両方のlinkを使う
const link = ApolloLink.from([stateLink, httpLink]);

export const client = new ApolloClient({ // これをProviderのclientへ渡す
  link,
  cache: new InMemoryCache()
});

値が変わった時にコンポーネントを更新する

いわゆる、reduxのconnectです。

以下のUIの例を用いて説明します。

f:id:about_hiroppy:20181107063753p:plain

この構成は数字の表示部分と横にbuttonsというコンポーネントが並びます。

// Top.tsx

const GET_CURRENT_COUNTER = gql`
  query {
    counter @client {
      current
    }
  }
`;

export const Top: React.SFC = () => (
  <>
    <Query query={GET_CURRENT_COUNTER}>
      {({ loading, error, data }) => {
        if (loading) return 'Loading...';
        if (error) return `Error! ${error.message}`;

        const { counter }: { counter: typeof Counter } = data;

        return <p>{counter.current}</p>;
      }}
    </Query>
    <Button />
  </>
);
// buttons.tsx

const INCREASE_CURRENT_COUNTER = gql`
  mutation increase($type: String!) {
   changeValue(type: $type) @client
  }
`;

const DECREASE_CURRENT_COUNTER = gql`
  mutation decrease($type: String!) {
    changeValue(type: $type) @client
  }
`;

function createValue(type: string) {
  return { variables: { type } };
}

export const Button: React.SFC = () => (
  <>
    <Mutation mutation={INCREASE_CURRENT_COUNTER}>
      {(increase, { loading, error, data }) => {
        if (loading) return 'Loading...';
        if (error) return `Error! ${error.message}`;

        return <button onClick={() => increase(createValue('+'))}>increase</button>
      }}
    </Mutation>
    <Mutation mutation={DECREASE_CURRENT_COUNTER}>
      {(decrease, { loading, error, data }) => {
        if (loading) return 'Loading...';
        if (error) return `Error! ${error.message}`;

        return <button onClick={() => decrease(createValue('-'))}>decrease</button>
      }}
    </Mutation>
  </>
);
// resolvers.ts

export const resolvers: IResolvers = {
  Mutation: {
    changeValue: (_, args, { cache }) => {
      const query = gql`
        query {
          counter {
            current
          }
        }
      `;

      const prev = cache.readQuery({ query });
      const current = args.type === '+' ?
        ++prev.counter.current :
        --prev.counter.current;
      const data = {
        counter: {
          current,
          __typename: 'Counter'
        }
      };

      // 書き込むと使われているコンポーネントが更新される
      cache.writeData({ data }); 

      return current;
    }
  }
};

特にcomponents側でなにかするわけではなく、これだけで関連するコンポーネントが更新されます。
この場合だと、ボタンがクリックされるたびにmutationが走り、cacheの書き込みがされたら、Top.tsxのラベルが自動的に更新されます。

つまりreduxでいうと、container components作成とactionの発火がなくなります。

テスト・storybookの書き方

以下を参考にしてください

github.com

問題点

redux-dev-toolsみたいなのほしい。。。(なにかあるのかな?)

github.com

結論

利点として、connectと似たような動きになるので部分更新が可能となります。(且つ、特に設定無しで自動更新可能)
apollo-link-stateを使うと、actionsがクエリになるイメージです。
クエリで@client自体が複合が可能なので、fetchとローカルからの問い合わせが可能で、表現が豊かになります。

reduxのmiddlewareでループを回して(e.g. timer)ゴニョゴニョやるみたいなのには向かないですが、APIからとってきて表示する程度であればこれで充分だと思います。
まぁそれはそもそもreact-apollo自体が向かないので、reduxとかに自分なら乗り換えるかな。。