React 18+ development with hooks, state management, component patterns, and Next.js integration. Use when building React applications or working with JSX/TSX components.
From ensemble-developmentnpx claudepluginhub fortiumpartners/ensemble --plugin ensemble-developmentThis skill uses the workspace's default tool permissions.
PATTERNS-EXTRACTED.mdREADME.mdREFERENCE.mdVALIDATION.mdexamples/README.mdexamples/component-patterns.example.tsxexamples/state-management.example.tsxtemplates/README.mdtemplates/component.template.tsxtemplates/component.test.template.tsxtemplates/context.template.tsxtemplates/hook.template.tsSearches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides agent creation for Claude Code plugins with file templates, frontmatter specs (name, description, model), triggering examples, system prompts, and best practices.
Version: 1.0.0 | Framework: React 18+ | Use Case: Fast lookups during active development
Load this skill when:
package.json contains "react" dependency (>=18.0.0).jsx or .tsx files in src/Minimum Detection Confidence: 0.8 (80%)
import { FC, useState } from 'react';
interface Props {
title: string;
onAction?: () => void;
}
export const MyComponent: FC<Props> = ({ title, onAction }) => {
const [count, setCount] = useState(0);
return (
<div>
<h1>{title}</h1>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<button onClick={onAction}>Action</button>
</div>
);
};
// 1. Imports (grouped and sorted)
import { useState, useEffect } from 'react';
import type { FC, ReactNode } from 'react';
// 2. Types/Interfaces
interface Props {
children: ReactNode;
className?: string;
}
// 3. Component
export const Component: FC<Props> = ({ children, className }) => {
// 4. Hooks (state, effects, context)
const [state, setState] = useState<string>('');
useEffect(() => {
// Side effects
}, []);
// 5. Event handlers
const handleClick = () => setState('clicked');
// 6. Early returns (error states, loading)
if (!children) return null;
// 7. Main render
return <div className={className}>{children}</div>;
};
// Container: Logic and data fetching
export const UserProfileContainer: FC<{ userId: number }> = ({ userId }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser)
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <LoadingSpinner />;
if (!user) return <ErrorMessage />;
return <UserProfile user={user} />;
};
// Presentational: Pure UI
export const UserProfile: FC<{ user: User }> = ({ user }) => (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
// Basic usage
const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
// Functional updates (when new state depends on old)
setCount(prevCount => prevCount + 1);
// Lazy initialization (expensive computation)
const [data, setData] = useState(() => expensiveComputation());
// Object state updates (always spread)
setForm(prev => ({ ...prev, email: 'new@email.com' }));
// Run once on mount
useEffect(() => {
fetchData();
}, []);
// Run when dependencies change
useEffect(() => {
fetchUser(userId);
}, [userId]);
// Cleanup function
useEffect(() => {
const subscription = api.subscribe();
return () => subscription.unsubscribe();
}, []);
// Abort fetch on unmount
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal });
return () => controller.abort();
}, [url]);
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// Provider
export const ThemeProvider: FC<{ children: ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
// Custom hook (always validate context exists)
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
return context;
};
type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' };
const reducer = (state: number, action: Action): number => {
switch (action.type) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
case 'reset': return 0;
default: return state;
}
};
const Counter = () => {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
};
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// Usage
const { data: user, loading, error } = useFetch<User>(`/api/users/${userId}`);
// Memoize component - only re-renders when props change
export const ExpensiveComponent = memo(({ data }: { data: Data }) => {
return <div>{/* expensive rendering */}</div>;
});
// With custom comparison
export const CustomMemo = memo(
({ user }: { user: User }) => <div>{user.name}</div>,
(prev, next) => prev.user.id === next.user.id
);
// useMemo: Memoize expensive computations
const filteredItems = useMemo(() => {
return items.filter(item => item.includes(filter));
}, [items, filter]);
// useCallback: Memoize function references
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
// Pass to memoized children to prevent re-renders
<MemoizedChild onClick={handleClick} />
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const App = () => (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
// Use semantic HTML
<button onClick={handleClick}>Click me</button> // Good
<div onClick={handleClick}>Click me</div> // Bad
// ARIA labels for screen readers
<button aria-label="Close dialog">X</button>
// ARIA descriptions
<input type="email" aria-describedby="email-hint" />
<span id="email-hint">We'll never share your email</span>
// Live regions for dynamic content
<div aria-live="polite" aria-atomic="true">{statusMessage}</div>
// Keyboard navigation
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
if (e.key === 'Enter') onSubmit();
};
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && <span id="email-error" role="alert">{errors.email}</span>}
</form>
interface Props {
title: string; // Required
subtitle?: string; // Optional
variant: 'primary' | 'secondary'; // Union types
onClick?: () => void; // Optional function
onSubmit: (data: FormData) => void; // Required function
children: ReactNode; // Children
user: User; // Complex type
}
export const Component: FC<Props> = ({ title, onClick }) => (
<div onClick={onClick}>{title}</div>
);
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => { e.preventDefault(); };
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {};
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => { inputRef.current?.focus(); }, []);
<input ref={inputRef} />
// DON'T: Mutate state directly
user.name = 'Jane'; setUser(user); // Bad
setUser({ ...user, name: 'Jane' }); // Good
// DON'T: Use index as key
{items.map((item, i) => <div key={i}>{item}</div>)} // Bad
{items.map(item => <div key={item.id}>{item}</div>)} // Good
// DON'T: Call hooks conditionally
if (condition) { useState(0); } // Bad - hooks must be top-level
// DON'T: Miss useEffect dependencies
useEffect(() => { fetchUser(userId); }, []); // Bad
useEffect(() => { fetchUser(userId); }, [userId]); // Good
// DON'T: Inline functions in JSX (when performance matters)
<Button onClick={() => handleClick(id)}>Click</Button> // Bad
<Button onClick={handleClickWithId}>Click</Button> // Good (with useCallback)
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
describe('Button', () => {
it('renders with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
it('handles click events', async () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click</Button>);
await userEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('has no accessibility violations', async () => {
const { container } = render(<Button>Click</Button>);
expect(await axe(container)).toHaveNoViolations();
});
});
When using this skill, ensure:
Version: 1.0.0 | Last Updated: 2025-01-01 | Status: Production Ready