React + Unstated Next: 複数コンポーネントのツリーの中で状態を共有して管理する

[Unstated Next](https://github.com/jamiebuilds/unstated-next#unstated-next)は、複数コンポーネントにより組み立てられたツリーの中で、状態を共有して管理するライブラリです。簡単なサンプルをつくりながら、使い方についてご紹介します。

[注記: 2020/11/03]「[コードを改善する](https://qiita.com/FumioNonaka/items/95603658819108da7552#%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E6%94%B9%E5%96%84%E3%81%99%E3%82%8B)」の項を追加。

# Unstated Nextの特徴
Reactに[フック](https://ja.reactjs.org/docs/hooks-intro.html)が採り入れられて、[`useContext`](http://fumiononaka.com/Business/html5/FN2005005.html)を使えばReduxに頼らなくても、扱う状態の規模がさほど大きくなければ手軽に管理できるようになりました。Unstated Nextは、それをさらにシンプルにしてくれるライブラリです。

Reactのカスタムフックと[コンテクスト](https://ja.reactjs.org/docs/context.html#gatsby-focus-wrapper)がわかっていれば、すぐに使いはじめられます。APIが最小限にまとめられ、ライブラリのサイズはわずか200バイトです。

Reactのコンテクストに当たる状態のまとめ役を、Unstated Nextではコンテナと呼びます。ひとつの状態をまとめて管理するReduxと比べると、コンテナは小分けできることもパフォーマンスの点からは有利です。ただしよく考えて設計しないと、結局コンテナが何重にも入れ子になってしまうことには、注意しなければなりません。

# はじめの一歩
まず、Reactアプリケーションのひな形は、[Create React App](https://ja.reactjs.org/docs/create-a-new-react-app.html#create-react-app)でつくりましょう。コマンドラインツールで`npx create-react-app`につづけて、アプリケーション名(今回は`react-unstated-next-example`)を打ち込んでください。

```terminal
npx create-react-app react-unstated-next-example
```

アプリケーション名でつくられたディレクトリに切り替えて(`cd react-unstated-next-example`)、コマンド`yarn start`でひな形アプリケーションのページがローカルホスト(`http://localhost:3000/`)で開くはずです。

つぎに、このディレクトリにインストールするのはUnstated Next(`unstated-next`)です。`yarn add`コマンドでライブラリが加えられます。

```terminal
yarn add unstated-next
```

yarnでなく、`npm install`コマンドでインストールしても構いません。

```terminal
npm install --save unstated-next
```


# カスタムフックと`useContext`を使ってつくるカウンター
カスタムフックとコンテクストがわかれば、Unstated Nextはすぐに使えます。ということで、まずは素のReactのカスタムフックと`useContext`だけで、ライブラリは使わずにカウンターのアプリケーションをつくってみましょう。

## カスタムフックをつくる
「フックとは、関数コンポーネントにstateやライフサイクルといった Reactの機能を"接続する(hook into)"ための関数です」(「[要するにフックとは？](https://ja.reactjs.org/docs/hooks-overview.html#but-what-is-a-hook)」)。さらに、フックを独自につくって、コンポーネントからロジックを切り出すこともできます。そうすれば、コンポーネントのコードがすっきり見やすくなるとともに、そのカスタムフックを使い回すこともできるのです。

> 自分独自のフックを作成することで、コンポーネントからロジックを抽出して再利用可能な関数を作ることが可能です。
> (「[独自フックの作成](https://ja.reactjs.org/docs/hooks-custom.html)」より)

カスタムフックのモジュール`src/useCounter.js`の定めはコード001のとおりです。カウンターのロジックですから、値の状態変数(`count`)にその設定関数(`setCount()`)、減算(`decrement()`)と加算(`increment()`)の関数を加えました。

カスタムフックは、関数コンポーネントと異なり、JSXで要素を返す必要はありません。戻り値のオブジェクトに収めたのは、状態変数(`count`)と減算(`decrement()`)および加算(`increment()`)の関数です。なお、カスタムフックの基本的な役割や考え方については「[React: コンポーネントのロジックをカスタムフックに切り出す ー カウンターの作例で](https://qiita.com/FumioNonaka/items/62285e474608388cbbe3)」をお読みください。

#### コード001■カウンターのカスタムフック
```react:src/useCounter.js
import { useState } from "react";

export const useCounter = (initialState = 0) => {
	const [count, setCount] = useState(initialState);
	const decrement = () => setCount(count - 1);
	const increment = () => setCount(count + 1);
	return { count, decrement, increment };
};
```


## カウンターを表示・操作するコンポーネントの作成
カウンターを表示・操作するコンポーネントが以下のコード002です。フック[`useContext`](https://ja.reactjs.org/docs/hooks-reference.html#usecontext)は、このあとアプリケーション(`App`)でつくられるコンテクスト(`CounterContext`)から、カスタムフック(`useCounter`)が返すオブジェクト(`counter`)を取り出します。その中の状態変数(`counter.count`)や減算(`counter.decrement`)・加算(`counter.increment`)の関数を、それぞれの要素に割り当てればよいのです。

#### コード002■カウンター表示のコンポーネント
```react:src/CounterDisplay.js
import React, { useContext } from "react";
import { CounterContext } from './App';

const CounterDisplay = () => {
	const counter = useContext(CounterContext);
	return (
		<div>
			<button onClick={counter.decrement}>-</button>
			<span>{counter.count}</span>
			<button onClick={counter.increment}>+</button>
		</div>
	);
}
export default CounterDisplay;
```


## コンテクストの`Provider`で子コンポーネントを包む
アプリケーションのモジュール(`src/App.js`)でコンテクスト(`CounterContext`)をつくります(コード003)。そのために呼び出す関数が[`createContext()`](https://ja.reactjs.org/docs/context.html#reactcreatecontext)です。コンテクストは[`Context.Provider`](https://ja.reactjs.org/docs/context.html#contextprovider)コンポーネントを備えています。このコンポーネントに含めた子はすべて、コンテクストが参照できるという仕組みです。

コンテクストに与える変数や関数の参照は、`Context.Provider`コンポーネントの`value`プロパティに与えてください。今回はカスタムフック`useCounter`から得たオブジェクト(`counter`)が渡されました。

#### コード003■コンテクストの`Provider`の`value`に参照するオブジェクトを与える
```react:src/App.js
import React, { createContext } from 'react';
import { useCounter } from './useCounter';
import CounterDisplay from './CounterDisplay';
import './App.css';

export const CounterContext = createContext();
function App() {
	const counter = useCounter();
	return (
		<CounterContext.Provider value={ counter }>
			<div className="App">
				<CounterDisplay />
			</div>
		</CounterContext.Provider>
	);
}
export default App;
```

これで、コンテクストを使ったカウンターができあがりました(図001)。

#### 図001■コンテクストを使ったカウンター
![2009001_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/857a2ed8-98ae-8665-31d0-c2189bc4ab26.png)


# Unstated Nextでカウンターをつくり替える
カスタムフックとコンテクストがわかりましたので、カウンターをUnstated Nextで動くようにつくり直して見ましょう。コンテクストに替えて、コンテナをつくります。


## カスタムフックからコンテナをつくる
モジュール`src/useCounter.js`のカスタムフックは基本的に変わりません。フックを関数[`createContainer()`](https://github.com/jamiebuilds/unstated-next#createcontainerusehook)でコンテナに包むのです。コンテナ(`CounterContainer`)は、いわばカスタムフック(`useCounter`)のロジックを備えたコンテクストといえます。

```react:src/useCounter.js
import { createContainer } from "unstated-next";

// export const useCounter = (initialState = 0) => {
const useCounter = (initialState = 0) => {

};

export const CounterContainer = createContainer(useCounter);
```


## コンポーネントをコンテナの`Provider`で包む
アプリケーションモジュール`src/App.js`は、コンテクストをUnstated Nextのコンテナに差し替えます。コンテナにもコンテクストと同じように[`<Container.Provider>`](https://github.com/jamiebuilds/unstated-next#containerprovider)が備わっているのです。`Provider`もコンテクストからコンテナに書き替えてください。ただし、`value`プロパティは要りません。

```react:src/App.js
// import React, { createContext } from 'react';
import React from 'react';
// import { useCounter } from './useCounter';
import { CounterContainer } from './useCounter';

// export const CounterContext = createContext();
function App() {
	// const counter = useCounter();
	return (
		// <CounterContext.Provider value={ counter }>
		<CounterContainer.Provider>

		{/* </CounterContext.Provider> */}
		</CounterContainer.Provider>
	);
}
```


## コンテナのロジックをコンポーネントが使う
コンテナはカスタムフックのロジックを備えているのでした。コンテナ(`CounterContainer`)に対して[`useContainer()`](https://github.com/jamiebuilds/unstated-next#usecontainercontainer)を呼び出すと、ロジックの参照が得られるのです。参照はコンテクストを使ったときと同じ変数(`counter`)に収めれば、ほかに書き直すところはありません。

```react:src/CounterDisplay.js
// import React, {useContext} from "react";
import React from "react";
// import { CounterContext } from './App';
import { CounterContainer } from './useCounter';

function CounterDisplay() {
  // const counter = useContext(CounterContext);
  const counter = CounterContainer.useContainer();

}
```

これでカウンターはUnstated Nextのコードに書き替えられました。モジュール3つの記述を以下のコード004にまとめます。カスタムフックとコンテクストでも組み立ては簡単でした。でも、ふたつをまとめたコンテナを使うことでさらにシンプルになったでしょう。CodeSandboxに作例をサンプル001として掲げました。

#### コード004■Unstated Nextを使ったカウンター
```react:src/useCounter.js
import { useState } from "react";
import { createContainer } from "unstated-next";

const useCounter = (initialState = 0) => {
	const [count, setCount] = useState(initialState);
	const decrement = () => setCount(count - 1);
	const increment = () => setCount(count + 1);
	return { count, decrement, increment };
};

export const CounterContainer = createContainer(useCounter);
```

```react:src/App.js
import React from 'react';
import { CounterContainer } from './useCounter';
import CounterDisplay from './CounterDisplay';
import './App.css';
function App() {
	return (
		<CounterContainer.Provider>
			<div className="App">
				<CounterDisplay />
			</div>
		</CounterContainer.Provider>
	);
}
export default App;
```

```react:src/CounterDisplay.js
import React from "react";
import { CounterContainer } from './useCounter';

const CounterDisplay = () => {
  const counter = CounterContainer.useContainer();
	return (
		<div>
			<button onClick={counter.decrement}>-</button>
			<span>{counter.count}</span>
			<button onClick={counter.increment}>+</button>
		</div>
	);
}
export default CounterDisplay;
```

#### サンプル001■Unstated Nextでカウンターをつくる
[![2009001_002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/ff53c6a1-7872-f96d-a3cd-560988b1a5c9.png)
\>> CodeSandboxへ](https://codesandbox.io/s/react-unstated-next-counter-example-3ifur?file=/src/App.js)


# コードを改善する
Unstated Nextを使った状態管理の仕組みがわかるよう、コードはできるだけ簡略化しました。けれど、実際にはもう少し改善した方がよいでしょう。

まず、状態を変えようとするとき、できることなら状態変数は参照しないことが望ましいといえます。少し込み入った処理では、最新の値が得られない場合もあるからです。直近の状態変数値にもとづいてその値を変えたいとき、状態設定関数(`setCount`)には関数を渡して「[関数型の更新](https://ja.reactjs.org/docs/hooks-reference.html#functional-updates)」ができます。更新関数の受け取る引数(`prevCount`)が直近の変数値です。


```react:src/useCounter.js
const useCounter = (initialState = 0) => {
	const [count, setCount] = useState(initialState);
	// const decrement = () => setCount(count - 1);
	const decrement = () => setCount((prevCount) => prevCount - 1);
	// const increment = () => setCount(count + 1);
	const increment = () => setCount((prevCount) => prevCount + 1);
	return { count, decrement, increment };
};
```

つぎに、カウンターのボタンのコールバックに定められた関数は、コンポーネントがレンダーされるたびにつくり直されます。[`useCallback`](https://ja.reactjs.org/docs/hooks-reference.html#usecallback)フックでラップすれば、関数に用いられる値が同じなら前の定義がそのまま用いられます(「メモ化」と呼ばれます)。ただし、useCallbackフックの第2引数に、配列で依存する値を与えなければなりません。それらの値が同じかどうかで、関数のつくり直しを決めるのです。

なお、依存配列を空で渡すと、依存なしとなり、初期化時のみ実行されます。状態設定関数の引数を関数型の更新で定めたときは、その引数値は依存配列に加える必要がありません。改善した`src/useCounter.js`の記述は、つぎのコード005のとおりです。CodeSandboxにサンプル002を公開しました。

#### コード005■コンテナのコードを改善
```react:src/useCounter.js
import { useCallback, useState } from "react";
import { createContainer } from "unstated-next";

const useCounter = (initialState = 0) => {
	const [count, setCount] = useState(initialState);
	const decrement = useCallback(() => setCount((prevCount) => prevCount - 1), []);
	const increment = useCallback(() => setCount((prevCount) => prevCount + 1), []);
	return { count, decrement, increment };
};

export const CounterContainer = createContainer(useCounter);
```

#### サンプル002■コンテナのコードを改善したカウンター
[![2009001_002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/ff53c6a1-7872-f96d-a3cd-560988b1a5c9.png)
\>> CodeSandboxへ](https://codesandbox.io/s/react-unstated-next-counter-example-refactored-j65q4?file=/src/useCounter.js)