preact の使い方

Fast 3kb React-compatible Virtual DOM library.

v10.29.114.7M/週MITフレームワーク
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

Preact の使い方 — 3kBの高速React互換Virtual DOMライブラリ

一言でいうと

PreactはReactと同じAPIを持ちながら、わずか**3kB(gzip)**という超軽量なVirtual DOMライブラリです。React互換レイヤー(preact/compat)を使えば、既存のReactエコシステムのほとんどをそのまま利用できます。

どんな時に使う?

  • バンドルサイズを極限まで削減したい時 — パフォーマンスが重要なLP、ウィジェット、埋め込みUIなどで、Reactの代わりに採用することでバンドルサイズを大幅に削減できます
  • 既存のReactプロジェクトを軽量化したい時preact/compatエイリアスを設定するだけで、既存のReactコードをほぼそのまま移行できます
  • サードパーティサイトに埋め込むウィジェットを開発する時 — 3kBという軽量さにより、他サイトへの影響を最小限に抑えたコンポーネント配布が可能です

インストール

# npm
npm install preact

# yarn
yarn add preact

# pnpm
pnpm add preact

TypeScriptを使う場合、Preactには型定義が同梱されているため、@typesパッケージの追加インストールは不要です。

tsconfig.jsonでJSXの設定を行います:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  }
}

基本的な使い方

最もよく使うパターンは、関数コンポーネント + Hooksの組み合わせです。

import { render } from 'preact';
import { useState, useCallback } from 'preact/hooks';

interface Todo {
  id: number;
  text: string;
  done: boolean;
}

const TodoApp = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [input, setInput] = useState('');

  const addTodo = useCallback(() => {
    if (!input.trim()) return;
    setTodos((prev) => [
      ...prev,
      { id: Date.now(), text: input, done: false },
    ]);
    setInput('');
  }, [input]);

  const toggleTodo = useCallback((id: number) => {
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      )
    );
  }, []);

  return (
    <div>
      <h1>Todo List</h1>
      <div>
        <input
          value={input}
          onInput={(e) => setInput((e.target as HTMLInputElement).value)}
          placeholder="タスクを入力"
        />
        <button onClick={addTodo}>追加</button>
      </div>
      <ul>
        {todos.map((todo) => (
          <li
            key={todo.id}
            onClick={() => toggleTodo(todo.id)}
            style={{ textDecoration: todo.done ? 'line-through' : 'none', cursor: 'pointer' }}
          >
            {todo.text}
          </li>
        ))}
      </ul>
    </div>
  );
};

render(<TodoApp />, document.getElementById('app')!);

よく使うAPI — Preactの主要APIの使い方

1. render — DOMへのマウント

アプリケーションのエントリーポイントで使用します。ReactのcreateRootに相当する機能を1つの関数で提供します。

import { render } from 'preact';

const App = () => <h1>Hello, Preact!</h1>;

// マウント
render(<App />, document.getElementById('app')!);

// アンマウント(nullをrenderする)
render(null, document.getElementById('app')!);

2. useState / useReducer — 状態管理(Hooks)

Reactと同じHooks APIがpreact/hooksから提供されます。

import { useState, useReducer } from 'preact/hooks';

// useState
const Counter = () => {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>;
};

// useReducer
type Action = { type: 'increment' } | { type: 'decrement' };

const reducer = (state: number, action: Action): number => {
  switch (action.type) {
    case 'increment': return state + 1;
    case 'decrement': return state - 1;
    default: return state;
  }
};

const CounterWithReducer = () => {
  const [count, dispatch] = useReducer(reducer, 0);
  return (
    <div>
      <span>{count}</span>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  );
};

3. useEffect / useRef — 副作用とDOM参照

import { useEffect, useRef } from 'preact/hooks';

const AutoFocusInput = () => {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    // マウント時にフォーカス
    inputRef.current?.focus();

    // クリーンアップ関数
    return () => {
      console.log('コンポーネントがアンマウントされました');
    };
  }, []);

  return <input ref={inputRef} placeholder="自動フォーカスされます" />;
};

4. createContext — コンテキストによる状態共有

import { createContext } from 'preact';
import { useContext, useState } from 'preact/hooks';

interface ThemeContext {
  theme: 'light' | 'dark';
  toggle: () => void;
}

const ThemeCtx = createContext<ThemeContext>({
  theme: 'light',
  toggle: () => {},
});

const ThemeProvider = ({ children }: { children: preact.ComponentChildren }) => {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');
  const toggle = () => setTheme((t) => (t === 'light' ? 'dark' : 'light'));

  return (
    <ThemeCtx.Provider value={{ theme, toggle }}>
      {children}
    </ThemeCtx.Provider>
  );
};

const ThemedButton = () => {
  const { theme, toggle } = useContext(ThemeCtx);
  return (
    <button
      onClick={toggle}
      style={{
        background: theme === 'dark' ? '#333' : '#fff',
        color: theme === 'dark' ? '#fff' : '#333',
      }}
    >
      現在のテーマ: {theme}
    </button>
  );
};

5. preact/compat — React互換レイヤー

既存のReactライブラリをPreactで使うためのエイリアス設定です。バンドラーの設定でreactのインポートをpreact/compatにリダイレクトします。

Viteの場合(vite.config.ts):

import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';

export default defineConfig({
  plugins: [preact()],
});

webpackの場合(webpack.config.js):

module.exports = {
  resolve: {
    alias: {
      'react': 'preact/compat',
      'react-dom': 'preact/compat',
      'react-dom/test-utils': 'preact/compat',
      'react/jsx-runtime': 'preact/jsx-runtime',
    },
  },
};

これにより、react-router-domreact-queryなど多くのReactライブラリがそのまま動作します。

// preact/compat経由でReact APIを使用
import { forwardRef, memo, lazy, Suspense } from 'preact/compat';

const ExpensiveList = memo(({ items }: { items: string[] }) => {
  return (
    <ul>
      {items.map((item, i) => (
        <li key={i}>{item}</li>
      ))}
    </ul>
  );
});

const LazyComponent = lazy(() => import('./HeavyComponent'));

const App = () => (
  <Suspense fallback={<div>Loading...</div>}>
    <LazyComponent />
  </Suspense>
);

類似パッケージとの比較

特徴PreactReactInfernoSolid
バンドルサイズ(gzip)~3kB~42kB~9kB~7kB
Virtual DOMありありありなし(リアクティブ)
React互換性◎(preact/compat)△(inferno-compat)
Hooks対応✅(部分的)✕(独自API)
SSR
エコシステムReactのものを流用可最大小規模成長中
TypeScript同梱@types/react@types/inferno同梱
学習コストReactと同等基準Reactと同等やや高い

選定の指針:

  • バンドルサイズを最優先し、Reactエコシステムも活用したい → Preact
  • エコシステムの広さ・安定性を最優先 → React
  • ランタイムパフォーマンスを極限まで追求 → Solid

注意点・Tips

1. イベントハンドラの命名が異なる

PreactではonChangeではなくonInputを使うのが正しい挙動です。ReactのonChangeはブラウザネイティブのinputイベントに対応していますが、Preactはブラウザ標準に忠実です。preact/compatを使えばReactと同じonChangeの挙動になります。

// Preact(preact/compatなし)
<input onInput={(e) => setValue((e.target as HTMLInputElement).value)} />

// preact/compat使用時はReactと同じ書き方でOK
<input onChange={(e) => setValue(e.target.value)} />

2. preact/compatのコストを理解する

preact/compatを追加すると約2kB増加します。それでもReact本体より遥かに小さいですが、React互換が不要ならpreactのコアAPIだけで構築するのが最も軽量です。

3. Signalsによる状態管理(推奨)

Preactチームが開発した@preact/signalsは、Hooksよりも効率的な状態管理を提供します。コンポーネント全体の再レンダリングを回避し、値が使われている箇所だけを更新します。

npm install @preact/signals
import { signal, computed } from '@preact/signals';

const count = signal(0);
const doubled = computed(() => count.value * 2);

const Counter = () => (
  <div>
    <p>Count: {count}</p>
    <p>Doubled: {doubled}</p>
    <button onClick={() => count.value++}>+1</button>
  </div>
);

4. DevToolsはReact DevToolsがそのまま使える

preact/devtoolsをアプリのエントリーポイントでインポートするだけで、ブラウザのReact DevTools拡張機能が使えます。

// main.tsx のトップに追加(開発時のみ)
if (process.env.NODE_ENV === 'development') {
  import('preact/debug');
}

5. preact/debugを開発時に有効にする

preact/debugをインポートすると、不正なpropsやkeyの欠落など、開発時に有用な警告が表示されます。本番ビルドでは必ず除外してください。

まとめ

Preactは「Reactと同じ書き心地で、バンドルサイズを劇的に削減したい」というニーズに最適なライブラリです。preact/compatによる高いReact互換性により、既存のReactプロジェクトからの移行も現実的な選択肢になります。パフォーマンスが重要なプロジェクトや、軽量なウィジェット開発において、まず検討すべきライブラリの一つです。