Unstated Nextは、複数コンポーネントにより組み立てられたツリーの中で、状態を共有して管理するライブラリです。簡単なサンプルをつくりながら、使い方についてご紹介します。
[注記: 2020/11/03]「コードを改善する」の項を追加。
Unstated Nextの特徴
Reactにフックが採り入れられて、useContext
を使えばReduxに頼らなくても、扱う状態の規模がさほど大きくなければ手軽に管理できるようになりました。Unstated Nextは、それをさらにシンプルにしてくれるライブラリです。
Reactのカスタムフックとコンテクストがわかっていれば、すぐに使いはじめられます。APIが最小限にまとめられ、ライブラリのサイズはわずか200バイトです。
Reactのコンテクストに当たる状態のまとめ役を、Unstated Nextではコンテナと呼びます。ひとつの状態をまとめて管理するReduxと比べると、コンテナは小分けできることもパフォーマンスの点からは有利です。ただしよく考えて設計しないと、結局コンテナが何重にも入れ子になってしまうことには、注意しなければなりません。
はじめの一歩
まず、Reactアプリケーションのひな形は、Create React Appでつくりましょう。コマンドラインツールでnpx create-react-app
につづけて、アプリケーション名(今回はreact-unstated-next-example
)を打ち込んでください。
npx create-react-app react-unstated-next-example
アプリケーション名でつくられたディレクトリに切り替えて(cd react-unstated-next-example
)、コマンドyarn start
でひな形アプリケーションのページがローカルホスト(http://localhost:3000/
)で開くはずです。
つぎに、このディレクトリにインストールするのはUnstated Next(unstated-next
)です。yarn add
コマンドでライブラリが加えられます。
yarn add unstated-next
yarnでなく、npm install
コマンドでインストールしても構いません。
npm install --save unstated-next
カスタムフックとuseContext
を使ってつくるカウンター
カスタムフックとコンテクストがわかれば、Unstated Nextはすぐに使えます。ということで、まずは素のReactのカスタムフックとuseContext
だけで、ライブラリは使わずにカウンターのアプリケーションをつくってみましょう。
カスタムフックをつくる
「フックとは、関数コンポーネントにstateやライフサイクルといった Reactの機能を"接続する(hook into)"ための関数です」(「要するにフックとは?」)。さらに、フックを独自につくって、コンポーネントからロジックを切り出すこともできます。そうすれば、コンポーネントのコードがすっきり見やすくなるとともに、そのカスタムフックを使い回すこともできるのです。
自分独自のフックを作成することで、コンポーネントからロジックを抽出して再利用可能な関数を作ることが可能です。
(「独自フックの作成」より)
カスタムフックのモジュールsrc/useCounter.js
の定めはコード001のとおりです。カウンターのロジックですから、値の状態変数(count
)にその設定関数(setCount()
)、減算(decrement()
)と加算(increment()
)の関数を加えました。
カスタムフックは、関数コンポーネントと異なり、JSXで要素を返す必要はありません。戻り値のオブジェクトに収めたのは、状態変数(count
)と減算(decrement()
)および加算(increment()
)の関数です。なお、カスタムフックの基本的な役割や考え方については「React: コンポーネントのロジックをカスタムフックに切り出す ー カウンターの作例で」をお読みください。
コード001■カウンターのカスタムフック
import { useState } from "react";
export const useCounter = (initialState = 0) => {
const [count, setCount] = useState(initialState);
const decrement = () => setCount(count - 1);
const increment = () => setCount(count + 1);
return { count, decrement, increment };
};
カウンターを表示・操作するコンポーネントの作成
カウンターを表示・操作するコンポーネントが以下のコード002です。フックuseContext
は、このあとアプリケーション(App
)でつくられるコンテクスト(CounterContext
)から、カスタムフック(useCounter
)が返すオブジェクト(counter
)を取り出します。その中の状態変数(counter.count
)や減算(counter.decrement
)・加算(counter.increment
)の関数を、それぞれの要素に割り当てればよいのです。
コード002■カウンター表示のコンポーネント
import React, { useContext } from "react";
import { CounterContext } from './App';
const CounterDisplay = () => {
const counter = useContext(CounterContext);
return (
<div>
<button onClick={counter.decrement}>-</button>
<span>{counter.count}</span>
<button onClick={counter.increment}>+</button>
</div>
);
}
export default CounterDisplay;
コンテクストのProvider
で子コンポーネントを包む
アプリケーションのモジュール(src/App.js
)でコンテクスト(CounterContext
)をつくります(コード003)。そのために呼び出す関数がcreateContext()
です。コンテクストはContext.Provider
コンポーネントを備えています。このコンポーネントに含めた子はすべて、コンテクストが参照できるという仕組みです。
コンテクストに与える変数や関数の参照は、Context.Provider
コンポーネントのvalue
プロパティに与えてください。今回はカスタムフックuseCounter
から得たオブジェクト(counter
)が渡されました。
コード003■コンテクストのProvider
のvalue
に参照するオブジェクトを与える
import React, { createContext } from 'react';
import { useCounter } from './useCounter';
import CounterDisplay from './CounterDisplay';
import './App.css';
export const CounterContext = createContext();
function App() {
const counter = useCounter();
return (
<CounterContext.Provider value={ counter }>
<div className="App">
<CounterDisplay />
</div>
</CounterContext.Provider>
);
}
export default App;
これで、コンテクストを使ったカウンターができあがりました(図001)。
図001■コンテクストを使ったカウンター
Unstated Nextでカウンターをつくり替える
カスタムフックとコンテクストがわかりましたので、カウンターをUnstated Nextで動くようにつくり直して見ましょう。コンテクストに替えて、コンテナをつくります。
カスタムフックからコンテナをつくる
モジュールsrc/useCounter.js
のカスタムフックは基本的に変わりません。フックを関数createContainer()
でコンテナに包むのです。コンテナ(CounterContainer
)は、いわばカスタムフック(useCounter
)のロジックを備えたコンテクストといえます。
import { createContainer } from "unstated-next";
// export const useCounter = (initialState = 0) => {
const useCounter = (initialState = 0) => {
};
export const CounterContainer = createContainer(useCounter);
コンポーネントをコンテナのProvider
で包む
アプリケーションモジュールsrc/App.js
は、コンテクストをUnstated Nextのコンテナに差し替えます。コンテナにもコンテクストと同じように<Container.Provider>
が備わっているのです。Provider
もコンテクストからコンテナに書き替えてください。ただし、value
プロパティは要りません。
// import React, { createContext } from 'react';
import React from 'react';
// import { useCounter } from './useCounter';
import { CounterContainer } from './useCounter';
// export const CounterContext = createContext();
function App() {
// const counter = useCounter();
return (
// <CounterContext.Provider value={ counter }>
<CounterContainer.Provider>
{/* </CounterContext.Provider> */}
</CounterContainer.Provider>
);
}
コンテナのロジックをコンポーネントが使う
コンテナはカスタムフックのロジックを備えているのでした。コンテナ(CounterContainer
)に対してuseContainer()
を呼び出すと、ロジックの参照が得られるのです。参照はコンテクストを使ったときと同じ変数(counter
)に収めれば、ほかに書き直すところはありません。
// import React, {useContext} from "react";
import React from "react";
// import { CounterContext } from './App';
import { CounterContainer } from './useCounter';
function CounterDisplay() {
// const counter = useContext(CounterContext);
const counter = CounterContainer.useContainer();
}
これでカウンターはUnstated Nextのコードに書き替えられました。モジュール3つの記述を以下のコード004にまとめます。カスタムフックとコンテクストでも組み立ては簡単でした。でも、ふたつをまとめたコンテナを使うことでさらにシンプルになったでしょう。CodeSandboxに作例をサンプル001として掲げました。
コード004■Unstated Nextを使ったカウンター
import { useState } from "react";
import { createContainer } from "unstated-next";
const useCounter = (initialState = 0) => {
const [count, setCount] = useState(initialState);
const decrement = () => setCount(count - 1);
const increment = () => setCount(count + 1);
return { count, decrement, increment };
};
export const CounterContainer = createContainer(useCounter);
import React from 'react';
import { CounterContainer } from './useCounter';
import CounterDisplay from './CounterDisplay';
import './App.css';
function App() {
return (
<CounterContainer.Provider>
<div className="App">
<CounterDisplay />
</div>
</CounterContainer.Provider>
);
}
export default App;
import React from "react";
import { CounterContainer } from './useCounter';
const CounterDisplay = () => {
const counter = CounterContainer.useContainer();
return (
<div>
<button onClick={counter.decrement}>-</button>
<span>{counter.count}</span>
<button onClick={counter.increment}>+</button>
</div>
);
}
export default CounterDisplay;
サンプル001■Unstated Nextでカウンターをつくる
コードを改善する
Unstated Nextを使った状態管理の仕組みがわかるよう、コードはできるだけ簡略化しました。けれど、実際にはもう少し改善した方がよいでしょう。
まず、状態を変えようとするとき、できることなら状態変数は参照しないことが望ましいといえます。少し込み入った処理では、最新の値が得られない場合もあるからです。直近の状態変数値にもとづいてその値を変えたいとき、状態設定関数(setCount
)には関数を渡して「関数型の更新」ができます。更新関数の受け取る引数(prevCount
)が直近の変数値です。
const useCounter = (initialState = 0) => {
const [count, setCount] = useState(initialState);
// const decrement = () => setCount(count - 1);
const decrement = () => setCount((prevCount) => prevCount - 1);
// const increment = () => setCount(count + 1);
const increment = () => setCount((prevCount) => prevCount + 1);
return { count, decrement, increment };
};
つぎに、カウンターのボタンのコールバックに定められた関数は、コンポーネントがレンダーされるたびにつくり直されます。useCallback
フックでラップすれば、関数に用いられる値が同じなら前の定義がそのまま用いられます(「メモ化」と呼ばれます)。ただし、useCallbackフックの第2引数に、配列で依存する値を与えなければなりません。それらの値が同じかどうかで、関数のつくり直しを決めるのです。
なお、依存配列を空で渡すと、依存なしとなり、初期化時のみ実行されます。状態設定関数の引数を関数型の更新で定めたときは、その引数値は依存配列に加える必要がありません。改善したsrc/useCounter.js
の記述は、つぎのコード005のとおりです。CodeSandboxにサンプル002を公開しました。
コード005■コンテナのコードを改善
import { useCallback, useState } from "react";
import { createContainer } from "unstated-next";
const useCounter = (initialState = 0) => {
const [count, setCount] = useState(initialState);
const decrement = useCallback(() => setCount((prevCount) => prevCount - 1), []);
const increment = useCallback(() => setCount((prevCount) => prevCount + 1), []);
return { count, decrement, increment };
};
export const CounterContainer = createContainer(useCounter);