协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
preact【开发与工程】Use this skill when the user says 'Preact', 'Preact setup', 'Preact signals', 'Preact hooks', 'Preact vs React', 'Preact project', 'Preact SSR', or when creating a Preact application. This skill enforces: Preact as React drop-in, signals for state management, compat layer usage, tiny bundle patterns, hooks compliance. Requires package.json with preac…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Build ultra-lightweight Preact applications using signals for reactive state, compat layer for React interop, and hooks for component logic — all under 3kB.
Exact user phrases: "Preact setup", "Preact signal", "Preact hooks", "Preact project", "Preact vs React", "Preact SSR", "preact app".
Before activating, verify:
No file output. Produces code snippets, config examples, and structural guidance as text.
Config:
// vite.config.ts
import { defineConfig } from 'vite'
import preact from '@preact/preset-vite'
export default defineConfig({ plugins: [preact()] })
Code: show component, signal, and hook definitions inline. No import statements.
No preamble. No postamble. No explanations. Compress output — why use many token when few do trick.
render() from preact (not ReactDOM).h pragma or compat layer.~4096 tokens.
What type of state?
Local UI state (toggle, input) -> useSignal() from @preact/signals
Shared across few siblings -> Props lifting + signals
Global application state -> Signals in a shared module file
Server/async state -> useSWR or TanStack Query (via compat)
Complex form state -> useReducer from preact/hooks
React library using state? -> Use preact/compat aliasing + their API
Does the project use React ecosystem libraries?
No -> Use Preact natively (smallest bundle)
Yes -> What libraries?
React Router -> preact/compat aliasing works
TanStack Query -> preact/compat aliasing works
Radix UI -> preact/compat may have issues — test first
Framer Motion -> Known compat issues — consider alternatives
// vite.config.ts
import { defineConfig } from 'vite'
import preact from '@preact/preset-vite'
export default defineConfig({
plugins: [preact()]
})
// src/main.tsx
import { render } from 'preact'
import { App } from './app'
render(<App />, document.getElementById('app')!)
import { signal, computed } from '@preact/signals'
const count = signal(0)
const doubled = computed(() => count.value * 2)
function Counter() {
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => count.value++}>+1</button>
</div>
)
}
// Global signals in module
// store/cart.ts
import { signal, computed } from '@preact/signals'
export const cartItems = signal<CartItem[]>([])
export const cartCount = computed(() => cartItems.value.reduce((sum, i) => sum + i.qty, 0))
export const cartTotal = computed(() => cartItems.value.reduce((sum, i) => sum + i.price * i.qty, 0))
export function addItem(item: CartItem) {
cartItems.value = [...cartItems.value, item]
}
// Signals with side effects
import { effect } from '@preact/signals'
effect(() => {
console.log('Cart updated:', cartItems.value)
localStorage.setItem('cart', JSON.stringify(cartItems.value))
})
import { useState, useEffect, useMemo } from 'preact/hooks'
function Timer() {
const [seconds, setSeconds] = useState(0)
useEffect(() => {
const id = setInterval(() => setSeconds(s => s + 1), 1000)
return () => clearInterval(id)
}, [])
return <div>{seconds}s elapsed</div>
}
// Replace React imports with preact/compat
import React from 'preact/compat'
import ReactDOM from 'preact/compat'
// Or in vite.config:
// resolve: { alias: { react: 'preact/compat', 'react-dom': 'preact/compat' } }
import { render } from 'preact-render-to-string'
import { App } from './app'
const html = render(<App />)
// Inject into HTML template and serve
import { lazy, Suspense } from 'preact/compat'
const Dashboard = lazy(() => import('./dashboard'))
const Settings = lazy(() => import('./settings'))
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Dashboard />
</Suspense>
)
}
render() from preact, not ReactDOM.createRoot().{count} in JSX works (automatic .value access), but in hooks/effects use count.value.hydrate().render() from preact, never ReactDOM.createRoot().@preact/signals) over useState for shared state.preact/hooks for lifecycle effects.createContext when signals provide simpler reactivity.| Aspect | Preact | React | SolidJS |
|---|---|---|---|
| Bundle size | ~3KB | ~45KB | ~8KB |
| Signals | Built-in (@preact/signals) | External | Built-in (createSignal) |
| VDOM | Yes, lightweight | Full | No (compiled) |
| React compat | preact/compat | Native | N/A |
| Hooks | Compatible subset | Full | Different API |
| SSR | preact-render-to-string | react-dom/server | solid-ssr |
Preact is a React-compatible library at 1/15th the size. It supports most React APIs (hooks, JSX, Context) but lacks React 18's concurrent features and some edge cases. Use Preact when bundle size is critical and advanced React features are not needed.
Both are lightweight alternatives to React. SolidJS avoids VDOM entirely for fine-grained updates; Preact keeps a lightweight VDOM for React compatibility. Preact is the drop-in replacement; SolidJS requires rewriting components.
useCallback and useMemo sparingly — Preact's diff is cheaper than React's.preact/compat has PureComponent and React.memo support.lazy() + Suspense (via compat).import { render, screen, fireEvent } from '@testing-library/preact'
import { Counter } from './Counter'
test('renders counter and increments', () => {
render(<Counter />)
expect(screen.getByText('Count: 0')).toBeTruthy()
fireEvent.click(screen.getByRole('button'))
expect(screen.getByText('Count: 1')).toBeTruthy()
})
import { signal, computed } from '@preact/signals'
test('signal computed values', () => {
const count = signal(0)
const doubled = computed(() => count.value * 2)
expect(doubled.value).toBe(0)
count.value = 5
expect(doubled.value).toBe(10)
})
@testing-library/preact for component tests.act() from preact/test-utils for async rendering.preact/compat in tests.| React | Preact |
|---|---|
import React from 'react' | import { h } from 'preact' or compat |
ReactDOM.createRoot().render() | render() from preact |
useState | useState from preact/hooks or signals |
createContext | createContext from preact or signals |
React.lazy | lazy() from preact/compat |
React.memo | memo() from preact/compat |
Migration steps: 1) Install preact + @preact/preset-vite, 2) Add alias in vite.config, 3) Replace react-dom imports with preact, 4) Add preact/compat for remaining React libs, 5) Verify bundle size reduction.
| Vue Concept | Preact Equivalent |
|---|---|
ref() | signal() |
computed() | computed() from @preact/signals |
watch() | effect() from @preact/signals |
v-if | {condition && <Component/>} |
v-for | {items.map(i => <Item />)} |
| Props | Component props argument |
@preact/preset-vite auto-configures JSX pragma.resolve.alias = { react: 'preact/compat', 'react-dom': 'preact/compat' }.vite-plugin-bundle-analyzer to verify preact/compat usage.preact-render-to-string for Node.js rendering, prerender for static sites.@preact/preset-vite — official Vite preset with HMR and alias config.preact-render-to-string — SSR rendering for Preact.@testing-library/preact — component testing utilities.preact-ssr-prepass — data prefetching for SSR (Apollo, TanStack Query).preact-iso — routing and lazy loading for Preact SPA without compat.preact-hooks-testing-library — hook testing utilities.npx preact size compares Preact vs React bundle size.| Library | Compat Status |
|---|---|
| React Router | Full compat |
| TanStack Query | Full compat |
| Zustand | Full compat |
| React Hook Form | Mostly works |
| Radix UI | Some issues |
| Framer Motion | Known issues |
render() from preact, never ReactDOM.createRoot().@preact/signals) over useState for shared state.preact/hooks for lifecycle effects.createContext when signals provide simpler reactivity.No artifact produced. Next skill: preact-ssr (if SSR needed) or frontend-testing. Carry forward: signal-based reactivity, hooks conventions, tiny-bundle mindset.
import { signal, computed, effect, Signal } from '@preact/signals';
// Reactive state
const count = signal(0);
const name = signal('world');
// Computed values (auto-derived)
const greeting = computed(() => `Hello, ${name.value}!`);
const doubled = computed(() => count.value * 2);
// Effects (reactions to state changes)
effect(() => {
console.log(`Count changed to: ${count.value}`);
});
// In a component
function Counter() {
return (
<div>
<p>{greeting.value}</p>
<p>Count: {count.value} (doubled: {doubled.value})</p>
<button onClick={() => count.value++}>+</button>
<button onClick={() => count.value--}>-</button>
</div>
);
}
import { h, Fragment, ComponentChildren } from 'preact';
import { useRef, useCallback, useEffect } from 'preact/hooks';
// Use memo for expensive renders
import { memo } from 'preact/compat';
interface ListItemProps {
id: string;
label: string;
onSelect: (id: string) => void;
}
const ListItem = memo(({ id, label, onSelect }: ListItemProps) => {
const handleClick = useCallback(() => onSelect(id), [id, onSelect]);
return <li onClick={handleClick}>{label}</li>;
});
// Portal integration
import { createPortal } from 'preact/compat';
function Modal({ children }: { children: ComponentChildren }) {
const container = useRef(document.getElementById('modal-root')!);
return createPortal(<div class="modal-overlay">{children}</div>, container.current);
}
// Suspense-compatible lazy loading
import { lazy, Suspense } from 'preact/compat';
const HeavyComponent = lazy(() => import('./heavy-component'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
| Using compat for everything | Bundle grows to React size | Use Preact APIs directly, compat for exceptions |
| Signals everywhere | Overkill for simple local state | useState for component-local, signals for shared |
| Not aliasing in bundler | Importing react instead of preact | Configure resolve.alias in bundler |
| Class components with compat | Unnecessary weight | Functional components + hooks |
| Mixing preact/compat and preact/hooks | Hook compatibility issues | Consistently use one or the other |
react imports to preact/compat. Enables using React ecosystem with Preact's small bundle size.| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% | Critical | Rollback or fix |
| p95 latency | > 500ms | Warning | Profile and optimize |
| Uptime | < 99.9% | Critical | Investigate infrastructure |
| Memory usage | > 80% | Warning | Check for leaks |
| CPU usage | > 80% | Warning | Scale up or optimize |
dangerouslySetInnerHTML. Always sanitize HTML content through DOMPurify before injecting. Never set __html from untrusted sources.innerHTML bypass JSX escaping. Always use {signal.value} in JSX, never construct raw HTML strings.preact/compat may expose React patterns like createElement with dangerouslySetInnerHTML. Audit compat-using components for injection vectors. Use PropTypes or TypeScript runtime checks for user input.preact-shadow-root or custom elements. Isolate state from host page globals.{value}. For rich text rendering, use a dedicated component with DOMPurify integration and never bypass JSX escaping with innerHTML.preact/compat pulls in more code. Audit compat dependencies for known vulnerabilities. Keep Preact and compat versions in sync to avoid security patch gaps.