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-domやreact-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>
);
類似パッケージとの比較
| 特徴 | Preact | React | Inferno | Solid |
|---|---|---|---|---|
| バンドルサイズ(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プロジェクトからの移行も現実的な選択肢になります。パフォーマンスが重要なプロジェクトや、軽量なウィジェット開発において、まず検討すべきライブラリの一つです。