Builds fast, lightweight React-compatible applications with Preact's 3KB library and optional Signals for fine-grained reactivity. Use when optimizing bundle size, migrating from React, or when user mentions Preact, preact/compat, or lightweight React alternative.
Provides Preact knowledge for building lightweight React-compatible apps. Triggers when users mention Preact, preact/compat, or need 3KB React alternative for bundle optimization.
/plugin marketplace add mgd34msu/goodvibes-plugin/plugin install goodvibes@goodvibes-marketThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Fast 3KB React alternative with the same modern API and optional Signals for fine-grained reactivity.
# Create new project
npm create preact my-app
# Or with Vite
npm create vite@latest my-app -- --template preact-ts
cd my-app
npm install
npm run dev
import { h } from 'preact';
// Basic component
function Greeting({ name }: { name: string }) {
return <h1>Hello, {name}!</h1>;
}
// With default export
export default function App() {
return (
<div>
<Greeting name="World" />
</div>
);
}
import { Component, h } from 'preact';
interface CounterState {
count: number;
}
interface CounterProps {
initialCount?: number;
}
class Counter extends Component<CounterProps, CounterState> {
state = { count: this.props.initialCount ?? 0 };
increment = () => {
this.setState(prev => ({ count: prev.count + 1 }));
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
import { useState } from 'preact/hooks';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}
import { useState, useEffect } from 'preact/hooks';
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (!cancelled) {
setUser(data);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchUser();
// Cleanup function
return () => {
cancelled = true;
};
}, [userId]); // Re-run when userId changes
if (loading) return <p>Loading...</p>;
if (!user) return <p>User not found</p>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
import { useRef } from 'preact/hooks';
function TextInput() {
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
inputRef.current?.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus</button>
</div>
);
}
import { useMemo, useCallback } from 'preact/hooks';
function ExpensiveList({ items, filter }: { items: Item[]; filter: string }) {
// Memoize expensive computation
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
// Memoize callback
const handleItemClick = useCallback((id: string) => {
console.log('Clicked:', id);
}, []);
return (
<ul>
{filteredItems.map(item => (
<li key={item.id} onClick={() => handleItemClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
import { createContext } from 'preact';
import { useContext } from 'preact/hooks';
interface Theme {
primary: string;
secondary: string;
}
const ThemeContext = createContext<Theme>({
primary: '#007bff',
secondary: '#6c757d',
});
function ThemeProvider({ children }) {
const theme = {
primary: '#6366f1',
secondary: '#8b5cf6',
};
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ backgroundColor: theme.primary, color: 'white' }}>
Themed Button
</button>
);
}
import { useReducer } from 'preact/hooks';
interface State {
count: number;
step: number;
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'setStep'; payload: number }
| { type: 'reset' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step };
case 'decrement':
return { ...state, count: state.count - state.step };
case 'setStep':
return { ...state, step: action.payload };
case 'reset':
return { count: 0, step: 1 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
return (
<div>
<p>Count: {state.count}</p>
<input
type="number"
value={state.step}
onChange={e => dispatch({ type: 'setStep', payload: +e.currentTarget.value })}
/>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}
Preact Signals provide fine-grained reactivity without re-rendering entire component trees.
import { signal, computed } from '@preact/signals';
// Create signals (outside component = global state)
const count = signal(0);
const doubled = computed(() => count.value * 2);
function Counter() {
// Signals auto-subscribe - only affected parts re-render
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => count.value++}>Increment</button>
</div>
);
}
import { useSignal, useComputed } from '@preact/signals';
function Counter() {
// Component-scoped signals
const count = useSignal(0);
const doubled = useComputed(() => count.value * 2);
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => count.value++}>Increment</button>
</div>
);
}
import { signal, effect, batch } from '@preact/signals';
const name = signal('');
const email = signal('');
// Runs whenever dependencies change
effect(() => {
console.log(`User: ${name.value} <${email.value}>`);
});
// Batch multiple updates
function updateUser(newName: string, newEmail: string) {
batch(() => {
name.value = newName;
email.value = newEmail;
});
}
import { signal, computed } from '@preact/signals';
interface Todo {
id: number;
text: string;
completed: boolean;
}
// State
const todos = signal<Todo[]>([]);
const filter = signal<'all' | 'active' | 'completed'>('all');
// Derived state
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(t => !t.completed);
case 'completed':
return todos.value.filter(t => t.completed);
default:
return todos.value;
}
});
const activeCount = computed(() =>
todos.value.filter(t => !t.completed).length
);
// Actions
function addTodo(text: string) {
todos.value = [
...todos.value,
{ id: Date.now(), text, completed: false }
];
}
function toggleTodo(id: number) {
todos.value = todos.value.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
);
}
function removeTodo(id: number) {
todos.value = todos.value.filter(t => t.id !== id);
}
// Component
function TodoApp() {
return (
<div>
<h1>Todos ({activeCount} active)</h1>
<div>
<button onClick={() => filter.value = 'all'}>All</button>
<button onClick={() => filter.value = 'active'}>Active</button>
<button onClick={() => filter.value = 'completed'}>Completed</button>
</div>
<ul>
{filteredTodos.value.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
Drop-in React replacement for using React libraries with Preact.
// vite.config.js
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';
export default defineConfig({
plugins: [preact()],
resolve: {
alias: {
'react': 'preact/compat',
'react-dom': 'preact/compat',
'react-dom/test-utils': 'preact/test-utils',
'react/jsx-runtime': 'preact/jsx-runtime',
},
},
});
// webpack.config.js
module.exports = {
resolve: {
alias: {
'react': 'preact/compat',
'react-dom': 'preact/compat',
'react-dom/test-utils': 'preact/test-utils',
'react/jsx-runtime': 'preact/jsx-runtime',
},
},
};
// React libraries work with preact/compat
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
import { motion } from 'framer-motion';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<UserList />
</motion.div>
</QueryClientProvider>
);
}
function UserList() {
const { data, isLoading } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
});
if (isLoading) return <p>Loading...</p>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
import { render } from 'preact';
import App from './App';
render(<App />, document.getElementById('app')!);
import { hydrate } from 'preact';
import App from './App';
hydrate(<App />, document.getElementById('app')!);
import { render } from 'preact-render-to-string';
import App from './App';
const html = render(<App />);
// Full page
const page = `
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="app">${html}</div>
<script src="/bundle.js"></script>
</body>
</html>
`;
import { memo } from 'preact/compat';
// Only re-renders when props change (shallow comparison)
const MemoizedItem = memo(function Item({ item }) {
return <li>{item.name}</li>;
});
// Custom comparison
const MemoizedUser = memo(
function User({ user }) {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => prevProps.user.id === nextProps.user.id
);
import { lazy, Suspense } from 'preact/compat';
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Router>
<Dashboard path="/dashboard" />
<Settings path="/settings" />
</Router>
</Suspense>
);
}
import { Component } from 'preact';
class ErrorBoundary extends Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div>
<h1>Something went wrong</h1>
<pre>{this.state.error?.message}</pre>
<button onClick={() => this.setState({ hasError: false })}>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
import { Router, Route, Link } from 'preact-router';
function App() {
return (
<div>
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/users/123">User 123</Link>
</nav>
<Router>
<Home path="/" />
<About path="/about" />
<User path="/users/:id" />
<NotFound default />
</Router>
</div>
);
}
function Home() {
return <h1>Home</h1>;
}
function About() {
return <h1>About</h1>;
}
function User({ id }: { id: string }) {
return <h1>User: {id}</h1>;
}
function NotFound() {
return <h1>404 - Not Found</h1>;
}
// Using @testing-library/preact
import { render, fireEvent, screen } from '@testing-library/preact';
import Counter from './Counter';
test('increments counter', async () => {
render(<Counter />);
const button = screen.getByRole('button', { name: /increment/i });
const count = screen.getByText(/count:/i);
expect(count).toHaveTextContent('Count: 0');
await fireEvent.click(button);
expect(count).toHaveTextContent('Count: 1');
});
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.