0. 概要
毎回Reactでアプリを作ろうとするたびに、redux
に精神をやられています。確かに堅牢だし、チーム開発に向いているかもしれないけど、記述量と学習コストが大きすぎる・・・。
いろいろ調べているうちにunstated-nextというライブラリを発見したので、試してみました。
型付きじゃないと夜も眠れないタイプの人間なので、TypeScriptで実装しています。
1. 想定している読者
-
create-react-app
でアプリケーションを作ったことがある人 -
React Hooks
に興味のある人 -
TypeScript
がなんとなく分かる人 -
redux
に疲れている人
React Hooks
については公式サイトの記事が充実しているのでそちらを参照してください。
2. 準備
2.1 プロジェクトの作成
カウンター値をボタンで操作したり表示したりする簡単なアプリケーションを作ってみます。
まずはnpx create-react-app
でサクッと React プロジェクトを作成。後ろに--typescript
を付けると、TypeScript のプロジェクトになります。
# プロジェクトの作成
create-react-app sample --typescript
# プロジェクトに移動
cd sample
2.2 パッケージのインストール
今回使うのはunstated-next
だけです。
npm install unstated-next -S
3. 実装
3.1 全体像
コードの説明の前に、全体像を載せてみます。矢印がデータやイベントの流れを示しています。
unstated-next
で登場するのは、2つだけです。
- ステート(カウンター値)の管理・操作方法を提供する
コンテナ
- ステートの値を画面に表示させたり、イベントを受け渡す
コンポーネント
ただし、コンポーネントは元々Reactにあるコンポーネントのことなので、実質覚えるのはコンテナの部分だけ。簡単!
それでは以上を踏まえて、全体コードをご覧ください。
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);
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公式サイトの説明をご参照ください。
やってることはこれだけです。
-
useState
によりHooks内にステートの値とそれを操作する関数(setCount
)を取得する -
setCount
をラップしたadd関数
とreset関数
を定義する。 - 戻り値としてコンテナ外で使いたい値や関数を返す。
/**
* カウンター操作用コンテナの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
でコンテナを作成します。
/** カウンター操作用コンテナ */
export const CounterContainer = createContainer(useCountContainer);
3.3 コンポーネント側での利用
コンテナを利用するには2つの手順が必要です。
まず1つは、Provider
によって、下位コンポーネントでコンテナを扱えるようにすることです。
サンプルでは、<CounterContainer.Provider>
の部分がそれです。これによってCouterOperate
やCouterDisplay
がコンテナに触れるようになっています。
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
関数に触れるようになります。
/**
* カウンター操作用コンポーネント
*/
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に比べて発展途上のライブラリではありますが、記述量の少なさに感動することひとしおです。お試しあれ。
(不勉強なところが多いので、ご指摘よろしくお願いします)