【React】学習コストゼロのステート管理は unreduxed で。

redux を代表するステート管理ライブラリは学習コストが高めですよね。ライブラリ独自の記法を覚えなくちゃいけないしライブラリ同士の組み合わせ方も考えなければならない。

React hooks の `useState` や `useReducer` で宣言するステートがそのまま他のコンポーネントと共有できたら楽だと思いませんか？
普段からコンポーネント単位では使い慣れているであろうフック関数でグローバルステートも管理できれば、ステート管理ライブラリについて学習する必要はないはずです。

そんなライブラリがあるのでしょうか？

あります。私が作りました。

# `unreduxed` の紹介

## リポジトリ

https://github.com/y-hiraoka/unreduxed

## 特徴

- **軽量**
  `unreduxed` は非常に軽量であなたのアプリケーションの負担になりません。ライブラリをビルドした後のjsファイルはたった88行で、 React にのみ依存しているため気兼ねなくインストールできます。


- **フックベース**
  React hooks についてのみ知っていればすぐにプロジェクトに導入することができます。ライブラリのために何かを学ぶ必要はありません。あなたの作ったカスタムフックを `unreduxed` で味付けするだけで、すぐにステートを複数コンポーネントに共有することができます。

- **TypeScript フレンドリー**
  ライブラリは TypeScript で作成されています。ライブラリを使用する際は型推論が効くようになっているため開発者体験は高いでしょう。もちろん JavaScript からも使用可能です。

- **高パフォーマンス**
  React における悩みの一つに、余計な再レンダリングをチューニングしなければならないことが挙げられます。 React context で共有されたステートを `useContext` で取得したらどうでしょう？再レンダリング避けられませんね。 `props` バケツリレーと `React.memo` でがんばりますか？つらいですね。`react-redux` 導入しますか？学習コストがかかりますね。`unreduxed` を使えば、共有されているステートがどんなに巨大でも、各コンポーネントは関心のある値の変化のみ検知して再レンダリングされます。無駄な再レンダリングは責任持って抑制させていただきます。

## 使い方

ライブラリの使い方を説明していきいます。ソースコードの記載が多くなりますがまったく読むことに苦痛はないはずです。なぜなら普段からカスタムフックに読み慣れているはずですから。

これから提示していくサンプルコードを含んだデモアプリはこちらの codesandbox に用意しています。なお、サンプルコードは TypeScript を使用しています。
https://codesandbox.io/s/unreduxed-demo-app-for-qiita-74zow

### インストール方法

```
npm i unreduxed
```

### コンテナフックを作成する

共有したいステートを持つカスタムフックを、コンテナフックと呼ぶことにしましょう。名前をつけただけです。あなたがいつも作っているカスタムフックを用意していただければ良いです。
このコンテナフックに初期値を注入したい場合は、フックの引数から受け取ることができます。一つ注意してもらいたいことは、引数は `undefined` を考慮してほしいということです。TypeScript なら `?` マークをつけて宣言してください。これはライブラリの型定義上避けられません。

```ts:container.ts
import React from "react";
import unreduxed from "unreduxed";

const useCounter = (init?: number) => {
  const [count, setCount] = React.useState(init ?? 0);

  const increment = React.useCallback(() => setCount((prev) => prev + 1), []);
  const decrement = React.useCallback(() => setCount((prev) => prev - 1), []);

  return { count, increment, decrement };
};

export const [ContainerProvider, useContainer] = unreduxed(useCounter);
```

繰り返しになりますが、 `useCounter` はただのカスタムフックです。つまり、 [フックのルール](https://ja.reactjs.org/docs/hooks-rules.html)さえ守られていれば、何をやってもいいのです。いくつも `useState` を宣言してもいいでしょう。 `useEffect` を使用してもけっこうです。サードパーティライブラリが提供するカスタムフックを使用してももちろんかまいません。

作成したコンテナフックは `unreduxed` 関数に渡してください。戻り値は固定長のタプルになっており、1つ目が `ContainerProvider` で、2つ目が `useContainer` です(固定長タプルなので、受け取る変数は好きな名前がつけられます)。

私は `ContainerProvider` と `useContainer` という名前をつけてみました。この名前から使い方のイメージが湧く人も多いでしょう。`ContainerProvider` はコンテナ(ステート)を共有したいコンポーネントツリーのトップに配置します。 `useContainer` は `ContainerProvider` のツリー内部で使用される必要があります。これは React context の `Provider` と `useContext` の関係と似ていますね(実際にライブラリの内部で React context が用いられているのでこの構造になっています)。

### `ContainerProvider` を設置する

`unreduxed` 関数から受け取った `ContainerProvider` を設置します。設置する場所はステートを共有したい範囲のトップレベルです。グローバルステートなら `index.tsx` や `App.tsx` といった場所に置くことになるでしょう。

```tsx:App.tsx
import React from "react";
import { ContainerProvider, useContainer } from "./container";
import styles from "./styles.module.css";

export default function App() {
  return (
    <div className={styles.root}>
      <ContainerProvider>
        <Count />
        <CountButtons />
        <ContainerProvider initialState={100}>
          <Count />
          <CountButtons />
        </ContainerProvider>
      </ContainerProvider>
    </div>
  );
}
```
`<Count />` と `<CountButtons />` が `ContainerProvider` に囲われています。
デモアプリの見栄えのため CSS Modules でスタイリングしていますが、本質では有りませんのでここでは言及しません。

`ContainerProvider` は React context の `Provider` をベースにしているため、ネストすることも可能です。サンプルコードでもネストさせてみました。内側の `ContainerProvider` には `props.initialState` が渡されていますね。ここで渡す値が、あなたがさきほど定義したコンテナフックの引数に渡されます。

### `useContainer` でコンテナから値を取り出す

`<Count />` と `<CountButtons />` を定義しましょう。

`<Count />` は現在のカウントを表示する役割を持ちます。
`<CountButtons />` はカウントを増減させるボタンを表示する役割を持ちます。

```tsx:App.tsx

const getRandomNum = () => Math.floor(Math.random() * 255);
const getColor = () =>
  `rgb(${getRandomNum()},${getRandomNum()},${getRandomNum()})`;

const Count: React.FC = () => {
  const count = useContainer((container) => container.count);

  // コンポーネントが再レンダリングされるたびに文字色が変わります
  const style = { color: getColor() };

  return (
    <p className={styles.countText} style={style}>
      count: <span className={styles.countValue}>{count}</span>
    </p>
  );
};

const CountButtons: React.FC = () => {
  const increment = useContainer((container) => container.increment);
  const decrement = useContainer((container) => container.decrement);

  // コンポーネントが再レンダリングされるたびに文字色が変わります
  const style = { color: getColor() };

  return (
    <div className={styles.countButtons}>
      <button className={styles.countButton} onClick={increment} style={style}>
        increment
      </button>
      <button className={styles.countButton} onClick={decrement} style={style}>
        decrement
      </button>
    </div>
  );
};
```

`useContainer` の使われ方に注目してください。`(container) => container.count` のようなアロー関数が渡されています。これはセレクター関数といって、指定することでコンテナからほしい値だけを選択して取得することができます。セレクター関数で関心のある値のみに絞り込むことで、それ以外の値の変更による再レンダリングを抑制します。
セレクター関数を指定せずに `const container = useContainer()` としてコンテナ全体を取得することもできます。が、基本的にはセレクター関数を渡して値の絞り込みをすべきでしょう。

サンプルコードのコメントにもありますが、再レンダリングを視覚化するためにレンダリングのたびに文字色を変える動作を仕込んでいます。これで本当に余分な再レンダリングが発生していないのかがわかりますね。

### 動作確認

![demo-app-qiita.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299625/cb7a8fda-8f74-50ed-839f-285d1606af32.gif)


ボタンをクリックしても(`count` の値が変化しても)テキストのカラーが変わる、つまり再レンダリングされるのは `<Count />` だけで `<CountButtons />` はそのままなのがわかります。

謳い文句通り、余分な再レンダリングは抑制されています。

### コンテナをモック化できる

`ContainerProvider` には `props.initialState` 以外に `props.mock` を渡すことができます。これを渡すとコンテナフックのロジックを停止して常に同じ `props.mock` を配信し続けることになります。

`ContainerProvider` に `props.mock` を渡せることの何が嬉しいかというと、見た目の確認に専念できるということです。
例えばコンテナフックの内部で `fetch` を使って Web API から取得した値をステートに保持しているとしましょう。

```tsx
type User = { id: string, name: string };

const useLoginUserInfo = () => {
  const [user, setUser] = React.useState<User>();

  React.useEffect(() => {
    (async() => {
      const response = await fetch("/api/user").then(r => r.json());
      setUser(response)
    })();
  }, []);

  return { user };
};

export const [LoginContainerProvider, useLoginContainer] = unreduxed(useLoginUserInfo);
```

`useLoginContainer` を使用しているコンポーネントの見た目の確認をしたいだけなのに、コンテナフックのロジックが動いてしまう以上 API サーバーを起動してリクエストを送信できるようにしておかないとエラーになって表示させることができません。

こんなときに `LoginContainerProvider` に `props.mock` を渡せば、 `fetch` が実行されなくなるので見た目の確認が非常に楽になります。

```tsx
function App() {
  const mockUser: User = {
    id: "mock-id",
    name: "mock-name"
  };

  return (
    <LoginContainerProvider mock={{ user: mockUser }}>
      {...}
    </LoginContainerProvider>
  );
}
```

この場合、 `useLoginContainer(container => container.user)` で取得できるオブジェクトは常に `mockUser` オブジェクトと等しくなります。
この機能は Storybook などで重宝するはずです。

# まとめ

私が開発した `unreduxed` というステート管理ライブラリを紹介しました。

使い方はカスタムフックを定義してわたすだけ。非常に簡単に導入が可能です。
他のステート管理ライブラリとも共存できるので部分的な導入も良いでしょう。

ぜひ使っていただきフィードバック等いただければ喜びます。