React-ReduxだけどReducerもActionも書かず、Dispatchすら使わず、データも何となく受け取れるようにする方法
前回
Reduxがあまりにも面倒なので、何もかも隠蔽しつつ、その力を引き出すことにした
とにかく面倒くさいFlux
React上での状態管理は、ことごとくがFluxの考え方を用いて作られています。
Actionのコマンドを定義してDispatchで送信し、Reducerで処理してStoreに書き込み、ViewがStoreの更新を察知して処理を行うという流れです。これらの処理の厄介なところは、全部が全部、記述箇所がバラバラになってしまうということです。
とにかく何もかも面倒くさいので、この地獄から抜け出す方法を探しました。結果として、上記に書いたようなことを一切書かず、簡単にStoreデータを操作する方法にたどり着きました。
## 環境設定と必要なパッケージ
今回のサンプルプログラムを動かすにはReact+TypeScript環境を用意してください。
また、以下のパッケージをインストールする必要があります。
npm -D i @jswf/redux-module
サンプルプログラム
HooksのFunctionコンポーネントと、Classコンポーネントでの利用法を同時に載せているので、プログラムが少々長くなっています。
中でやっているのはinputの内容をコンポーネント間で共有するというものです。
もしこれをやるためだけにReduxの書式を真面目に書いたら、確実に無駄地獄へ落ちることが出来るでしょう。
コンポーネント間のデータ共有にはReduxModuleというクラスを継承して利用します。
クラスを作ると、クラスごとに一つStore領域が割り当てられます。
そのため同じクラスを使用する限り、同じデータを参照することが出来ます。
また、読み書きの受付も全てこのReduxModule継承クラスが担当するので、処理をあちこちに書く必要はありません
https://github.com/JavaScript-WindowFramework/redux-module-sample
import React, { Component } from "react";
import * as ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import {
ModuleReducer,
useModule,
ReduxModule,
mapModule,
mapConnect
} from "@jswf/redux-module";
/**
*データ構造の定義(TypeScript使用時)
*
* @export
* @interface TestState
*/
export interface TestState {
msg: string;
}
/**
*Storeアクセス用クラス
*(クラスごとに自動的にStoreに領域を確保する)
* @export
* @class TestModule
* @extends {ReduxModule<TestState>}
*/
export class TestModule extends ReduxModule<TestState> {
//ここに初期値を設定可能
protected static defaultState: TestState = {
msg: "初期値"
};
//以下のようなアクセス用のメソッドは、必ずしも作る必要は無い
//getStateとsetStateはpublicなので、外から直接書き換えてしまってもOK
public getMessage() {
return this.getState("msg")!;
}
public setMessage(msg: string) {
this.setState({ msg });
}
}
/**
*Hooks用サンプル
*
* @returns
*/
function HooksApp() {
//モジュールのインスタンスを受け取る
//useModuleの使用可能場所の制限は他のhookと同じ
const testModule = useModule(TestModule);
//以下のようにPrefixを付けると、同じクラスが違う領域を持つことも出来る
//const testModule = useModule(TestModule,"Prefix");
return (
<>
<div>FunctionComponent</div>
<input
value={testModule.getMessage()}
onChange={e => testModule.setMessage(e.target.value)}
/>
<hr />
</>
);
}
/**
*Class用サンプル
*
* @class _ClassApp
* @extends {Component}
*/
class _ClassApp extends Component {
render() {
//モジュールのインスタンスを受け取る
//Hooksと名前と引数が微妙に違うので注意
const testModule = mapModule(this.props, TestModule);
return (
<>
<div>ClassComponent</div>
<input
value={testModule.getMessage()}
onChange={e => testModule.setMessage(e.target.value)}
/>
<hr />
</>
);
}
}
//クラスコンポーネントを利用する場合は以下の方法でマッピングする
//ここで宣言したモジュール以外はクラスで使用できない
//モジュールは配列で複数指定も可能
const ClassApp = mapConnect(_ClassApp, TestModule);
//Reduxに専用のReducerを関連付ける
//他のReducerと併用することも可能
const store = createStore(ModuleReducer);
ReactDOM.render(
<Provider store={store}>
<HooksApp />
<ClassApp />
</Provider>,
document.getElementById("root") as HTMLElement
);
## 必要なことのまとめ
1 ModuleReducerをReduxのStoreに関連付ける
2 ReduxModuleを継承したデータクラスを作成する
3 Classコンポーネントを使う場合はmapConnectで使用するデータクラスを関連付ける
4 useModule/mapModuleでデータクラスを呼び出す
5 setState/getStateでデータの読み書きを行う
一応解説しておく付加機能
蛇足になるので最小限にとどめますが、データクラスは外部参照機能も付いています。
別のデータクラスの機能が必要な場合はincludesに利用するモジュールを指定しておけば、getModuleで対象のクラスを呼び出すことが出来ます。
export class OtherModule extends ReduxModule {
static includes = [TestModule]
public getMessage() {
return this.getModule(TestModule).getState("msg")!;
}
public setMessage(msg: string) {
this.getModule(TestModule).setState({ msg });
}
}
まとめ
とにかく状態管理が楽になりました。これを使うことによって、コンポーネント間の手続きの大部分が省略可能となります。コンポーネント間のデータ共有にReduxを使いたいけれど、Flux的な書き方が嫌だと考えているのならぜひ使ってみてください。