preact
Preact 10のパターンにReact-compatとModule Federationシングルトン設定を組み合わせたスキルです。Preactコンポーネント、hooks、型定義の作成、またはRsbuild/Rslib/RstestでPreactを設定する場合に発動します。
description の原文を見る
Preact 10 patterns with React-compat and Module Federation singleton setup. Trigger: When writing Preact components, hooks, types, or configuring Preact in Rsbuild/Rslib/Rstest.
SKILL.md 本文
Context7 — ライブドキュメント
実装する前に、Context7 MCP 経由で最新のドキュメントを取得してください(古い API を避けるため):
resolve-library-id→"preact"get-library-docsで解決されたID と特定のトピックを指定
使用時期
- Preact の関数コンポーネントまたはカスタムフックを記述するとき
- フック、型、または JSX ユーティリティをインポートするとき
- Rsbuild / Rslib / Rstest を Preact 向けに設定するとき
- Module Federation の共有設定をセットアップするとき
forwardRefまたは互換ブリッジを使用するとき- JSX トランスフォーム の問題を確認または修正するとき
アーキテクチャ規則(コンポーネントを書く前に必ず読んでください)
このプロジェクトには shell(スマート)と ui-components(ダム)の厳密な分離があります。
ui-components — 表示のみ
- Zustand ストアをインポートまたは作成しない
- ビジネスロジック、認証、ルーティング、A/B テストを追加しない
- React Context を使用してもよい — ただし Provider は常に
shellに存在 - すべてのデータとコールバックを props または shell からのコンテキスト 経由で受け取る
- すべての依存関係は
peerDependencies— コンポーネント出力は何もバンドルしない
shell — スマートレイヤー
- Zustand ストア、Context プロバイダー、認証、ルーティング、ビジネスロジックを所有
- Zustand から読み込み、props またはコンテキスト経由で
ui-componentsにデータを渡す ui-componentsがデータを取得するために遡及しないようにする
状態判定木
グローバルなアプリ状態が必要? → shell 内の Zustand ストア、コンポーネントに prop として渡す
サブツリー全体で共有が必要? → shell 内の Context プロバイダー、コンポーネント内で useContext
ローカル UI 状態が必要? → コンポーネント内の useState / useReducer(ui-components でも問題なし)
アプリロジックをトリガーしたい? → shell からコンポーネントに渡されたコールバック prop
重要なパターン
1. tsconfig — 常に jsxImportSource: preact
モノレポ内のすべての tsconfig.json は以下を含める必要があります:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}
決して "jsxImportSource": "react" を使用しないでください。このプロジェクトは React 名前空間をインポートしません。
2. インポート — 信頼できる情報源
// ✅ フック → preact/hooks
import { useState, useEffect, useRef, useCallback, useMemo, useReducer, useContext, createContext } from "preact/hooks";
// ✅ コア型とプリミティブ → preact
import { h, Fragment, createRef, cloneElement } from "preact";
import type { FunctionalComponent, ComponentChildren, VNode, RefObject } from "preact";
// ✅ forwardRef, memo, lazy, Suspense → preact/compat
import { forwardRef, memo, lazy, Suspense } from "preact/compat";
// ❌ しない — react がエイリアスされていても直接インポートしない
import React from "react";
import { useState } from "react";
import type { FC } from "react";
3. 関数コンポーネントパターン
import type { FunctionalComponent, ComponentChildren } from "preact";
import { useState } from "preact/hooks";
interface CardProps {
title: string;
children: ComponentChildren;
onClose?: () => void;
}
export const Card: FunctionalComponent<CardProps> = ({ title, children, onClose }) => {
const [open, setOpen] = useState(true);
if (!open) return null;
return (
<div className="card">
<h2>{title}</h2>
<div>{children}</div>
{onClose && (
<button type="button" onClick={() => { setOpen(false); onClose(); }}>
Close
</button>
)}
</div>
);
};
4. フックパターン
import { useState, useEffect, useRef, useCallback } from "preact/hooks";
import type { RefObject } from "preact";
export function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// 型を指定した useRef
const inputRef: RefObject<HTMLInputElement> = useRef<HTMLInputElement>(null);
5. forwardRef パターン
import { forwardRef } from "preact/compat";
import type { Ref } from "preact";
interface InputProps {
label: string;
value: string;
onChange: (value: string) => void;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, value, onChange }, ref) => (
<label>
{label}
<input
ref={ref}
value={value}
onInput={(e) => onChange((e.target as HTMLInputElement).value)}
/>
</label>
)
);
6. Signals(オプション、@preact/signals がインストールされている場合)
import { signal, computed, effect } from "@preact/signals";
const count = signal(0);
const doubled = computed(() => count.value * 2);
effect(() => {
console.log("count changed:", count.value);
});
// コンポーネント内 — signal は読み取り時に自動購読
export const Counter: FunctionalComponent = () => (
<button type="button" onClick={() => count.value++}>
{count} × 2 = {doubled}
</button>
);
型リファレンス
| 型 | 出所 | 用途 |
|---|---|---|
FunctionalComponent<P> | preact | 関数コンポーネント(React.FC の代わり) |
ComponentChildren | preact | children prop の型(React.ReactNode の代わり) |
VNode | preact | JSX 要素の戻り値の型 |
RefObject<T> | preact | useRef<T>() の戻り値の型 |
JSX.CSSProperties | preact | インラインスタイルオブジェクト(React.CSSProperties の代わり) |
Ref<T> | preact | コールバックref と RefObject<T> の両方を受け入れ |
ComponentType<P> | preact | FC とクラスコンポーネント型の共用体 |
React との主な違い
| React | Preact の同等物 |
|---|---|
React.FC<P> | preact からの FunctionalComponent<P> |
React.ReactNode | preact からの ComponentChildren |
React.CSSProperties | preact からの JSX.CSSProperties |
import { useState } from "react" | import { useState } from "preact/hooks" |
import { forwardRef } from "react" | import { forwardRef } from "preact/compat" |
class 属性 | class と className の両方が機能(互換が正規化) |
React.createElement | preact からの h(ただし直接使用はほぼ不要) |
React.Fragment | preact からの Fragment または <>...</> 省略形 |
Module Federation — Singleton(重大)
Preact はすべての MF ホストとリモートで singleton として設定する必要があります。重複した Preact ランタイムはフックを無言で壊します。
// rsbuild.config.ts / rslib.config.ts
import { pluginModuleFederation } from "@module-federation/rsbuild-plugin";
export default {
plugins: [
pluginModuleFederation({
name: "my_app",
shared: {
preact: {
singleton: true,
requiredVersion: "^10.0.0",
},
"preact/hooks": {
singleton: true,
requiredVersion: "^10.0.0",
},
"preact/compat": {
singleton: true,
requiredVersion: "^10.0.0",
},
"preact/jsx-runtime": {
singleton: true,
requiredVersion: "^10.0.0",
},
},
}),
],
};
ビルド設定 — pluginPreact() は必須
pluginPreact() はすべての rsbuild.config.ts、rslib.config.ts、rstest.config.ts に存在する必要があります。これなしでは JSX トランスフォーム が壊れ、HMR が正しく機能しません。
// rsbuild.config.ts
import { defineConfig } from "@rsbuild/core";
import { pluginPreact } from "@rsbuild/plugin-preact";
export default defineConfig({
plugins: [pluginPreact()],
});
// rstest.config.ts
import { defineConfig } from "@rstest/core";
import { pluginPreact } from "@rsbuild/plugin-preact";
export default defineConfig({
plugins: [pluginPreact()],
// ... テスト設定
});
コマンド
bun run dev # Preact HMR 付きの dev サーバー
bun run test # pluginPreact() 付きで rstest を実行
bun run build # Preact 付きで rslib/rsbuild ビルド
bun run typecheck # tsc --noEmit — jsxImportSource を検証
Suspense + Refs — 重大なタイミングの落とし穴
親コンポーネントのフックが useEffect で ref に依存する場合、ref ターゲット要素を <Suspense> 境界内に配置しないでください。
コンポーネントが遅延子を <Suspense> でラップするとき、子は遅延インポート が解決されるまで DOM に存在しません。しかし親の useEffect はマウント時に即座に実行されます — ref.current はまだ null のときです。ref は安定したオブジェクト識別子なので、エフェクトは 再実行されず、リスナーまたはオブザーバーをアタッチするフック(useSwipe、useFocusTrap、useClickOutside、IntersectionObserver、ResizeObserver など)は ゼロエラーで無言に失敗します。
// ❌ BAD — useEffect 実行時に ref は null、リスナーがアタッチされない
const MyComponent: FunctionalComponent = () => {
const contentRef = useRef<HTMLDivElement>(null);
useSwipe(contentRef, { onSwipeLeft: goNext }); // エフェクト実行、ref.current は null → サイレント無操作
return (
<Suspense fallback={<Skeleton />}>
<LazyChild>
<div ref={contentRef}>content</div> {/* 遅延解決まで存在しない */}
</LazyChild>
</Suspense>
);
};
// ✅ GOOD — ref ターゲットは Suspense 外、マウント時に利用可能
const MyComponent: FunctionalComponent = () => {
const contentRef = useRef<HTMLDivElement>(null);
useSwipe(contentRef, { onSwipeLeft: goNext }); // エフェクト実行、ref.current 存在 ✓
return (
<div ref={contentRef}> {/* マウント時に存在、子からのイベントはバブルアップ */}
<Suspense fallback={<Skeleton />}>
<LazyChild>content</LazyChild>
</Suspense>
</div>
);
};
重要なポイント:
- Suspense は複数の遅延コンポーネントをラップできます(全体で1つのスケルトンは問題なし)
- フックが依存する ref ターゲットのみが境界外である必要があります
- 子 DOM イベント(タッチ、クリック、キーボード)は親 ref 要素に自然にバブルアップします
- このバグは 完全に無言です — エラーなし、警告なし、機能は動作しません
よくある間違いを避ける
"react"からフックをインポート — サイレント失敗またはスロー;常に"preact/hooks"を使用- rstest で
pluginPreact()を除外 — テストは JSX をパースできない - MF で重複した Preact — リモート境界を越えてフックの状態が失われる;常に
singleton: true React.FCを使用 —preactからのFunctionalComponent<P>を代わりに使用- 任意の tsconfig で
jsxImportSource: "react"— そのパッケージ全体の JSX トランスフォーム を破壊 <Suspense>内に ref ターゲット — useEffect 実行時に ref は null;リスナーがアタッチされない(上記セクション参照)
ライセンス: Apache-2.0(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- Hyperxq
- ライセンス
- Apache-2.0
- 最終更新
- 2026/4/28
Source: https://github.com/Hyperxq/modular-frontend-architecture / ライセンス: Apache-2.0
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。