[注記2020/07/15] 本稿は解説を全面的に改め、記事「React + TypeScript: Apollo Clientのリアクティブな変数(reactive variables)で複数コンポーネント間の状態を管理する」として公開しました。本稿は当面記録として残すものの、解説は新記事をお読みください。
Apollo ClientはReactで使える状態管理ライブラリです。ローカルとリモートのデータをGraphQLで扱えます。本稿が解説するのは、GraphQLのクエリは用いず、ローカルの状態を複数コンポーネントの間でどう共有するかです1。具体的には、リアクティブな変数(「Reactive variables」)の使い方になります(「Local State Management with Reactive Variables」参照)。
useState
で状態をもつ簡単なカウンター作例
まずは、Apollo Clientは用いることなく、useState
で状態をもつ簡単なカウンターの作例です(コード001)。
ルートモジュール(src/App.tsx
)はカウンター表示のコンポーネント(src/CounterDisplay.tsx
)を読み込んでアプリケーションのページに加えます。コンポーネントのJSXに差し込まれているのは加算・減算のボタンと数値の表示要素です。ボタンに与えるonClick
ハンドラ(decrement
およびincrement
)と数値(count
)は、カウンターロジックを別モジュール(src/useCounter.ts
)に切り分けたカスタムフック(useCounter
)から受け取りました。
コード001■useState
で状態をもつ簡単なカウンター
import React from 'react';
import { CounterDisplay } from './CounterDisplay';
import './App.css';
function App() {
return (
<div className="App">
<CounterDisplay />
</div>
);
}
export default App;
import { VFC } from 'react';
import { useCounter } from './useCounter';
export const CounterDisplay: VFC = () => {
const { count, decrement, increment } = useCounter();
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}
import { useState } from 'react';
export const useCounter = (initialCount = 0) => {
const [count, setCount] = useState(initialCount);
const decrement = () => setCount(count - 1);
const increment = () => setCount(count + 1);
return { count, decrement, increment };
};
カウンターのアプリケーションの動きは、CodeSandboxに公開したサンプル001でお確かめください。
サンプル001■React + TypeScript: Using reactive variables of Apollo Client 00
Apollo Clientで状態を扱う
では、Apollo Clientをインストールしましょう。今回、GraphQLのクエリは使わないものの、依存があるのでインストールに加えてください。
npm install @apollo/client graphql
Apollo Clientでは、アプリケーションで共有する状態を、ひとつひとつリアクティブな変数(Reactive variables)として定めます(「Announcing the Release of Apollo Client 3.0」の「Reactive variables」参照)。変数をつくるのは、文字どおりmakeVar
という関数です。引数には初期値を渡します(「Initialize reactive variables」参照)。ただし、戻り値はリアクティブな変数を扱う関数です。
その関数に新たな値を引数に渡して呼び出せば、変数値が改められます(引数を省くと、返されるのは変数の現行値です)。そして、更新されたコンポーネントの状態変数値は、useReactiveVar
フックで得られます(「Reacting」参照)。引数として渡すのはmakeVar
から返された関数です。
// import { useState } from 'react';
import { useCallback } from 'react';
import { makeVar, useReactiveVar } from '@apollo/client';
const countVar = makeVar(0);
// export const useCounter = (initialCount = 0) => {
export const useCounter = () => {
// const [count, setCount] = useState(initialCount);
const count = useReactiveVar(countVar);
const setCount = useCallback((value: number) => countVar(value), [])
};
カウンターのロジックを扱うカスタムフックのモジュール(src/useCounter.ts
)は、つぎのように書き改められました(コード002)。
コード002■Apollo Clientを用いたカスタムフックのモジュール
import { useCallback } from 'react';
import { makeVar, useReactiveVar } from '@apollo/client';
const countVar = makeVar(0);
export const useCounter = () => {
const count = useReactiveVar(countVar);
const setCount = useCallback((value: number) => countVar(value), [])
const decrement = () => setCount(count - 1);
const increment = () => setCount(count + 1);
return { count, decrement, increment };
};
Apollo Clientのリアクティブな変数を用いたCodeSandbox作例は、サンプル002に掲げました。なお、CodeSandboxでは、graphql
のバージョンを16以上にすると、つぎのようなTypeErrorが出てしまうようです。お気をつけください。
Cannot read properties of undefined (reading 'QUERY')
サンプル002■React + TypeScript: Using reactive variables of Apollo Client 01
複数のコンポーネントから状態を共有する
もっとも、状態がひとつのモジュール内に収まるのなら、useState
フックを使うことで別に差し支えはありません。そこで、あえて子コンポーネントに分けましょう。カウンターのボタン(CounterButton
)と数値表示(後述CountNumber
)です。カウンター表示のコンポーネント(CounterDisplay
)からカウンターボタンには、クリックイベントのハンドラ(handleClick
)と表示するテキスト(text
)をプロパティとして渡します。
import { CounterButton } from './CounterButton';
export const CounterDisplay: VFC = () => {
return (
<div>
{/* <button onClick={decrement}>-</button> */}
<CounterButton handleClick={decrement} text="-" />
{/* <button onClick={increment}>+</button> */}
<CounterButton handleClick={increment} text="+" />
</div>
);
}
カウンターボタンのモジュール(src/CounterButton.tsx
)の記述は、つぎのとおりです(コード003)。親コンポーネントから引数のプロパティで受け取ったクリックイベントハンドラ(handleClick
)とボタンテキスト(text
)を、それぞれJSXの<button>
要素に定めます。
コード003■カウンターボタンのモジュール
import { VFC } from 'react';
type Props = {
handleClick: () => void;
text: string;
};
export const CounterButton: VFC<Props> = ({ handleClick, text }) => {
return <button onClick={handleClick}>{text}</button>;
}
数値表示のモジュール(src/CountNumber.tsx
)は、数値(count
)を親からは受け取りません(コード004)。カウンターのカスタムフック(useCounter
)から、直に値が得られるからです。
コード004■数値表示のモジュール
import { VFC } from 'react';
import { useCounter } from './useCounter';
export const CountNumber: VFC = () => {
const { count } = useCounter();
return <span>{count}</span>
}
したがって、親コンポーネントのモジュール(src/CounterDisplay.tsx
)は、カウンターのカスタムフック(useCounter
)から数値(count
)を取り出すことなく、数値表示のコンポーネント(CountNumber
)をそのままJSXに差し込むだけで済みます。
import { CountNumber } from './CountNumber';
export const CounterDisplay: VFC = () => {
// const { count, decrement, increment } = useCounter();
const { decrement, increment } = useCounter();
return (
<div>
{/* <span>{count}</span> */}
<CountNumber />
</div>
);
}
書き改めたカウンター表示のモジュール(src/CounterDisplay.tsx
)は、つぎのような記述になりました(コード005)。
コード005■カウンター表示のモジュール
import { VFC } from 'react';
import { useCounter } from './useCounter';
import { CounterButton } from './CounterButton';
import { CountNumber } from './CountNumber';
export const CounterDisplay: VFC = () => {
const { decrement, increment } = useCounter();
return (
<div>
<CounterButton handleClick={decrement} text="-" />
<CountNumber />
<CounterButton handleClick={increment} text="+" />
</div>
);
}
複数コンポーネントから状態を共有するよう書き改めたCodeSandbox作例がつぎのサンプル003です。
サンプル003■React + TypeScript: Using reactive variables of Apollo Client 02
リアクティブな変数を汎用的なカスタムフックでつくる
Apollo Clientでは、リアクティブな変数をひとつひとつつくります。すると、それぞれについて変数の参照と設定関数が求められるのです。では、そのふたつを返す汎用的なカスタムフックがあれば、手間は減り、統一的な扱いもできます。
そこで、そのようなカスタムフック(useReactiveVarHooks
)を定めたのがつぎのモジュール(src/useReactiveVarHooks.ts
)です(コード006)。フックにはmakeVar
の戻り値の関数を渡し、変数の参照と設定関数の配列が返されます。前掲コード002のカスタムフック(useCounter
)から関数本体のはじめの2行を切り出したかたちです。ただ、リアクティブな変数にはどのようなデータも収められます。そのため、TypeScriptの型づけはジェネリックを用いた少し面倒な定めとなりました。
コード006■リアクティブな変数をつくるカスタムフックのモジュール
import { ReactiveVar, useReactiveVar } from '@apollo/client';
import { useCallback, useMemo } from 'react';
export type ReactiveVarHooks<T> = [T, (payload: T) => void];
export const useReactiveVarHooks = <T>(
reactiveVar: ReactiveVar<T>
): ReactiveVarHooks<T> => {
const value = useReactiveVar(reactiveVar);
const setValue = useCallback(
(payload: T) => {
reactiveVar(payload);
},
[reactiveVar]
);
return useMemo(() => [value, setValue], [setValue, value]);
};
変数はひとつずつ別モジュールにして、その中に加えたカスタムフックで前掲コード006のuseReactiveVar
から返される変数の参照と設定関数の配列をそのまま返すことにします。新たにカウンター数値のリアクティブな変数をつくるために定めたのがつぎのモジュール(src/countVar.ts
)です(コード007)。
コード007■リアクティブな変数を定めるモジュール
import { makeVar } from '@apollo/client';
import { ReactiveVarHooks, useReactiveVarHooks } from './useReactiveVarHooks';
const initialValue = 0;
const countVar = makeVar(initialValue);
export const useCountVar = (): ReactiveVarHooks<number> =>
useReactiveVarHooks(countVar);
そうすると、カウンターのカスタムフックのモジュール(src/useCounter.ts
)は、もはやApollo Clientの関数は使いません。カウンター数値のリアクティブな変数を定めるフック(useCountVar
)から、変数の参照と設定関数を配列で受け取ればよいのです。この構文がReactのuseState
と同じであることにご注目ください。これで、リアクティブな変数をモジュールで定めさえすれば、Reactの状態と同じように扱え、しかもアプリケーション全体で共有できるということです。
// import { useCallback } from 'react';
// import { makeVar, useReactiveVar } from '@apollo/client';
import { useCountVar } from './countVar';
// export const countVar = makeVar(0);
export const useCounter = () => {
// const count = useReactiveVar(countVar);
// const setCount = useCallback((value: number) => countVar(value), [])
const [count, setCount] = useCountVar();
};
書き改めたカウンターのカスタムフックのモジュール(src/useCounter.ts
)は、つぎのような記述になりました(コード008)。
コード008■カウンターのカスタムフックのモジュール
import { useCountVar } from './countVar';
export const useCounter = () => {
const [count, setCount] = useCountVar();
const decrement = () => setCount(count - 1);
const increment = () => setCount(count + 1);
return { count, decrement, increment };
};
Apollo Clientのリアクティブな変数を用いてつくる本稿のカウンター作例はこれででき上がりです。アプリケーションの動きはCodeSandboxに公開したつぎのサンプル004でお確かめください。
サンプル004■React + TypeScript: Using reactive variables of Apollo Client 03
-
GraphQLのクエリを使う場合については、「Reactive variables【Apollo Client】による local state 管理」が参考になるでしょう。 ↩