Unstated Nextで状態管理

React Hooks以降、複数コンポーネント間の状態管理にはContextとuseReducerを組み合わせるのが一般的になっているようです。以前、このブログ内でも取り上げました。

上記はプレーンなReactだけで実現できます。手軽な反面、コードが直感的にならない印象を受けます。Contextだけならともかく、useReducerを使うとReduxライクな記述にするか、または独自の書き方をするか、組む人に左右される部分もあります。

今回取り上げるUnstated Nextは、Reactコンポーネント間で状態を管理するライブラリです。これを使えばdispatchなどの概念は頭からポイできます🗑

yarn add unstated-next

以降、A・Bというコンポーネント間でフォームの状態値を取り扱う例を考えます。

createContainer

まず必要なのは、状態を保管するContainerを作成することです。これはReduxにおけるstoreと似ていて、状態値や更新関数が保管されます。store.jsというファイルを作りましょう。

import { useState } from 'react';
import { createContainer } from 'unstated-next';

export const useStore = () => {
  // 入力中の状態
  const [input, setInput] = useState('');
  // テキスト値
  const [text, setText] = useState('sample');

  // 入力中の状態値を変更するハンドラ関数
  const handleInput = event => {
    setInput(event.target.value);
  };

  // 確定時のハンドラ関数
  const handleSubmit = event => {
    event.preventDefault();
    if (input.length === 0) return;
    setText(input);
    setInput('');
  };

  return {
    input,
    text,
    handleInput,
    handleSubmit,
  };
};

export const StoreContainer = createContainer(useStore);

最後一行以外は単なるCustom Hookです。そしてcreateContainerはReact ContextにおけるcreateContextです。引数にCustom Hookを指定しており、これでContainerが作成できます。中には状態値とハンドラが格納されていて、外部で使えるのはreturn内の値です。

内部は単なるCustom Hookのため、直感的なコードになります。

useContainer

コンポーネントで値を読み出してみましょう。A.jsというファイルを作成します。

import React from 'react';
import { StoreContainer } from '../store';

const A = () => {
  const form = StoreContainer.useContainer();
  return (
    <>
      <p>{form.text}</p>
      <input
        type="text"
        value={form.input}
        onChange={form.handleInput}
      />
      <button type="button" onClick={form.handleSubmit}>
        確定
      </button>
    </>
  );
};

export default A;

先程のStoreContainerをインポートし、useContainerを呼び出しています。

const form = StoreContainer.useContainer();

formという変数でContainerでreturnした値にアクセスできます。例えばform.inputとすれば、入力状態値を扱う変数を取り扱えます。

Provider

最後にContainer名.Providerで利用するコンポーネントをラップします。React ContextのProviderと似ていますが、valueを渡す必要はありません。

App.jsに以下を記述します。

import React from 'react';
import A from './components/A';
import B from './components/B';
import { StoreContainer } from './store';

const App = () => (
  <>
    <StoreContainer.Provider>
      <A />
      <B />
    </StoreContainer.Provider>
  </>
);

export default App;

Bコンポーネントは、Aコンポーネントと名前以外はまったく同じコンポーネントです。これを実行してみると、AとBで同じ状態を利用できているのが確認できます。

例の動作

ネストしたコンポーネントや他の状態やハンドラを呼び出すことも可能です。

少し凝った例 – ECサイト

React Routerで読み込むコンポーネント内でも利用できます。以下、簡易なECサイトのカートを再現してみました。※バックエンドなどは省いています。

コードはSadboxに置いてあります。

この例では利用していませんが、React.lazyで動的に分割コンポーネントを読み込み、その中でContainerの値を取得もできます。

おわりに

Unstated Nextを使うと、複数コンポーネントの状態管理が簡単になります。200バイトというミニマムサイズなのも👍気になれば、是非。