Skip to main content

Stateful React via event

state 管理をするとき、ReactではReduxjotaiなどのパッケージを利用しますが、 今回は 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 , SetMap ではなるべく 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} />;
}

{/* ~~~ */}

Happy HardCode🙃