React + TypeScript: Apollo Clientのリアクティブな変数(reactive variables)で複数コンポーネント間の状態を管理する

[Apollo Client](https://www.apollographql.com/docs/react/)はReactで使える状態管理ライブラリです。ローカルとリモートのデータをGraphQLで扱えます。本稿が解説するのは、GraphQLのクエリは用いず、ローカルの状態を複数コンポーネントの間でどう共有するかです。具体的には、リアクティブな変数(「[Reactive variables](https://www.apollographql.com/docs/react/local-state/reactive-variables/)」)の使い方になります(「[Local State Management with Reactive Variables](https://www.apollographql.com/blog/apollo-client/caching/local-state-management-with-reactive-variables/)」参照)。

Apollo Clientは、データを扱う技術としてGraphQLと並んで注目されています。「[データ層](https://2020.stateofjs.com/ja-JP/technologies/datalayer/)([Data Layer](https://2020.stateofjs.com/en-US/technologies/datalayer/))」のグラフは、JavaScriptに興味がある世界中のIT技術者約2万4000人に向けて、2020年にアンケートを集めた結果です。利用度と認知度こそ定番Reduxに及ばないものの、満足度や興味はGraphQLにつぐ2位につけています。前述のとおり、本稿はGraphQLには触れません。けれど、合わせて使うなら最強のライブラリでしょう。

#### 「データ層」2020年アンケート結果
| 技術 | 満足度 | 興味 | 利用度 | 認知度 |
| --- | --- | --- | --- | --- |
| GraphQL | 1位(94%) | 1位(87%) | 2位(47%) | 1位(98%) |
| Apollo Client | 2位(88%) | 2位(75%) | 3位(33%) | 3位(75%) |
| Redux | 5位(67%) | 4位(55%) | 1位(67%) | 2位(97%) |


# `useState`で状態をもつ簡単なカウンター作例
まずは、Apollo Clientは用いることなく、`useState`で状態をもつ簡単なカウンターの作例です(コード001)。

ルートモジュール(`src/App.tsx`)はカウンター表示のコンポーネント(`CounterDisplay`)を読み込んでアプリケーションのページに加えます。コンポーネントのJSXに差し込まれているのは加算・減算のボタンと数値の表示要素です。カウンターの数値をもつ状態変数(`count`)とその増減のための関数(`setCounter`)は、このあとに定めるカスタムフック(`useCounter`)から受け取っています。増減の関数はボタンに加えた`onClick`ハンドラに与えました。

`useState`フックで状態変数(`count`)とその設定関数(`setCount`)を保持するのが、カスタムフックのモジュール(`src/useCounter.ts`)です。カウンター数値増減の関数(`setCounter`)は、引数に受け取った数値をカウンターの現在値に足し込みます。フックからは、状態変数とカウンター増減の関数をオブジェクトに収めて返しました。

#### コード001■ボタンで数値を増減するカウンター
```react:src/App.tsx
import { CounterDisplay } from './CounterDisplay';
import './App.css';

function App() {
	return (
		<div className="App">
			<CounterDisplay />
		</div>
	);
}

export default App;
```

```react:src/CounterDisplay.tsx
import { FC } from 'react';
import { useCounter } from './useCounter';

export const CounterDisplay: FC = () => {
	const { count, setCounter } = useCounter();
	return (
		<div>
			<button onClick={() => setCounter(-1)}>-</button>
			<span>{count}</span>
			<button onClick={() => setCounter(1)}>+</button>
		</div>
	);
};
```

```react:src/useCounter.ts
import { useCallback, useState } from 'react';

export const useCounter = () => {
	const [count, setCount] = useState(0);
	const setCounter = useCallback(
		(step: number) => setCount(count + step),
		[count]
	);
	return { count, setCounter };
};
```

これで、`useState`フックを使った簡単なカウンターのでき上がりです。各モジュールのコードと動きは、CodeSandboxに公開したサンプル001でお確かめください。なお、関数コンポーネントのTypeScrptによる型づけは、v18からは`React.FunctionComponent`(`React.FC`)を用いることになりました。`React.VoidFunctionComponent`(`React.VFC`)は推奨されません(「[関数コンポーネント](https://qiita.com/FumioNonaka/items/4361d1cdf34ffb5a5338#%E9%96%A2%E6%95%B0%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88)」参照)。

#### サンプル001■React + TypeScript: Using reactive variables of Apollo Client 01
https://codesandbox.io/s/react-typescript-using-reactive-variables-of-apollo-client-01-ez7zi0?file=/src/index.tsx


# ボタンと数値表示をモジュール分けする
もっとも、今回のお題は複数コンポーネント間で、ひとつの状態をどう扱うかです。コンポーネントは分けましょう。まず、カウンターの数値を表示するコンポーネント(`CounterNumber`)です。つぎに、カウンター増減のボタン(`CounterButton`)は、加算と減算のふたつで使い回します。それらをカウンター表示のモジュール(`src/CounterDisplay.tsx`)が読み込んで、JSXに差し込むというわけです。

ただし、まだApollo Clientは使いません。この場合、親コンポーネント(`CounterDisplay`)がカスタムフック(`useCounter`)から受け取った状態変数(`count`)と数値増減の関数(`setCounter`)は、子コンポーネントにプロパティとして渡すのがReactではお約束です。また、ボタン(`CounterButton`)は使い回すので、増減する数値(`step`)とボタンのテキスト(`text`)もプロパティに加えます。

```react:src/CounterDisplay.tsx
import { CounterNumber } from './CounterNumber';
import { CounterButton } from './CounterButton';

export const CounterDisplay: FC = () => {

	return (
		<div>
			{/* <button onClick={() => setCounter(-1)}>-</button> */}
			<CounterButton setCounter={setCounter} step={-1} text="-" />
			{/* <span>{count}</span> */}
			<CounterNumber count={count} />
			{/* <button onClick={() => setCounter(1)}>+</button> */}
			<CounterButton setCounter={setCounter} step={1} text="+" />
		</div>
	);
};
```

そして、子コンポーネントは親からプロパティとして受け取った変数(`count`)や関数(`setCounter`)などを、JSXに組み込むのです。

```react:src/CounterNumber.tsx
import { FC } from 'react';

type Props = {
	count: number;
};
export const CounterNumber: FC<Props> = ({ count }) => {
	return <span>{count}</span>;
};
```

```react:src/CounterButton.tsx
import { FC } from 'react';

type Props = {
	setCounter: (step: number) => void;
	step: number;
	text: string;
};
export const CounterButton: FC<Props> = ({ setCounter, step, text }) => {
	return <button onClick={() => setCounter(step)}>{text}</button>;
};
```

これが、標準Reactの作法にしたがった、コンポーネント間での状態の受け渡しです。はじめの作例と同じく、カウンターの値は正しく増減できます。細かいコードや動きは、CodeSandboxに上げたサンプル002でご確認ください。

#### サンプル002■React + TypeScript: Using reactive variables of Apollo Client 02
https://codesandbox.io/s/react-typescript-using-reactive-variables-of-apollo-client-02-p0y15c?file=/src/CounterDisplay.tsx


# 各コンポーネントからカスタムフックを呼び出そうとすると
ここから、Apollo Clientの導入です。親モジュール(`src/CounterDisplay.tsx`)からカスタムフックの呼び出しを外します。つまり、もはや状態をもちません。ないものは子コンポーネントにも渡せないことになります。

```react:src/CounterDisplay.tsx
// import { useCounter } from './useCounter';
import { CounterNumber } from './CounterNumber';
import { CounterButton } from './CounterButton';

export const CounterDisplay: FC = () => {
	// const { count, setCounter } = useCounter();
	return (
		<div>
			{/* <button onClick={() => setCounter(-1)}>-</button> */}
			<CounterButton step={-1} text="-" />
			{/* <span>{count}</span> */}
			<CounterNumber />
			{/* <button onClick={() => setCounter(1)}>+</button> */}
			<CounterButton step={+1} text="+" />
		</div>
	);
};
```

カスタムフック(`useCounter`)は、子コンポーネントがそれぞれ呼び出して、状態を取得・設定しようというのです。

```react:src/CounterNumber.tsx
import { useCounter } from './useCounter';

/* type Props = {
	count: number;
}; */
// export const CounterNumber: FC<Props> = ({ count }) => {
export const CounterNumber: FC = () => {
	const { count } = useCounter();

};
```

```react:src/CounterButton.tsx
import { useCounter } from './useCounter';

type Props = {
	// setCounter: (step: number) => void;

};
// export const CounterButton: FC<Props> = ({ setCounter, step, text }) => {
export const CounterButton: FC<Props> = ({ step, text }) => {
	const { setCounter } = useCounter();

};
```

もちろん、これではカウンターは正しく動きません(コード002)。カスタムフック(`useCounter`)から得た状態は、呼び出したコンポーネントごとに閉じているからです。加算や減算のボタンをクリックしても、それぞれのボタンの状態変数(`count`)が増減するだけで、カウンターの数値表示の状態は変わりません。

#### コード002■子コンポーネントがそれぞれカスタムフックを呼び出す
```react:src/CounterNumber.tsx
import { FC } from 'react';
import { useCounter } from './useCounter';

export const CounterNumber: FC = () => {
	const { count } = useCounter();
	return <span>{count}</span>;
};
```

```react:src/CounterButton.tsx
import { FC } from 'react';
import { useCounter } from './useCounter';

type Props = {
	step: number;
	text: string;
};
export const CounterButton: FC<Props> = ({ step, text }) => {
	const { setCounter } = useCounter();
	return <button onClick={() => setCounter(step)}>{text}</button>;
};
```

```react:src/CounterDisplay.tsx
import { FC } from 'react';
import { CounterNumber } from './CounterNumber';
import { CounterButton } from './CounterButton';

export const CounterDisplay: FC = () => {
	return (
		<div>
			<CounterButton step={-1} text="-" />
			<CounterNumber />
			<CounterButton step={1} text="+" />
		</div>
	);
};
```


# Apollo Clientのリアクティブな変数で状態を扱う
前掲コード002の3つのモジュールのコードには手を触れません。カスタムフックのモジュール(`src/useCounter.ts
`)にApollo Clientを組み込みます。ここで、Apollo Client(`@apollo/client`)を[インストール](https://www.apollographql.com/docs/react/get-started/#1-setup)しましょう。今回、GraphQL(`graphql`)のクエリは使わないものの、依存があるのでインストールに加えてください。

```terminal
npm install @apollo/client graphql
```

Apollo Clientでは、アプリケーションで共有する状態を、ひとつひとつリアクティブな変数([Reactive variable](https://www.apollographql.com/docs/react/local-state/reactive-variables/))として定めます(「[Announcing the Release of Apollo Client 3.0](https://www.apollographql.com/blog/announcement/frontend/announcing-the-release-of-apollo-client-3-0/#reactive-variables)」の「Reactive variables」参照)。変数をつくるのは、文字どおり[`makeVar`](https://www.apollographql.com/docs/react/local-state/reactive-variables/#creating)というメソッドです。引数には初期値を与えます(「[Initialize reactive variables](https://www.apollographql.com/docs/tutorial/local-state/#initialize-reactive-variables)」参照)。そして、返されるのはリアクティブな変数値を設定するための関数です。

```react
const 設定関数 = makeVar(初期値);
```

その設定関数に引数として新たな値を渡して呼び出せば、リアクティブな変数値が改められます(引数を省くと、返されるのは変数の現行値です)。

そして、値が保持されるリアクティブな状態変数の参照は、コンポーネントから[`useReactiveVar`](https://www.apollographql.com/docs/react/local-state/reactive-variables/)フックを呼び出して得られます(「[Reacting](https://www.apollographql.com/docs/react/local-state/reactive-variables/#reacting)」参照)。フックの引数に渡すのは`makeVar`から返された関数です。

```react
const リアクティブな状態変数 = useReactiveVar(設定関数);
```

カスタムフックのモジュール(`src/useCounter.ts`)は、つぎのように書き改めます。大事なのは、フック(`useCounter`)の外で`makeVar`がリアクティブな変数を初期化していることです。つまり、フックがどこから呼び出されようと、モジュールが参照するリアクティブな変数はひとつになります。コンポーネント間で変数の参照が共有化されるのです。フックが返すのは、状態変数(`count`)とカウンターの設定関数(`setCounter`)のまま、変わりありません。

```react:src/useCounter.ts
// import { useCallback, useState } from 'react';
import { useCallback } from 'react';
import { makeVar, useReactiveVar } from '@apollo/client';

const countVar = makeVar(0);
export const useCounter = () => {
	// const [count, setCount] = useState(0);
	const count = useReactiveVar(countVar);
	// const setCounter = useCallback((step: number) => setCount(count + step), [
	const setCounter = useCallback((step: number) => countVar(count + step), [
		count
	]);

};
```

書き直したカスタムフックのモジュール(`src/useCounter.ts`)の記述全体は、つぎのコード003のとおりです。これで、前掲コード002のモジュールが、カウンターとして正しく動くようになります。CodeSandboxに掲げたのが以下のサンプル003です。

#### コード003■Apollo Clientのリアクティブな変数に状態をもたせたカスタムフック
```react:src/useCounter.ts
import { useCallback } from 'react';
import { makeVar, useReactiveVar } from '@apollo/client';

const countVar = makeVar(0);
export const useCounter = () => {
	const count = useReactiveVar(countVar);
	const setCounter = useCallback((step: number) => countVar(count + step), [
		count
	]);
	return { count, setCounter };
};
```

#### サンプル003■React + TypeScript: Using reactive variables of Apollo Client 03
https://codesandbox.io/s/react-typescript-using-reactive-variables-of-apollo-client-03-4t4ko1?file=/src/useCounter.ts

なお、CodeSandboxでは、`graphql`のバージョンを16以上にすると、つぎのようなTypeErrorが出てしまうようです。お気をつけください。

> `Cannot read properties of undefined (reading 'QUERY')`


# リアクティブな変数を汎用的なカスタムフックでつくる
Apollo Clientのリアクティブな変数は、Reduxのように状態をひとまとめにするのでなく、それぞれ個別に管理できます。反面、変数の数だけモジュールが増えてしまうのは悩みどころです。統一的な扱いができるように、汎用的なカスタムフックを定めました(コード004)。

モジュール`src/useReactiveVarHooks.ts`のカスタムフック`useReactiveVarHooks`は、引数として`makeVar`の返す設定関数(`reactiveVar`)を受け取ります。そして、`useReactiveVar`フックでリアクティブな変数の参照(`value`)を得たうえで、フックの戻り値はその参照と設定関数に値を渡す関数(`setValue`)の配列です。変数にどのような値も定められるように、TypeScriptの型づけはジェネリックを用いた少し面倒な記述になりました。

リアクティブな変数のモジュール(`src/countVar.ts`)は、まず`makeVar`で値を初期化し、共有値として保持します。そのうえで、モジュールのカスタムフック(`useCountVar`)が返すのは、汎用的なフック(`useReactiveVarHooks`)に設定関数(`countVar`)を渡した戻り値の配列そのままです。

#### コード004■汎用的なカスタムフックとリアクティブな変数のモジュール
```react:src/useReactiveVarHooks.ts
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) => {
			reactiveVar(payload);
		},
		[reactiveVar]
	);
	return useMemo(() => [value, setValue]
	, [setValue, value]);
};
```

```react:src/countVar.ts
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`)は、変数のフック(`useCountVar`)を`import`して呼び出します。受け取るのは、変数の参照(`count`)と設定関数(`setCount`)を要素とする配列です。この構文がReactの組み込みフック`useState`と同じであることにご注目ください。つまり、`useState`と同じように使いつつ、しかも状態変数がモジュール間で共有できるということです。

```react: src/useCounter.ts
// import { makeVar, useReactiveVar } from '@apollo/client';
import { useCountVar } from './countVar';

// const countVar = makeVar(0);
export const useCounter = () => {
	// const count = useReactiveVar(countVar);
	const [count, setCount] = useCountVar();
	// const setCounter = useCallback((step: number) => countVar(count + step), [
	const setCounter = useCallback((step: number) => setCount(count + step), [
		count,
		setCount
	]);
	return { count, setCounter };
};
```

リアクティブな変数を用いるモジュール(`src/useCounter.ts`)の書き改めた記述は、以下のコード005のとおりです。各モジュールのコードや動きは、CodeSandboxに掲げたサンプル004でお確かめください。

Apollo Clientを使うと、Reduxのように大掛かりになることなく、状態が変数ごとに切り分けられ、しかもモジュール間で共有できます。とくに、GraphQLも使う場合には、イチ推しのライブラリです(GraphQLの使い方については、「[React + TypeScript: Apollo ClientのGraphQLクエリを使ってみる](https://qiita.com/FumioNonaka/items/0c6b711627e3443ff73b)」をご参照ください)。

#### コード005■リアクティブな変数のフックから汎用的なフックの戻り値を使う
```react: src/useCounter.ts
import { useCallback } from 'react';
import { useCountVar } from './countVar';

export const useCounter = () => {
	const [count, setCount] = useCountVar();
	const setCounter = useCallback((step: number) => setCount(count + step), [
		count,
		setCount
	]);
	return { count, setCounter };
};
```

#### サンプル004■React + TypeScript: Using reactive variables of Apollo Client 04
https://codesandbox.io/s/react-typescript-using-reactive-variables-of-apollo-client-04-f5xqzn?file=/src/useCounter.ts