🍖

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() が呼ばれると...

  1. currentHook = 0

  2. useState(0)hooks[0] を使う

  3. currentHook++ → 1

  4. useState(false)hooks[1] を使う

  5. また 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;
}

これで:

  1. 仮想DOMノードからDOMを生成

  2. 関数コンポーネントも対応

  3. イベントバインドもOK

  4. ネストされた子要素も描画!

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版で書き直したりもできます!必要だったら声かけてください 🙌

Social PLUS Tech Blog

Discussion