2020終わり頃のApollo Client 3から、 reactive variables なるものが追加されたようです。リアクティブな変数を持たせることができるようになったということです。

New in Apollo Client 3, reactive variables are a useful mechanism for storing local state outside of the Apollo Client cache. Because they’re separate from the cache, reactive variables can store data of any type and structure, and you can interact with them anywhere in your application without using GraphQL syntax.

https://www.apollographql.com/docs/react/local-state/reactive-variables/

これを使うと、ReduxやRecoilで実現しているようなストアがApolloで手軽に実現できます。
すでにApolloを使っていて、通常のキャッシュで管理しない値を遠いコンポーネントでも同期させたいなと思ったらこれを使うのも手ですね。
クエリの結果をキャッシュしてもらうのとは別で自由に管理できる点もメリットとして挙げられています。

公式の謳い文句を意訳すると、こんなニュアンスになります。

  • GraphQLの操作なしに、アプリケーションのどこからでもリアクティブ変数を読み書きできます。
  • Apollo Clientのキャッシュと違いデータ正規化は行われず、好きな形式でデータを持たせることができます。
  • リアクティブな変数に依存している値がある場合、それが変更された際には関連するすべてのアクティブなクエリが自動的にリフレッシュされます。

柔軟お手軽に、リアクティブな変数をどこからでも触れるというわけですね。

今回は、こんな感じのタブナビゲーションを作ってみることにします。

completed

環境構築

読んで雰囲気をつかむだけならスキップでOKです。
といっても、Next.jsのexamplesからApollo導入済みのテンプレートを活用するので、ほとんど準備することはありません。

これ: https://github.com/vercel/next.js/tree/canary/examples/with-apollo

$ yarn create next-app --example with-apollo apollo-store
$ cd apollo-store
$ yarn dev

任意のディレクトリで上記コマンドを実行してください。 http://localhost:3000 でデフォルトで実装されているページが表示されるはずです。

今回利用する useReactiveVar は3.2で追加されたものなので、必要であれば@apollo/clientのバージョンを上げておきましょう。執筆時は3.1.1がインストールされていたので、3.3.0に上げて使いました。

$ yarn upgrade @apollo/client@3.3.0

準備

まずは、components以下のApp.js以外と、pagesの_app.js, index.js以外を消してしまいましょう。

init-file-tree

したらベースとなるアプリをつくります。全部コピペでOKです。

components/App.js

export default function App({ children }) {
  return (
    <main>
      {children}
      <style jsx>{`
        * {
          font-family: Menlo, Monaco, 'Lucida Console', 'Liberation Mono',
            'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New',
            monospace, serif;
        }
        body {
          margin: 0;
          padding: 0;
        }
        main {
          padding: 25px 50px;
        }
        p {
          font-size: 14px;
          line-height: 24px;
        }
      `}</style>
    </main>
  )
}

components/Tab.js

import Link from "next/link";

export default function Tab({ name }) {
  return (
    <>
      <Link href={`/${name}`}>
        <div className={`tab_item ${true && 'active'}`}>{name}</div>
      </Link>
      <style jsx global>{`
        .tab_item {
          display: flex;
          justify-content: center;
          width: 100%;
          padding: 20px;
          background-color: lightgray;
          cursor: pointer;
        }
        .active {
          background-color: lightgreen !important;
        }
      `}</style>
    </>
  )
}

components/Tabs.js

import Tab from "./Tab";

export default function Tabs() {
  return (
    <div className="tab_container">
      <Tab name="home"/>
      <Tab name="about"/>
      <Tab name="detail"/>
      <Tab name="setting"/>
      <style jsx global>{`
        .tab_container {
          display: flex;
          justify-content: space-between;
          width: 100vw;
          position: fixed;
          bottom: 0;
        }
      `}</style>
    </div>
  )
}

pages/home.js

import App from "../components/App";

const Home = () => (
  <App>
    <div>home</div>
  </App>
);

export default Home;

pages/about.js

import App from "../components/App";

const About = () => (
  <App>
    <div>about</div>
  </App>
);

export default About;

同様にdetailとsettingも作ってください。

pages/_app.js

iimport { ApolloProvider } from '@apollo/client'
import { useApollo } from '../lib/apolloClient'
import Tabs from "../components/Tabs";

export default function App({ Component, pageProps }) {
  const apolloClient = useApollo(pageProps)

  return (
    <ApolloProvider client={apolloClient}>
      <Component {...pageProps} />
      <Tabs/>
    </ApolloProvider>
  )
}

ここまでやると、こんな画面になると思います。

default

さて、問題はこちらです。

components/Tab.js

import Link from "next/link";

export default function Tab({ name }) {
  return (
    <>
      <Link href={`/${name}`}>
        <div className={`tab_item ${true && 'active'}`}>{name}</div>

アクティブなタブの判定部分ですね。 通常はURLを監視しておいて判定したり、グローバルなストアで管理したりするものだと思います。

今回はここの処理にApolloを活用してみようと思います。

実装

lib/apolloClient.js を編集します。

import { makeVar } from '@apollo/client'

export const activeTabName = makeVar('')

function createApolloClient() {
  return new ApolloClient({
    ssrMode: typeof window === 'undefined',

略

    cache: new InMemoryCache({
      typePolicies: {
        Query: {
          fields: {
            activeTabName: {
              read() {
                return activeTabName()

makeVarをimportしつつ、cacheに渡してあげます。

components/Tab.js

import { useReactiveVar } from "@apollo/client";
import { activeTabName } from "../lib/apolloClient";

export default function Tab({ name }) {
  const activeName = useReactiveVar(activeTabName)

  return (
    <>
      <Link href={`/${name}`}>
        <div className={`tab_item ${name === activeName && 'active'}`}>{name}</div>
      </Link>

useReactiveVaractiveTabNameをimportし、読み出したものを条件にします。

あとは、activeTabNameの書き換えですね。

pages/home.js

import App from "../components/App";
import { activeTabName } from "../lib/apolloClient";

const Home = () => {
  useEffect(() => {
    activeTabName('home')
  }, [])

  return (
    <App>
      <div>home</div>
    </App>
  )
};

export default Home;

importしたactiveTabNameに値を渡せば更新してくれます。他のページも同じように書き換えれば完成です。

completed