🆕

巷で話題の新しい状態管理ライブラリ "unreduxed" を試す!

2020/11/18に公開2

unreduxed

リリースされたばかりの unreduxed が話題ですね! react-reduxunstated-next に影響を受けた API になっており、非常にシンプルで扱いやすいライブラリになっているようです。

今回はそんな unreduxed の使い方を記していきます!

リポジトリ

特徴

  • シンプルな API

非常にシンプルな API が特徴になっているそうです。react hooks と react context の理解さえあればすぐにライブラリを使用することができます。

API は unstated-nextcreateContainer をベースに、react-reduxuseSelector の考えを取り込んでいます。

  • 余分な再レンダリングを抑制する

ベースになっている unstated-nextunreduxed 同様に hooks と context の理解だけでステート共有が可能なります。
しかし問題点として、context の機能をそのまま利用しているため、購読側コンポーネントの関心がないステートの変化まで検知して再レンダリングされてしまうことが挙げられます。
小さなアプリやステートの変化が少ないアプリでは unstated-next でグローバルステートを管理してもそれほど問題にはなりませんが、アプリの規模が大きくなっていくにつれてパフォーマンスが悪くなっていきます。
redux はアプリ全体でただひとつのステートを持ちますが、react-reduxuseSelector がうまく値をキャッシュして余分な再レンダリングを抑制します。ただし学習コストが高いと言われています。

unreduxedunstated-next の非常にシンプルな考えに react-reduxuseSelector を持ち込みました。

使い方

何はともあれ、使ってみましょう!

インストール

npm install unreduxed

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

unreduxed でいうコンテナフックとは複数コンポーネント間で共有したいステートを宣言するための、値を返却するカスタムフックのことのようです。

よくある数字をカウントするだけのカスタムフックで試していきます。

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);

ライブラリから default export される関数にコンテナフックを突っ込むだけですね。
コンテナフックには Provider 経由で初期値を渡すことも可能です(ただし初期値を渡すことは任意なので undefined を考慮する必要があります)。ここでもコンテナフックの引数から初期値 init を受け取るように作ります。

ContainerProvider を配置する

上で取得された ContainerProvider (変数名はなんでもいい) を共有したいコンポーネントツリーのトップに配置します。
ContainerProvider に囲われたコンポーネント (ここでは <Count /><Buttons />) は useContainer が使用可能になります。
初期値は initialState という props で渡します。 Provider のネストももちろん可能です。

export default function App() {
  return (
    <ContainerProvider>
      <Count />
      <Buttons />
      <ContainerProvider initialState={100}>
        <Count />
        <Buttons />
      </ContainerProvider>
    </ContainerProvider>
  );
}

これは unstated-next と同じ、というより React.createContext をベースにしているいろんなライブラリと同じ書き方なのでわかりやすいですね。useContainerReact.useContext のラッパーであることも想像がつきます。

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

ContainerProvider の内側のコンポーネントで useContainer を使用します。
useContainer には selector 関数が渡せて、値の絞り込みが可能になっています。これが react-reduxuseSelector を持ち込んだと述べている点になりますね。

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

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

  const style = { color: getColor() };

  return <p style={style}>{count}</p>;
};

const Buttons = () => {
  const increment = useContainer((container) => container.increment);
  const decrement = useContainer((container) => container.decrement);

  const style = { color: getColor() };

  return (
    <div>
      <button onClick={increment} style={style}>
        increment
      </button>
      <button onClick={decrement} style={style}>
        decrement
      </button>
    </div>
  );
};

余分な再レンダリングを抑制するのが特徴とのことなので、再レンダリングを視覚的に確認できるようにランダムに文字色を変える動作を仕込んでいます。これについては こちらのブログ記事 を参考にしました。

動かしてみる

ここまでのソースコードを含んだデモアプリがこちらになります。

Buttons コンポーネント内で useContainer を使っているのに、各ボタンをクリックしてステートが変化しても Buttons 自体の色は変わりません!確かに余分な再レンダリングが抑制されているようです。

モックコンテナを渡すこともできる

ContainerProvidermock というコンテナと同じ型の props を受け取れるようになっています。 mock を渡すとコンテナフックは実行されなくなり、代わりに mock が配信されることになります。

const MockProvider: React.FC = props => {
  const mock = {
    count: 500,
    increment: () => console.log("increment!"),
    decrement: () => console.log("decrement!"),
  }

  return (
    <ContainerProvider mock={mock}>
      {props.children}
    </ContainerProvider>
  )
}

これは Storybook など見た目を確認するツールで利用されることを想定されています。ロジックが停止されて固定された値が常に取得できれば見た目の確認のみに集中することができますね。

まとめ

unreduxed を非常に簡単にグローバルなステートを宣言できるようになりますね。しかも各コンポーネントで使用したい値だけを取り出せば、余分な再レンダリングは抑制してくれるためパフォーマンスの心配もなさそうです。

よければみなさんもぜひ使ってみてください!

実は

記事の冒頭に「話題に」と言われていますがきっと読者の方は初めて耳にするライブラリだという人ばかりでしょう。
それもそのはずで、これは私が先日作成したステート管理ライブラリだからです。自分のライブラリの紹介記事でした。

そこでライブラリの中身について少し書き記していきます。

余分な再レンダリングを抑制する仕組み

context に愚直にステートを突っ込んで useContext で拾い上げる unstated-next パターンは、ステートが更新されたときに useContext しているコンポーネントすべてが再レンダリングされてしまいます。
useContext で再レンダリングされない条件は context の中身が同じ参照を持ち続けていることです。ただ、 useContext で取得するオブジェクトが不変だと購読側コンポーネントは変わってほしいときにも再レンダリングされなくなってしまいます。そこで思いついたのが、購読側コンポーネントが Provider から値を受け取るのではなく、購読側コンポーネントを再レンダリングさせる関数を Provider に集約することでした。

unreduxedの内部イメージ
unreduxed の内部イメージ

上の内部イメージにおいて、notifier はクラスインスタンスとして宣言されており、それを Provider 内部で React.useRef で保持しています。
context で配信するのは notifier インスタンスなので、参照が変更されることはありません。そのため、useContext による再レンダリングは発生しません。
notifier インスタンスは useContainer によって購読側コンポーネントまで下っていき、そこで購読側コンポーネントを再レンダリングするための listener 関数を集めます。
listener 関数ひとつひとつはコンテナを受け取り selector に渡して次の値を評価します。useContaineruseRef で前回値を保持しているので、次の値と比較して変更されていれば ref を更新して新しい値を返します。
ContainerProvider 内では、コンテナの変化を検知する useEffect によってすべての listener を発火します。

React hooks 時代にクラスインスタンスかよと思われるかもしれませんが、私もそう思うので API はまったくそれを感じさせないために unstated-next を踏襲した関数型のインターフェイスにしました(作ってから別にクラスインスタンスじゃなくてもできそうって思った)。ライブラリを使用する側は何も意識することなくただカスタムフックを組み合わせることでステートの共有が可能となります。学習コストもほぼゼロでしょう。

つらつらと日本語でしゃべるよりもプログラム言語で読んだほうが理解が早いと思うので興味のある方はこちらでご確認ください。

https://github.com/y-hiraoka/unreduxed/blob/master/src/createUseContainer.ts#L15-L35

今後について

React v18 で Concurrent Mode という機能が搭載されると言われています。React コンポーネントで非同期処理を扱いやすくするための機能です。Promise を throw すると React が吸収してなんかいい感じにしてくれるアレです(よくわかっていない)。
unreduxed はまったく Concurrent Mode を考慮していません。おとなしく、、、 Recoil を、、、使いましょう、、、。

終わりに

useContext でも余計な再レンダリングしたくない!という一心でライブラリを作ってみました。
良ければ使っていただき、いいところ悪いところのフィードバックをいただけると幸いです。
特に内部実装の問題点があれば指摘していただけると助かります。

GitHubで編集を提案

Discussion

uttkuttk

@stin

非常にシンプルに扱えて、とても良いですね👍
インスタンスをPropsで渡すことで描画更新を抑えるアイディアは、
私には無かったので、とても参考になりました🙌 
ソースコードもとても見やすかったです。

これはunstated-nextでもいえる事ですが、Providerが多くなって大変になりそうな感じがします。私としては、Providerを一つにまとめる事が出来れば嬉しいですね。

最近はRecoilなどのAtomicな状態管理が話題ですが、色々と問題点は多いと思っているので、unreduxedのように、違ったアプローチを持った状態管理が必要だと個人的に思っています。

すてぃんすてぃん

ご覧頂きありがとうございます!
Provider を何重にも積み上げなければならない問題は私も同じことを感じますし他の開発者の方もよく言及しているのを見かけます。
現状の unreduxed の作りでは、コンテナフックの数だけ Provider を生成しなければなりません。コンテナフックをひとつにしてすべての共有したいステートとロジックを詰め込むことで Provider をひとつにすることも可能ですが、責務の異なるステートやロジックをひとつにまとめることは誰も好まないでしょう😇

複数のコンテナフックを登録してただひとつの Provider を生成するような作りにアップデートするかは現在構想中です。
useContainer のインターフェイスをどうするかを、他のライブラリも調査して丁寧に考えないといけないと思っています。

ソースコードまで見ていただいて本当に嬉しいです。ありがとうございます🙇‍♂️