概要
JavascriptでReactフックスのuseState、useMemoチックな動きを実装したので備忘録として残します。
実際の実装
useState的なクラス
ReactフックスのuseState的な感じで、値を保持しつつその値が更新された際に通知するクラスです。
クラス定義
State.js
//=======================================
// Stateクラス
//=======================================
define(function () {
// State
class State {
/**
* コンストラクタ
*/
constructor(initialValue = null, force = false) {
// 初期値のセット
this._value = initialValue;
// 値の変更をサブスクライブする関数のリスト
this._listenerCallbackList = [];
// 値が変わらなくてもサブスクした関数を発火させるか(force = trueで強制発火)
this._force = force;
}
/**
* 取得
*/
get value() {
return this._value;
}
/**
* 格納
*/
set value(newValue) {
if (!this._force && this._value === newValue) {
// セットされた値が以前の値と変わらない(かつ強制発火させない)場合はサブスクの関数を呼び出さない
return;
}
// セットされた新しい値を保持
this._value = newValue;
// サブスクしている関数群を呼び出して、新しい値を通知
this._listenerCallbackList.forEach((callback) => callback(newValue));
}
/**
* 購読
*/
subscribe(callback) {
// subscribeした時点で1回発火
callback(this._value);
// サブスク関数を登録
this._listenerCallbackList = [...this._listenerCallbackList, callback];
// サブスク関数の削除処理
const unsubscribe = () => {
this._listenerCallbackList = this._listenerCallbackList.filter((listenerCallback) => listenerCallback !== callback);
};
// サブスクを解除するための関数を返却
return unsubscribe;
}
}
return State;
});
上記を利用して、文字列が入力された際に任意のUIのテキストを更新してみます。
利用サンプル
Sample.js
define(function () {
// State.jsをrequire
const State = require("State");
class Sample {
/**
* コンストラクタ
*/
constructor(initialValue = null, force = false) {
// 文字列のStateを定義
this._textState = new State("");
// 購読する関数(this._updateViewText)をStateにセット
this._unsubscribe = this._textState.subscribe(this._updateViewText);
}
/**
* 文字列変更イベント(UIから呼び出し)
*/
onChangeText(text) {
// this._textStateに値をセットすると、this._textStateを購読しているthis._updateViewTextが発火する
this._textState.value = text;
}
/**
* UIの文字列変更処理(UIに値をセット)
*/
_updateViewText(text) {
// UIのテキストプロパティに購読した値をセットすることで、onChangeTextが発火したら任意のUIの文字列が変わる
{任意UIのテキストプロパティ} = text;
}
}
return Sample;
});
textという値を保持しつつ、その値が更新されたら任意UIのテキストを更新することができました。
this._textState.valueを呼び出すことで、値が変わった時ではなく、任意のタイミングで値を取得することも可能です。
useMemo的なクラス
こちらはuseMemo的な感じで、前述したStateを引数として受け取り、内部で値を変換・保持しつつ変換後の値が更新された際に通知するクラスです。
クラス定義
ComputedState.js
//=======================================
// ComputedStateクラス
//=======================================
define(function () {
class ComputedState {
/**
* コンストラクタ
*/
constructor(selector, deps) {
// 依存するStateの配列
this._deps = deps;
// State郡の現在の値を変換するための関数
this._selector = selector;
// 初期値のセット
this._currentValue = undefined;
// 値の変更をサブスクライブする関数のリスト
this._listenerCallbackList = [];
// 最初は必ず値変換処理を実行
this._recompute(true);
}
/**
* 取得
*/
get value() {
return this._currentValue;
}
/**
* 購読
*/
subscribe(callback) {
// subscribeした時点で一回発火
callback(this._currentValue);
// サブスク関数を登録
this._listenerCallbackList = [...this._listenerCallbackList, callback];
// サブスク関数の削除処理
const unsubscribe = () => {
this._listenerCallbackList = this._listenerCallbackList.filter((listenerCallback) => listenerCallback !== callback);
};
// サブスクを解除するための関数を返却
return unsubscribe;
}
/**
* 再計算
*/
_recompute(force) {
// 各Stateから値を取り出す
const depValues = this._deps.map((d) => d.value);
// 値変換処理を実行し、次の値を取得
const next = this._selector(...depValues);
if (!force && this._currentValue !== next) {
// 変換後の値が以前の値と変わらない(かつ強制発火させない)場合はサブスクの関数を呼び出さない
return;
}
// セットされた新しい値を保持
this._currentValue = next;
// サブスクしている関数群を呼び出して、新しい値を通知
this._listenerCallbackList.forEach((callback) => callback(next));
}
}
return ComputedState;
});
上記を利用して、文字列が入力されたかつ、チェックが付けられた際にボタンを活性状態にしてみます。
利用サンプル
Sample.js
define(function () {
// State.js、ComputedState.jsをrequire
const State = require("State");
const ComputedState = require("ComputedState");
class Sample {
/**
* コンストラクタ
*/
constructor(initialValue = null, force = false) {
// 文字列のStateを定義
this._textState = new State("");
// チェック状態のStateを定義
this._isCheckedState = new State(false);
// ボタンの非活性ComputedStateを定義
// 第一引数: textとisCheckedが変更された場合の変換処理
// 第二引数: textとisCheckedのState
this._isDisabledButtonState = new ComputedState((text, isChecked) => {
// テキストが未入力もしくはチェックが付けられていない場合は非活性とする
return text.length === 0 || !isChecked;
}, [this._textState, this._isCheckedState]);
// 購読する関数(this._updateViewButtonIsDisabled)をStateにセット
this._unsubscribe = this._isDisabledButtonState.subscribe(this._updateViewButtonIsDisabled);
}
/**
* 文字列変更イベント(UIから呼び出し)
*/
onChangeText(text) {
// this._textStateに値をセットすると、this._textStateを監視しているthis._isDisabledButtonStateの変換処理が実行される
this._textState.value = text;
}
/**
* チェック状態変更イベント(UIから呼び出し)
*/
onChangeIsChecked(isChecked) {
// this._isCheckedStateに値をセットすると、this._isCheckedStateを監視しているthis._isDisabledButtonStateの変換処理が実行される
this._isCheckedState.value = isChecked;
}
/**
* ボタンの活性状態変更処理(UIに値をセット)
*/
_updateViewButtonIsDisabled(isDisabled) {
// ボタンの非活性プロパティに購読した値をセットすることで、テキストが入力されていないもしくはチェックが付けられていない場合はボタンを非活性とする
{ボタンの非活性プロパティ} = isDisabled;
}
}
return Sample;
});
text/isCheckedという値を保持しつつ、その2つの値が更新されたら任意ボタンの活性状態を更新することができました。
まとめ
ネイティブのJSでReactフックスのuseState,useMemo的な動きを実現したい場合は、この備忘録を参考にしてみてください。