MiniReactでReactの骨格を再現してみた
こんにちは!Reactを普段使っていて、
「これって中で何が起きてるの?」
と思ったことありませんか?
今回は、Reactの超ミニマル版「MiniReact」を自作してみます。
全部JavaScriptで作ります。難しい依存関係はナシ!
対応している機能はこちら:
-
関数コンポーネント ✅
-
useState()(複数呼び出し対応)✅ -
JSXっぽい記法(
h()関数)✅ -
仮想DOMからの描画 ✅
なんと100行以下!
なぜ作るの?
Reactはすごく便利だけど、中身はちょっとブラックボックス。
シンプルなReactを自作すると、次のような仕組みがスッキリ理解できます:
-
useState()はどうやって状態を覚えてるの? -
コンポーネントはどうやって再描画されるの?
-
仮想DOMって何?
-
イベントや再描画の流れはどうなってるの?
Reactの骨格をのぞいてみたい方、ぜひ一緒にコードをかじっていきましょう🦴🐶
ファイル構成
使うファイルはたったの3つ:
index.html // HTMLの入口
mini-react.js // MiniReactのコア
app.js // MiniReactを使ったアプリ
index.html
<body>
<div id="app"></div>
<script type="module" src="app.js"></script>
</body>
ここでアプリをマウントします。Reactでいう ReactDOM.render() と同じですね。
useStateの仕組み
まず状態を記憶するための変数を準備:
let hooks = [];
let currentHook = 0;
hooks[] はすべての状態を保存する配列。
currentHook は「今どの状態を使ってるか」を指すポインタです。
useState 本体
function useState(initialValue) {
const hookIndex = currentHook;
hooks[hookIndex] = hooks[hookIndex] ?? initialValue;
function setState(newValue) {
hooks[hookIndex] = newValue;
currentHook = 0; // ← ここ超大事!
rerender(); // UIを再描画!
}
currentHook++;
return [hooks[hookIndex], setState];
}
要点は:
-
hooks[]に状態を保存 -
currentHookを進めながら順番に使う -
setState()で状態を更新→再描画!
描画中に何が起きてるの?
たとえばこんなコンポーネントがあったとします:
function Counter() {
const [count, setCount] = useState(0);
const [clicked, setClicked] = useState(false);
}
このとき render() が呼ばれると...
-
currentHook = 0 -
useState(0)はhooks[0]を使う -
currentHook++→ 1 -
useState(false)はhooks[1]を使う -
また
currentHook++
currentHook = 0 をリセットしなかったら?
次のレンダーで useState() を呼んでも、
間違ったインデックスから読んでしまいます!
-
本来
hooks[0]を読むはずがhooks[2]に… -
状態がめちゃくちゃに!
-
アプリの動作が不安定に!
だから毎回 currentHook = 0 にリセット!
レンダーのたびにフックの順番が決まっている必要があります。
-
最初の
useState()はhooks[0] -
次は
hooks[1] -
その次は
hooks[2]…
Reactも同じルールです。だから、
-
フックは条件分岐の中で呼んじゃダメ!
-
必ずコンポーネントの最上部で呼ぶ!
というルールがあるんですね。
補足:currentHook = 0 の場所について
実は setState() の中で currentHook = 0 としているのは、
「ここでリセットが必要だよ!」という説明のためです。
でも実際には、リセット処理は rerender() の中にまとめた方がスッキリします:
function rerender() {
currentHook = 0;
// ...
}
これなら他の場所でリセット忘れを防げます。
setState() の中の currentHook = 0 は削除してOKです!
h() - JSXの代わり
export function h(type, props, ...children) {
return { type, props: props || {}, children };
}
たとえば <div>Hello</div> を書きたければ:
h("div", null, "Hello")
これで仮想DOMノードが作れます!
createDom() - 仮想DOM → 本物のDOMへ!
function createDom(node) {
if (typeof node === "string" || typeof node === "number") {
return document.createTextNode(node);
}
if (typeof node.type === "function") {
return createDom(node.type({ ...node.props, children: node.children }));
}
const el = document.createElement(node.type);
Object.entries(node.props || {}).forEach(([key, value]) => {
if (key.startsWith("on") && typeof value === "function") {
el.addEventListener(key.slice(2).toLowerCase(), value);
} else if (key !== "children") {
el.setAttribute(key, value);
}
});
node.children
.flat()
.map(createDom)
.forEach(child => el.appendChild(child));
return el;
}
これで:
-
仮想DOMノードからDOMを生成
-
関数コンポーネントも対応
-
イベントバインドもOK
-
ネストされた子要素も描画!
render()とrerender()
let rootComponent, rootContainer;
export function render(component, container) {
rootComponent = component;
rootContainer = container;
rerender();
}
function rerender() {
currentHook = 0;
rootContainer.innerHTML = "";
const dom = createDom(rootComponent());
rootContainer.appendChild(dom);
}
-
render()は初回レンダリング -
rerender()は状態が変わったときの再描画
app.jsでMiniReactを使ってみよう!
import { h, render, useState } from "./mini-react.js";
function Counter() {
const [count, setCount] = useState(0);
return h("div", null,
h("h1", null, `Count: ${count}`),
h("button", { onClick: () => setCount(count + 1) }, "Increment"),
h("button", { onClick: () => setCount(count - 1) }, "Decrement")
);
}
function App() {
return h("main", null,
h("h2", null, "Mini React Demo"),
h(Counter),
h(Counter)
);
}
render(App, document.getElementById("app"));
-
h()でUIを定義 -
useState()で状態を使う -
setCount()で状態を更新&再描画 -
コンポーネントのネストもOK!
※注意:この実装だと hooks[] は全コンポーネントで共有されているため、完全なReact互換ではありません。複数コンポーネントの状態を独立させるには「Fiberツリー」などの工夫が必要です。
まとめ
| 概念 | 実装内容 |
|---|---|
| 仮想DOM |
h() オブジェクトで表現 |
| 関数コンポーネント | 普通の関数でOK |
| useState & フック | 配列で状態管理 |
| 再描画 |
rerender() を手動で呼び出し |
| JSX代替 |
h() を使ってUIを記述 |
| イベントバインド |
onClick などで直接紐付け |
もっと進化させたいなら?
MiniReactは以下のように拡張できます:
-
useEffect()の追加 -
仮想DOMの差分計算(diffing)
-
複数のフックを持つコンポーネント管理(Fiber)
-
JSXを本物として使いたい場合はBabel導入
-
サーバーサイドレンダリング(SSR)
おわりに
Reactの裏側って、実はただの関数と状態とDOMの組み合わせなんです。
MiniReactを自作してみると、今まで「魔法」に見えていたReactの挙動が、
「あれ、意外とシンプルじゃん」と思えるはず!
本物のReactに手を出す前に、こうやって自分のReact脳を鍛えるのもおすすめですよ!
この内容をGitHubリポジトリにしたり、TypeScript版で書き直したりもできます!必要だったら声かけてください 🙌
Discussion