Stateful React via event
state 管理をすると き、React
ではRedux
やjotai
などのパッケージを利用しますが、
今回は event listener
のように state 管理できるような構造をつくってみました。
KeyController test - CodeSandbox からあそべます。
今回は、 KeyEvent によって、文字列をかえる App をつくりました。
KeyInput
でつくった function を、他の自由な場所から実行できるようになります。
ここで利用している Key
Provider と useKey
の API を作成していきます。
function KeyInput() {
const [key, setKey] = React.useState("");
useKey(" ", (___, _ = "_") => setKey((p) => p + _) || 1);
return <div>{key || "PRESS_KEYBOARD"}</div>;
}
const A = () => void useKey("a", (key) => key(" ", "A") || 1);
const B = () => void useKey("b", (key) => key(" ", "B") || 1);
const C = () => void useKey("c", (key) => key(" ", "C") || 1);
{/* D to Z */}
root.render(
<Key>
<KeyInput />
<A />
<B />
<C />
{/* D to Z */}
</Key>
)
Getting Started
makeEvent
について
今回は coldi
という方のリポジトリ の createPubSub.ts
を参考にして、
前回紹介した makeQueue
を拡張した makeEvent
を作成しました。
function makeEvent() {
const map = new Map();
return {
add(fun, key) {
const queue = map.get(key) || map.set(key, makeQueue()).get(key);
queue.add(fun);
},
delete: (fun, key) => map.get(key)?.delete?.(fun),
flush: (key, ...args) => map.get(key)?.flush?.(...args)
}
}
helpers について
また、前回 から、objectでも使えるようになった、each
を使っています。
Array
, Set
や Map
ではなるべく forEach
を使うようにします。
TypeScriptでは typeof
が使いにくいので、 is
を利用しています。
function each(obj, fun, ctx) {
if (is.arr(obj) || is.set(obj) || is.map(obj)) obj.forEach(fun, ctx);
else for (const key in obj)
if (obj.hasOwnProperty(key)) fun.call(ctx, obj[key], key);
}
const is = {
arr: Array.isArray,
str: (a) => typeof a === "string",
obj: (a) => a?.constructor?.name === "Object",
set: (a) => a?.constructor?.name === "Set",
map: (a) => a?.constructor?.name === "Map"
};
Controller
の作成
前回と同じように、Controller
を定義します。
この class を各拡張することで、様々な state を管理することができます。
const _obj = (key, fun) => (is.str(key) ? { [key]: fun } : key);
const _arr = (arg) => (is.arr(arg) ? arg : [arg]);
class Controller {
constructor() {
const event = makeEvent();
const flush = (...args) => (p, k) => event.flush(k, ...args, ..._arr(p));
const state = (key, ...args) => each(_obj(key, []), flush(state, ...args));
state.add = (events) => each(events, event.add);
state.delete = (events) => each(events, event.delete);
state.memo = {};
this.state = state;
}
{/*~~ ➊ ~~*/}
}
つぎのような class method を Controller
の ➊ に追加し、
hookから使うと、class based で React をつかうことができます。
この方法については、class based React.js hooks | TSEI.JP をご覧ください。
effect() {
const { state } = this;
if (state.isInitialized) return false;
return (state.isInitialized = true);
}
clean() {
const { state } = this;
if (state.isInitialized) return false;
return !(state.isInitialized = false);
}
apply(props) {
this.props = props;
return this.state;
}
function useCtrl(Controller, props) {
const [ctrl] = useState(() => new Controller());
useEffect(() => void ctrl.effect());
useEffect(() => () => ctrl.clean(), [ctrl]);
return ctrl.apply(props);
}
useEvent について
作成した Controller.state
へ、自由に function を追加できるようにします。
ひとつだけ function を 追加できるように、 string から object にする _obj
を使いました。
function useEvent(state, ...args) {
const ref = useMutable(_obj(...args));
useEffect(() => void state.add(ref), [state, ref]);
useEffect(() => () => state.delete(ref), [state, ref]);
return state;
}
useMutable
について
前回 と同じく、function が重複して追加されないように、固有な function にします。
function useMutable(target) {
const ref = useRef();
ref.current = target;
return useMemo(() => {
const memo = {};
each(ref.current, (fun, key) => {
memo[key] = (...args) => fun(...args);
});
return memo;
}, []);
}
create your app
今回は、Keyboard を使うためのAPIをつくります。
class KeyController extends Controller {
keydown = (e) => this.state(e.key);
effect() {
if (super.effect()) window.addEventListener("keydown", this.keydown);
}
clean() {
if (super.clean()) window.removeEventListener("keydown", this.keydown);
}
}
必要な API をつくると、はじめの App になります。
import * as React from "react";
import { createRoot } from "react-dom/client";
import { Controller, useCtrl, useEvent } from "./hooks";
const KeyContext = React.createContext({});
const KeyProvider = KeyContext.Provider;
const useKeyContext = () => React.useContext(KeyContext);
const useKey = (...args) => useEvent(useKeyContext(), ...args);
function Key(props) {
const { children, ...other } = props;
const state = useCtrl(KeyController, other);
return <KeyProvider value={state} children={children} />;
}
{/* ~~~ */}