Skip to main content

How to global state in React

React で Global state をつくってみました。codesandbox で demo をあそべます。


Context について

React は global state をつかうために React.createContext があります。 しかし、Context が増えると Provider地獄になったり、performance がさがります。 library は経験が必要かつ size も大きいので、global state をつくってみました。

// Context Example
import React from "react";
export const Context = React.createContext("");
export const Provider = Context.Provider;
export const useContext = () => React.useContext(Context);

atom

mount / clean で listener を Set に add / delete します。 flush は次の値について、すべての listener を呼び出します。

const atom = (value) => {
const set = new Set();
const ret = () => value;
ret.mount = (fun) => void set.add(fun);
ret.clean = (fun) => void set.delete(fun);
ret.flush = (next) => {
const isFun = typeof next === "function";
value = isFun ? next(value) : next;
set.forEach((l) => l(value));
};
return ret;
};

useAtomValue

React をつかっているので、listener としてset が add / delete されます。 atom は immutable なので、 useEffect は mount / clean でのみ実行されます。

const useAtomValue = (atom) => {
const [value, set] = useState(atom);
useEffect(() => void atom.mount(set), [atom]);
useEffect(() => () => atom.clean(set), [atom]);
return value;
};

Getting started

以上で global state をつくることができました。 今回は、 Recoilの Getting Started を移植してみます。 TextValue, TextCount, TextInput component をつくります。

createRoot(document.getElementById("root")).render(
<>
<TextValue />
<TextCount />
<TextInput />
</>
);

TextValue, TextCountuseAtomValue から textAtom の値を取得し、 TextInputatom.flush から値を update します。

import { atom, useAtom, useAtomValue } from "./hooks";

const textAtom = atom("");
const set = (e) => textAtom.flush(e.target.value);

const TextValue = () => <span>Echo: {useAtomValue(textAtom)}</span>;
const TextCount = () => <div>Count: {useAtomValue(textAtom).length}</div>;
const TextInput = () => <input defaultValue={text()} onChange={set} />;

以下のコードは上記と同様にうごきます。

useAtomuseState とおなじように使えるようにします。 TextInputuseAtom をつかって、textAtom の値を更新します。

const useSetAtom = (atom) => atom.flush;

const useAtom = (atom) => [useAtomValue(atom), atom.flush];

const TextInput = () => {
const [text, set] = useAtom(textAtom);
const handleChange = (e) => set(e.target.value);
return <input defaultValue={text} onChange={handleChange} />;
}

migrate to solid.js

solid.jsに移植してみました。 codesandbox であそぶことができます。

import html from "https://cdn.skypack.dev/solid-js/html";
import { atom } from "https://cdn.skypack.dev/-/[email protected]/dist=es2019,mode=imports/optimized/reii.js";
import { render } from "https://cdn.skypack.dev/solid-js/web";
import { createSignal, onCleanup, onMount } from "https://cdn.skypack.dev/solid-js";

const useAtomValue = (atom) => {
const [value, set] = createSignal(atom);
onMount(() => void atom.mount(set));
onCleanup(() => () => atom.clean(set));
return value;
};

const useAtom = (atom) => [useAtomValue(atom), atom.flush];
const textAtom = atom("");
const set = (e) => textAtom.flush(e.target.value);

const TextValue = () => html`<span>Echo: ${useAtomValue(textAtom)}</span>`;
const TextCount = () => html`<div>Count: ${useAtomValue(textAtom).length}</div>`;
const TextInput = () => html`<input defaultValue=${text} onKeyup=${set} />`;
const App = () => html`<div>${TextValue}${TextCount}${TextInput}</div>`;

render(App, document.body);

migrate to pure.js

今回の demoatom だけで十分動かすことができます。 codesandbox であそぶことができます。

<body>
<div>
<span id="echo">Echo:</span>
<div id="count">Count:</div>
<input id="input"></input>
</div>
<script type="module">
import { atom } from "https://cdn.skypack.dev/-/[email protected]/dist=es2019,mode=imports/optimized/reii.js";

const echo = document.getElementById("echo");
const count = document.getElementById("count");
const input = document.getElementById("input");
const textAtom = atom("");

textAtom.mount(() => echo.textContent = "Echo: " + textAtom());
textAtom.mount(() => count.textContent = "Count: " + textAtom().length);
input.addEventListener("keyup", (e) => textAtom.flush(e.target.value));
</script>
</body>