11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

React #2Advent Calendar 2020

Day 1

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

Last updated at Posted at 2020-12-01

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

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

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

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

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 なら ? マークをつけて宣言してください。これはライブラリの型定義上避けられません。

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 はただのカスタムフックです。つまり、 フックのルールさえ守られていれば、何をやってもいいのです。いくつも useState を宣言してもいいでしょう。 useEffect を使用してもけっこうです。サードパーティライブラリが提供するカスタムフックを使用してももちろんかまいません。

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

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

ContainerProvider を設置する

unreduxed 関数から受け取った ContainerProvider を設置します。設置する場所はステートを共有したい範囲のトップレベルです。グローバルステートなら index.tsxApp.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 /> はカウントを増減させるボタンを表示する役割を持ちます。

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

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

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

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

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

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

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 サーバーを起動してリクエストを送信できるようにしておかないとエラーになって表示させることができません。

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

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

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

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

まとめ

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

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

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

11
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?