apollo-link-stateで状態管理を行う
昨日、会社の同僚とreduxにapolloのキャッシュを乗せるかどうかって話をしたので、サンプルを書いてみました。
自分はreduxのアーキテクチャが大変好きですが、Apolloとは相性が悪いためと思っています。
(なんでApolloClient.reducer()
がなくなったのかも考えるといいかも)
また、ここでは話しませんがprop drillingを解決するための他の方法として、context apiの使用も考えることが可能です。
サンプルコード
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の例を用いて説明します。
この構成は数字の表示部分と横に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の書き方
以下を参考にしてください
問題点
redux-dev-toolsみたいなのほしい。。。(なにかあるのかな?)
結論
利点として、connect
と似たような動きになるので部分更新が可能となります。(且つ、特に設定無しで自動更新可能)
apollo-link-stateを使うと、actionsがクエリになるイメージです。
クエリで@client
自体が複合が可能なので、fetchとローカルからの問い合わせが可能で、表現が豊かになります。
reduxのmiddlewareでループを回して(e.g. timer)ゴニョゴニョやるみたいなのには向かないですが、APIからとってきて表示する程度であればこれで充分だと思います。
まぁそれはそもそもreact-apollo自体が向かないので、reduxとかに自分なら乗り換えるかな。。