59
48

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 5 years have passed since last update.

reduxに疲れたのでunstated-next を試してみた

Posted at

0. 概要

毎回Reactでアプリを作ろうとするたびに、reduxに精神をやられています。確かに堅牢だし、チーム開発に向いているかもしれないけど、記述量と学習コストが大きすぎる・・・。

いろいろ調べているうちにunstated-nextというライブラリを発見したので、試してみました。
型付きじゃないと夜も眠れないタイプの人間なので、TypeScriptで実装しています。

1. 想定している読者

  • create-react-appでアプリケーションを作ったことがある人
  • React Hooksに興味のある人
  • TypeScriptがなんとなく分かる人
  • reduxに疲れている人

React Hooksについては公式サイトの記事が充実しているのでそちらを参照してください。

2. 準備

2.1 プロジェクトの作成

カウンター値をボタンで操作したり表示したりする簡単なアプリケーションを作ってみます。

↓ 動作イメージ
実装後.gif

まずは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 全体像

コードの説明の前に、全体像を載せてみます。矢印がデータやイベントの流れを示しています。

全体像.png

unstated-nextで登場するのは、2つだけです。

  • ステート(カウンター値)の管理・操作方法を提供するコンテナ
  • ステートの値を画面に表示させたり、イベントを受け渡すコンポーネント

ただし、コンポーネントは元々Reactにあるコンポーネントのことなので、実質覚えるのはコンテナの部分だけ。簡単!

それでは以上を踏まえて、全体コードをご覧ください。

CounterContainer.ts (コンテナ)
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);
CounterComponent.tsx (コンポーネント)
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公式サイトの説明をご参照ください。

やってることはこれだけです。

  1. useStateによりHooks内にステートの値とそれを操作する関数(setCount)を取得する
  2. setCountをラップしたadd関数reset関数を定義する。
  3. 戻り値としてコンテナ外で使いたい値や関数を返す。
CounterContainer.ts
/**
 * カウンター操作用コンテナの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でコンテナを作成します。

CounterContainer.ts(続き)
/** カウンター操作用コンテナ */
export const CounterContainer = createContainer(useCountContainer);

3.3 コンポーネント側での利用

コンテナを利用するには2つの手順が必要です。
まず1つは、Providerによって、下位コンポーネントでコンテナを扱えるようにすることです。
サンプルでは、<CounterContainer.Provider>の部分がそれです。これによってCouterOperate CouterDisplayがコンテナに触れるようになっています。

CounterContainer.tsx
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関数に触れるようになります。

CounterContainer.tsx
/**
 * カウンター操作用コンポーネント
 */
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に比べて発展途上のライブラリではありますが、記述量の少なさに感動することひとしおです。お試しあれ。

(不勉強なところが多いので、ご指摘よろしくお願いします)

59
48
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
59
48

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?