はじめに:カウンターはただの練習問題ではない

「カウンターなんてHello Worldと同じでしょ?」
たしかに機能としてはそうかもしれません。しかし、実務でUIを設計・レビューする立場に立つと、カウンターは単なる加算機能ではなくなります。

  • 状態をどこに定義するのか
  • イベントと状態の結びつけ方は明示的か
  • DOM更新のトリガーは何か
  • 初期レンダリングと再描画の扱いに違いはあるか
  • 構造が大きくなったときに状態が破綻しないか

そうした要素が、たった「+1」のボタンに全部詰まっているのです。
この記事では、主要9種のUIフレームワーク/ライブラリを横並びで比較し、構造・可読性・開発工数感・レビューしやすさを検証していきます。

比較対象:9種の主要フレームワーク/ライブラリ

今回比較するのは以下の9技術です。

  • React(with Hooks)
  • Vue(Composition API)
  • SolidJS
  • Preact
  • Qwik
  • Svelte
  • Angular
  • Lit
  • Astro(+任意UIライブラリ)

比較の前提と想定シナリオ

前提仕様:

  • UI上に「現在の数値」と「+1」ボタンを表示
  • ボタンクリックで数値を加算
  • 再描画は状態の変更によってトリガーされる
  • 表示文例:count: 5

評価軸:

  • コード量(状態定義、DOM、イベント)
  • 状態管理の明示性
  • 記述の自然さ(JSX/テンプレート)
  • 開発工数(セットアップ・理解コスト)
  • レビュー観点(副作用、責務分離など)

React:状態とイベントをHooksで構造化

react
import { useState } from 'react'
function Counter() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <p>count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  )
}

分析:

  • useStateにより状態の定義は明示的。
  • イベント(onClick)と状態変更が結合されており、構造が直感的。
  • JSXに慣れていれば可読性は高いが、初学者にはJavaScriptとの混在が冗長に感じることも。

工数観点:

  • Reactプロジェクトの起動・開発環境構築にはcreate-react-appViteが必要。
  • 状態が増えるとHooksの抽象化が必要になり、レビューでは依存追跡が課題になる。

Vue(Composition API):テンプレートとロジックの分離

vue
<template>
  <div>
    <p>count: {{ count }}</p>
    <button @click="increment">+1</button>
  </div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
</script>

分析:

  • 状態はref()で定義され、テンプレート上では``でバインディング。
  • イベントとロジックを分離して記述できる。
  • ref.value++などVue独自の書き方には慣れが必要。

工数観点:

  • script setup構文を使えば、記述量はReactより少なめ。
  • テンプレート記述とロジックが明確に分かれており、UIと状態の責務が自然に読める。
  • 状態が多くなるとwatchcomputedを追加する必要が出るが、構造自体は破綻しにくい。

SolidJS:最小粒度のリアクティブ構造

SolidJS
import { createSignal } from 'solid-js'
function Counter() {
  const [count, setCount] = createSignal(0)
  return (
    <div>
      <p>count: {count()}</p>
      <button onClick={() => setCount(count() + 1)}>+1</button>
    </div>
  )
}

分析:

  • 状態はcreateSignalで定義され、値の取得はcount()という呼び出し形式。
  • JSX構造はReactに近いが、再描画の仕組みは仮想DOMを使わず部分的に最適化されている。
  • イベントとの結合も自然で、依存関係は自動で追跡される。

工数観点:

  • React経験者なら習得は容易。
  • 状態更新が関数呼び出し形式のため、初見では違和感を持つ可能性あり。
  • コード量は少なく、リアクティブ性の理解さえあれば設計・レビューともに効率が良い。

Preact:React構文互換の超軽量実装

Preact
import { h } from 'preact'
import { useState } from 'preact/hooks'
function Counter() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <p>count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  )
}

分析:

  • 構文・設計はReactと完全互換。違いはパッケージ名とパフォーマンス。
  • 既存のReact知識でそのまま読み書きが可能。
  • 仮想DOMやHooksも提供されており、開発者体験に大きな違いはない。

工数観点:

  • Reactベースの既存コードをそのまま移植可能。
  • ファイルサイズや初期読み込みが重要な環境では選定理由になる。
  • 実質的にReactコードのレビュー手法がそのまま適用可能。

Qwik:遅延評価とResumable構造のカウンター

Qwik
import { component$, useSignal } from '@builder.io/qwik'
export const Counter = component$(() => {
  const count = useSignal(0)
  return (
    <div>
      <p>count: {count.value}</p>
      <button onClick$={() => count.value++}>+1</button>
    </div>
  )
})

分析:

  • 状態はuseSignalで定義され、.valueでアクセス。
  • JSX構文ながら、イベントはonClick$のようにQwik専用シンタックス。
  • JavaScriptは初期描画時に実行されず、イベント発火まで遅延される(Resumable)。

工数観点:

  • コンセプト理解に時間がかかる。特に復元・再実行タイミングの設計には習熟が必要。
  • 小規模では過剰になりがちだが、パフォーマンス要件が厳しいプロジェクトでは有力。
  • コード量は少なく見えるが、ランタイムの挙動まで把握する工数が別途必要。

Svelte:テンプレート駆動で直感的な構造

Svelte
<script>
  let count = 0
  const increment = () => count += 1
</script>
<div>
  <p>count: {count}</p>
  <button on:click={increment}>+1</button>
</div>

分析:

  • 状態はプレーン変数で定義し、書き換えによって再描画が自動で走る。
  • テンプレートとロジックが同一ファイルにあり、読みやすく書きやすい。
  • $:reactive statementがないぶん、シンプルなカウンターでは明快。

工数観点:

  • 学習コストは非常に低く、状態の扱いも自然。
  • ビルドでDOM命令に変換されるため、構造は素直でもデバッグは若干注意が必要。
  • 小〜中規模での導入は容易。構造が大きくなると状態の集中設計が必要。

Angular:型と構造で統制されたイベント設計

Angular
@Component({
  selector: 'app-counter',
  template: `
    <div>
      <p>count: </p>
      <button (click)="increment()">+1</button>
    </div>
  `
})
export class CounterComponent {
  count = 0
  increment() {
    this.count++
  }
}

分析:

  • @Componentでテンプレートとロジックを明示的に分離。
  • countはクラス変数、イベントはメソッド呼び出しとして定義。
  • DI、モジュール構成、サービス注入などの影響がない分、シンプルな例では冗長に見える。

工数観点:

  • セットアップと構造理解に一定の学習コストあり。
  • 小さな構造では過剰な仕組みが多く感じられるが、大規模拡張を前提とした設計力を評価できる。
  • レビュー時には責務の分離が明確で、意図の読み取りやすさは高い。

Lit:Web標準に即した再描画モデル

Lit
import { LitElement, html } from 'lit'
import { customElement, state } from 'lit/decorators.js'
@customElement('my-counter')
class MyCounter extends LitElement {
  @state() count = 0
  render() {
    return html`
      <div>
        <p>count: ${this.count}</p>
        <button @click=${() => this.count++}>+1</button>
      </div>
    `
  }
}

分析:

  • @state()でプロパティが変更可能状態になる。更新は自動的にDOMに反映。
  • HTMLテンプレートに近い構文だが、記述はJavaScript内。
  • Shadow DOMによりスタイルや構造が分離され、独立性が高い。

工数観点:

  • Web標準を活かして書けるため、長期的に保守性が高い。
  • 状態やイベントは構造として読みやすいが、テンプレート記法にはやや癖がある。
  • Web Componentsとの親和性が高く、再利用重視の場面に向く。

Astro:HTML重視構成にJavaScriptを限定的に使う

astro

// counter.astro
let count = 0
function increment() {
  count += 1
}
<div>
  <p>count: {count}</p>
  <button on:click={increment}>+1</button>
</div>

分析:

  • Astro単体では状態変更による再描画はできない。上記はビルド時の静的描画。
  • 実際のカウンターを動かすにはReact/Svelte等の「Island」を別途導入する必要がある。
  • ここでは「状態を持たない」構造を基本とし、動的要素は局所化される。

工数観点:

  • 構造の把握はしやすい。HTMLとしてまず完成させられる。
  • 動的UIの必要な範囲にだけJSを導入する思想は、レビューや保守性の面でも理にかなっている。
  • ただし実装の「途中まで」は容易だが、インタラクション導入から一段難易度が上がる

各フレームワークにおける工数感のまとめ

技術 状態定義 JSX/テンプレート 構造の直感性 学習負荷 初期セットアップ
React useState JSX
Vue ref テンプレート
SolidJS createSignal JSX
Preact useState JSX
Qwik useSignal$ JSX + Resumable 低(要慣れ)
Svelte 変数 テンプレート
Angular クラス変数 テンプレート 中〜高
Lit @state html``
Astro HTMLベース 静的 + Island 高(静的部分)
  • 状態の扱いが素直なものは、実装が早くレビューも楽
  • JSX/テンプレートの構造が実DOMと近いほど可読性が高い
  • セットアップコストはプロジェクト体制に直結し、特に初期の技術選定時に重視される

まとめ:たかがカウンター、されどカウンター

UIを最小構成で書くとき、最も見えてくるのは「構造設計の思想」です。

  • 状態をどう持つか
  • どのようにDOMに反映させるか
  • イベントと状態の結びつきをどこまで明示するか

たとえ「+1」しか機能しないコードであっても、その技術の全体設計方針が現れるのがカウンター実装です。

レビューアーとしては、この初期構造を観察することで、

  • その技術が読みやすいか
  • チーム内で共有可能か
  • 将来的に責務を分離できるか

といった視点で選定の判断材料とすることができます。

カウンターは、構造の鏡です。

次回は同じ観点でフォーム入力と状態バインディングの比較を行う予定です。