一覧に戻る

ReactNとHooksによるグローバルな状態管理に関する調査と実験

#TypeScript#React#react-hooks

https://github.com/dai-shi/react-hooks-global-state これ使っとけばよさそう()

この記事は実験なので内容の確からしさや有用性はなんともいえません

はじめに

Vueでグローバルステートを管理するライブラリと言えばVuexですが、ReactではReduxというのがあるらしいです。でもReact hooks登場以前の情報が多かったです。そしてパッと見、hooksがあれば、よりシンプルに状態を管理出来るのではないかと思い、色々調べて実験してみました。

ちなみに私はVueしか使ってこなかったReactの初学者です。初学者が王道を避けていきなり考察しているのは、アホっぽいですが、Hooks登場以後の王道パターンは調べてもわからなかった。まだ確立していないなら、どこかの誰かの役には立つかもしれない。

調べたこと

私が調べた時系列順です

Reduxについて

https://qiita.com/daijinload/items/c7015b1117c5beb7e49f

最初にこれを読んだためReduxやる気がなくなってしまったのですが、納得感のある記事(DBデータの二重管理はすべきでないとか)。ここでクラス+シングルトンというパターンに触れられていました。

シングルトンについて

https://qiita.com/NeGI1009/items/f8b17d856a4b15b1ecbc

JSでシングルトンとか考えた事なかったですが、上記の例に添えばとても簡単に実装出来ます。どこでimportしても同一のインスタンスが得られるので、まさにグローバルステートです。これを関心単位でクラス分けすれば、ライブラリを用いずとも見通しのよいステート管理が出来るのでは。

hooksについて

https://ja.reactjs.org/docs/hooks-reference.html#usecontext

useContextを用いれば、ツリーの子コンポーネントのスコープでグローバルな状態管理が出来るそう、ですがコードの見た目があんまり好きじゃなかったです。と思いきや、このAPIを用いたカスタムフックを提供するライブラリ「ReactN」があるらしい。

ReactNについて

https://qiita.com/tuttieee/items/e5b2725b3e58cae9ddd6 https://github.com/CharlesStover/reactn#function-components-1

ドキュメントのとおり、Function Componentに対応しuseGlobalというカスタムフックを提供しています。その前にreact-hooks-global-stateというライブラリもチェックしました、ReactNと似たような事が出来ますがこちらは"フラットな"Objectのみを管理可能でした(1階層のObjectという意味)。ReactNはクラスインスタンスを突っ込んでも問題ありません。

実験してみた

上記の調査から、

  • ReactNによるグローバルステート管理
  • データストアクラスのシングルトン

を試してみる

データストアクラス

export class DataStore {
    private val1: number;
    private val2: number;
    constructor() {
        this.val1 = 100;
        this.val2 = 1000;
    }
    add1(x: number) {
        this.val1 += x;
        return this;
    }
    add2(x: number) {
        this.val2 += x;
        return this;
    }
    get1() {
        return this.val1;
    }
    get2() {
        return this.val2;
    }
}
// インスタンスをexport
export const dataStore = new DataStore();

メンバ変数をprivateにする事でgetter,setterの使用を強制する

ReactNの環境構築

インストール


npm install reactn

手動で型定義

管理すべき状態(=State)の型を定義しておく

// /src/global.d.ts

import 'reactn';
import { DataStore } from './store';

declare module 'reactn/default' {
    export interface State {
        state1: DataStore;
        state2: number;
    }
}

関数コンポーネント側でimport

ReactNはReact自体をラップしますので、以下のようにimportします。

import React, { useEffect, useRef, useState } from 'react';

なら、以下に書き換える

import React, { useEffect, useRef, useState, useGlobal } from 'reactn';

ReactのAPIはそのまま使えて、useGlobalやsetGlobalといった関数が追加されています。

グローバル変数を定義・初期値をセット

setGlobal()を実行することでグローバル変数を定義出来ます。ドキュメントによれば、ReactDom.render()前にすべきとの事ですので、プロジェクトのエントリーポイントで一度だけsetGlobal()するのがよいでしょう。 https://github.com/CharlesStover/reactn#initializing-your-state

import { dataStore } from './store';
const state = {
    state1: dataStore,
    state2: 999,
};
setGlobal(state);

ReactDOM.render(
    <React.StrictMode>
        <App />
    </React.StrictMode>,
    document.getElementById('root'),
);

グローバル変数を取得・更新

useGlobal()を用います。デフォルトのhooksであるuseState()と全く同じ使い方です。

// setGlobal()の引数のオブジェクトのkeyで取得する変数を指定出来る
// TypeScriptによるコード補完が効く
const [state1, setState1] = useGlobal('state1');
const [state2, setState2] = useGlobal('state2');

// 
return (
    <div
        className="App"
        onClick={() => {
            // DataStore
            //state1.val1 += 5000; //これはできない
            setState1(state1.add1(10));

            // number
            setState2(state2 + 1); // 999 -> 1000
        }}
    >
        <span>
            {state1.get1()} {state2}
        </span>
    </div>
);

シングルトンをsetGlobal()するなら、最低限getter,setterで値を操作するようにしないと、状態管理がエライ事になると思います。

グローバル変数の更新に応じDOMを再レンダリング

ReactNによるグローバル変数周りのDOM再レンダリングは以下の条件で発生します。

  • useGlobal()の2つ目の戻り値(例:setHogehoge())が実行された場合に再レンダリングが走る
  • その際に再レンダリングされるのは、useGlobal()を実行しているコンポーネントのみ

終わりに

  • 少なくとも、flatに値を保持するなら、ReactNで良い気がする
  • ReactNはflatなオブジェクトだけでなく、クラスやネストしたオブジェクトをグローバルで扱いつつDOMに反映させられるので便利だと思うけど、扱いが難しそう(useGlobalで取得したオブジェクトはconstだけど、メンバ変数は保護されないし…)

以下が解決すればうれしいなぁと妄想

  • setGlobal()した値が、下位のコンポーネントでしか参照出来ないようにする(今は親でも取得出来てしまうのでスコープを与えるということ)
  • DOMの再レンダリングのタイミング(今は(上記の例で言えば)setState()しないとレンダリングされないが、クラス変数に変更があったら、値を参照している箇所で再レンダリングが走って欲しい、もしかしたらuseEffectで出来たりして)