reduxに疲れたのでunstated-next を試してみた

# 0. 概要

毎回Reactでアプリを作ろうとするたびに、`redux`に精神をやられています。確かに堅牢だし、チーム開発に向いているかもしれないけど、記述量と学習コストが大きすぎる･･･。

いろいろ調べているうちに[unstated-next](https://github.com/jamiebuilds/unstated-next)というライブラリを発見したので、試してみました。
型付きじゃないと夜も眠れないタイプの人間なので、TypeScriptで実装しています。


# 1. 想定している読者

- `create-react-app`でアプリケーションを作ったことがある人
- `React Hooks`に興味のある人
- `TypeScript`がなんとなく分かる人
- `redux`に疲れている人

`React Hooks`については[公式サイト](https://ja.reactjs.org/docs/hooks-intro.html)の記事が充実しているのでそちらを参照してください。



# 2. 準備

## 2.1 プロジェクトの作成

カウンター値をボタンで操作したり表示したりする簡単なアプリケーションを作ってみます。

↓ 動作イメージ
![実装後.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/fdc79a0a-52be-eaf8-e3a0-71f74ccfc5b5.gif)


まずは`npx create-react-app`でサクッと React プロジェクトを作成。後ろに`--typescript`を付けると、TypeScript のプロジェクトになります。


```bash
# プロジェクトの作成
create-react-app sample --typescript
# プロジェクトに移動
cd sample
```

## 2.2 パッケージのインストール

今回使うのは`unstated-next`だけです。

```bash
npm install unstated-next -S
```

# 3. 実装
## 3.1 全体像

コードの説明の前に、全体像を載せてみます。矢印がデータやイベントの流れを示しています。

![全体像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/dacb87b2-2357-372e-1bd3-0330f2fa8e70.png)


`unstated-next`で登場するのは、２つだけです。

- ステート(カウンター値)の管理・操作方法を提供する`コンテナ`
- ステートの値を画面に表示させたり、イベントを受け渡す`コンポーネント`

ただし、コンポーネントは元々Reactにあるコンポーネントのことなので、実質覚えるのはコンテナの部分だけ。簡単！


それでは以上を踏まえて、全体コードをご覧ください。

```ts:CounterContainer.ts　(コンテナ)
import { useState } from "react";
import { createContainer } from "unstated-next";

/**
 * カウンター操作用コンテナのHooks
 */
const useCountContainer = () => {
  // カウンターの数値と、数値更新用の関数を取得する
  const [count, setCount] = useState<number>(0);

  /**
   * カウンタを加算する
   * @param amount 加算する量
   */
  const add = (amount: number) => {
    setCount((count) => count + amount);
  };

  /**
   * カウンタをリセットする
   */
  const reset = () => {
    setCount(0);
  };

  return { count, add, reset };
};

/** カウンター操作用コンテナ */
export const CounterContainer = createContainer(useCountContainer);
```

```tsx:CounterComponent.tsx　(コンポーネント)
import React from "react";
import { CounterContainer } from "./CounterContainer";

/**
 * カウンター操作用コンポーネントのプロパティ
 */
interface CounterOperateProps {
  /** 増減量 */
  amount: number;
}

/**
 * カウンター操作用コンポーネント
 */
const CouterOperate: React.FC<CounterOperateProps> = (props) => {
  const counterContainer = CounterContainer.useContainer();
  const onClick = () => {
    // 増減量の分だけカウンターを増やす (or 減らす)
    counterContainer.add(props.amount);
  };
  return <button onClick={onClick}>amount : {props.amount}</button>;
};

/**
 * カウンターリセット用コンポーネント
 */
const CouterReset: React.FC = () => {
  const counterContainer = CounterContainer.useContainer();
  const onClick = () => {
    // 増減量の分だけカウンターをリセットする
    counterContainer.reset();
  };
  return <button onClick={onClick}>リセット</button>;
};

/**
 * カウンター表示用コンポーネント
 */
const CouterDisplay: React.FC = () => {
  const counterContainer = CounterContainer.useContainer();
  return <div>count : {counterContainer.count}</div>;
};

/**
 * カウンターコンテナの提供用コンポーネント
 */
export const CounterComponent: React.FC = () => {
  return (
    <CounterContainer.Provider>
      <CouterOperate amount={1} />
      <CouterOperate amount={10} />
      <CouterOperate amount={-1} />
      <CouterReset />
      <CouterDisplay />
    </CounterContainer.Provider>
  );
};

```

## 3.2 コンテナについて

先に述べた通り、コンテナではステートの管理とステートを操作する関数を提供します。

まず必要になるのが、React Hooksの定義です。これは`unstated-next`の機能ではないので[React Hooks公式サイト](https://ja.reactjs.org/docs/hooks-intro.html)の説明をご参照ください。

やってることはこれだけです。

1. `useState`によりHooks内にステートの値とそれを操作する関数(`setCount`)を取得する
2. `setCount`をラップした`add関数`と`reset関数`を定義する。
2. 戻り値としてコンテナ外で使いたい値や関数を返す。


```ts:CounterContainer.ts
/**
 * カウンター操作用コンテナのHooks
 */
const useCountContainer = () => {
  // カウンターの数値と、数値更新用の関数を取得する
  const [count, setCount] = useState<number>(0);

  /**
   * カウンタを加算する
   * @param amount 加算する量
   */
  const add = (amount: number) => {
    setCount((count) => count + amount);
  };

  /**
   * カウンタをリセットする
   */
  const reset = () => {
    setCount(0);
  };

  return { count, add, reset };
};
```

Hooksが出来上がったら、`unstated-next`からimportした`createContainer`でコンテナを作成します。

```ts:CounterContainer.ts(続き)
/** カウンター操作用コンテナ */
export const CounterContainer = createContainer(useCountContainer);
```

## 3.3 コンポーネント側での利用

コンテナを利用するには２つの手順が必要です。
まず１つは、`Provider`によって、下位コンポーネントでコンテナを扱えるようにすることです。
サンプルでは、`<CounterContainer.Provider>`の部分がそれです。これによって`CouterOperate `や`CouterDisplay`がコンテナに触れるようになっています。

```tsx:CounterContainer.tsx
import { CounterContainer } from "./CounterContainer";

// (中略)
 
/**
 * カウンターコンテナの提供用コンポーネント
 */
export const CounterComponent: React.FC = () => {
  return (
    <CounterContainer.Provider>
      <CouterOperate amount={1} />
      <CouterOperate amount={10} />
      <CouterOperate amount={-1} />
      <CouterReset />
      <CouterDisplay />
    </CounterContainer.Provider>
  );
};
```

次が、`useContainer`によるコンテナの読み込みです。
コンポーネント内で`CounterContainer.useContainer()`を呼び出すことによって、コンテナの定義時に作成したHooksの戻り値である`count`の値や、`add`関数、`reset`関数に触れるようになります。

```tsx:CounterContainer.tsx
/**
 * カウンター操作用コンポーネント
 */
const CouterOperate: React.FC<CounterOperateProps> = (props) => {
  const counterContainer = CounterContainer.useContainer();
  const onClick = () => {
    // 増減量の分だけカウンターを増やす (or 減らす)
    counterContainer.add(props.amount);
  };
  return <button onClick={onClick}>amount : {props.amount}</button>;
};

/**
 * カウンターリセット用コンポーネント
 */
const CouterReset: React.FC = () => {
  const counterContainer = CounterContainer.useContainer();
  const onClick = () => {
    // 増減量の分だけカウンターをリセットする
    counterContainer.reset();
  };
  return <button onClick={onClick}>リセット</button>;
};

/**
 * カウンター表示用コンポーネント
 */
const CouterDisplay: React.FC = () => {
  const counterContainer = CounterContainer.useContainer();
  return <div>count : {counterContainer.count}</div>;
};
```

# 4 まとめ

「いやこれ、ステートの管理をコンポーネントの外に出しただけジャン」と思われるかもしれません。実際その通りです。
ただ、どのコンポーネントでもプロパティを介さずにグローバル変数のようにコンテナを扱うことガできるのは大きな利点です。「ステートを外に出しただけ」なので学習コストもかなり低いです。

React Hooks自体に制約が多いのでそちら側の慣れは必要ですが、制約に反した書き方をすると警告が出るようになっているのでミスには気づきやすいかと思います。

reduxに比べて発展途上のライブラリではありますが、記述量の少なさに感動することひとしおです。お試しあれ。



(不勉強なところが多いので、ご指摘よろしくお願いします)