redux を代表するステート管理ライブラリは学習コストが高めですよね。ライブラリ独自の記法を覚えなくちゃいけないしライブラリ同士の組み合わせ方も考えなければならない。
React hooks の useState
や useReducer
で宣言するステートがそのまま他のコンポーネントと共有できたら楽だと思いませんか?
普段からコンポーネント単位では使い慣れているであろうフック関数でグローバルステートも管理できれば、ステート管理ライブラリについて学習する必要はないはずです。
そんなライブラリがあるのでしょうか?
あります。私が作りました。
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 なら ?
マークをつけて宣言してください。これはライブラリの型定義上避けられません。
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
です(固定長タプルなので、受け取る変数は好きな名前がつけられます)。
私は ContainerProvider
と useContainer
という名前をつけてみました。この名前から使い方のイメージが湧く人も多いでしょう。ContainerProvider
はコンテナ(ステート)を共有したいコンポーネントツリーのトップに配置します。 useContainer
は ContainerProvider
のツリー内部で使用される必要があります。これは React context の Provider
と useContext
の関係と似ていますね(実際にライブラリの内部で React context が用いられているのでこの構造になっています)。
ContainerProvider
を設置する
unreduxed
関数から受け取った ContainerProvider
を設置します。設置する場所はステートを共有したい範囲のトップレベルです。グローバルステートなら index.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 />
はカウントを増減させるボタンを表示する役割を持ちます。
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()
としてコンテナ全体を取得することもできます。が、基本的にはセレクター関数を渡して値の絞り込みをすべきでしょう。
サンプルコードのコメントにもありますが、再レンダリングを視覚化するためにレンダリングのたびに文字色を変える動作を仕込んでいます。これで本当に余分な再レンダリングが発生していないのかがわかりますね。
動作確認
ボタンをクリックしても(count
の値が変化しても)テキストのカラーが変わる、つまり再レンダリングされるのは <Count />
だけで <CountButtons />
はそのままなのがわかります。
謳い文句通り、余分な再レンダリングは抑制されています。
コンテナをモック化できる
ContainerProvider
には props.initialState
以外に props.mock
を渡すことができます。これを渡すとコンテナフックのロジックを停止して常に同じ props.mock
を配信し続けることになります。
ContainerProvider
に props.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 サーバーを起動してリクエストを送信できるようにしておかないとエラーになって表示させることができません。
こんなときに LoginContainerProvider
に props.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
というステート管理ライブラリを紹介しました。
使い方はカスタムフックを定義してわたすだけ。非常に簡単に導入が可能です。
他のステート管理ライブラリとも共存できるので部分的な導入も良いでしょう。
ぜひ使っていただきフィードバック等いただければ喜びます。