Free SKILL.md scraped from GitHub. Clone the repo or copy the file directly into your Claude Code skills directory.
npx versuz@latest install yandy-r-claude-plugins-ycc-skills-frontend-patternsgit clone https://github.com/yandy-r/claude-plugins.gitcp claude-plugins/SKILL.MD ~/.claude/skills/yandy-r-claude-plugins-ycc-skills-frontend-patterns/SKILL.md---
name: frontend-patterns
description: Frontend patterns for React and Next.js — composition, compound components, render props, custom hooks, state management with Context+useReducer, data fetching, performance optimization (React.memo/useMemo/useCallback, lazy/Suspense, virtualization), forms with validation, error boundaries, animation with Framer Motion, and accessibility patterns (keyboard nav, focus management). Use when the user is building React components, asks about useState/useReducer/Context, asks about useMemo/useCallback/React.memo, asks about code splitting with React.lazy and Suspense, asks about long-list virtualization with @tanstack/react-virtual, asks about form validation, error boundaries, custom hooks, Framer Motion animations, keyboard navigation/focus management, or wants modern React/Next.js idioms.
---
# Frontend Development Patterns
Modern frontend patterns for React, Next.js, and performant user interfaces.
## When to Activate
- Building React components (composition, props, rendering)
- Managing state (`useState`, `useReducer`, Zustand, Context)
- Implementing data fetching (SWR, React Query, server components)
- Optimizing performance (memoization, virtualization, code splitting)
- Working with forms (validation, controlled inputs, Zod schemas)
- Handling client-side routing and navigation
- Building accessible, responsive UI patterns
## Component Patterns
### Composition Over Inheritance
```tsx
// Good: Component composition
interface CardProps {
children: React.ReactNode;
variant?: 'default' | 'outlined';
}
export function Card({ children, variant = 'default' }: CardProps) {
return <div className={`card card-${variant}`}>{children}</div>;
}
export function CardHeader({ children }: { children: React.ReactNode }) {
return <div className="card-header">{children}</div>;
}
export function CardBody({ children }: { children: React.ReactNode }) {
return <div className="card-body">{children}</div>;
}
// Usage
<Card>
<CardHeader>Title</CardHeader>
<CardBody>Content</CardBody>
</Card>;
```
### Compound Components
```tsx
interface TabsContextValue {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabsContext = createContext<TabsContextValue | undefined>(undefined);
export function Tabs({ children, defaultTab }: { children: React.ReactNode; defaultTab: string }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return <TabsContext.Provider value={{ activeTab, setActiveTab }}>{children}</TabsContext.Provider>;
}
export function TabList({ children }: { children: React.ReactNode }) {
return <div className="tab-list">{children}</div>;
}
export function Tab({ id, children }: { id: string; children: React.ReactNode }) {
const context = useContext(TabsContext);
if (!context) throw new Error('Tab must be used within Tabs');
return (
<button className={context.activeTab === id ? 'active' : ''} onClick={() => context.setActiveTab(id)}>
{children}
</button>
);
}
// Usage
<Tabs defaultTab="overview">
<TabList>
<Tab id="overview">Overview</Tab>
<Tab id="details">Details</Tab>
</TabList>
</Tabs>;
```
### Render Props Pattern
```tsx
interface DataLoaderProps<T> {
url: string;
children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode;
}
export function DataLoader<T>({ url, children }: DataLoaderProps<T>) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return <>{children(data, loading, error)}</>;
}
// Usage
<DataLoader<User[]> url="/api/users">
{(users, loading, error) => {
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <UserList users={users!} />;
}}
</DataLoader>;
```
## Custom Hooks Patterns
### State Management Hook
```tsx
export function useToggle(initialValue = false): [boolean, () => void] {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue((v) => !v);
}, []);
return [value, toggle];
}
// Usage
const [isOpen, toggleOpen] = useToggle();
```
### Async Data Fetching Hook
```tsx
interface UseQueryOptions<T> {
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
enabled?: boolean;
}
export function useQuery<T>(key: string, fetcher: () => Promise<T>, options?: UseQueryOptions<T>) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);
const refetch = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await fetcher();
setData(result);
options?.onSuccess?.(result);
} catch (err) {
const error = err as Error;
setError(error);
options?.onError?.(error);
} finally {
setLoading(false);
}
}, [fetcher, options]);
useEffect(() => {
if (options?.enabled !== false) {
refetch();
}
}, [key, refetch, options?.enabled]);
return { data, error, loading, refetch };
}
// Usage
const {
data: users,
loading,
error,
refetch,
} = useQuery('users', () => fetch('/api/users').then((r) => r.json()), {
onSuccess: (data) => console.log('Fetched', data.length, 'users'),
onError: (err) => console.error('Failed:', err),
});
```
> For production data fetching, prefer **TanStack Query (React Query)** or **SWR** instead of rolling your own. The hook above is shown for understanding the pattern.
### Debounce Hook
```tsx
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// Usage
const [searchQuery, setSearchQuery] = useState('');
const debouncedQuery = useDebounce(searchQuery, 500);
useEffect(() => {
if (debouncedQuery) {
performSearch(debouncedQuery);
}
}, [debouncedQuery]);
```
## State Management Patterns
### Context + Reducer Pattern
```tsx
interface State {
items: Item[];
selectedItem: Item | null;
loading: boolean;
}
type Action =
| { type: 'SET_ITEMS'; payload: Item[] }
| { type: 'SELECT_ITEM'; payload: Item }
| { type: 'SET_LOADING'; payload: boolean };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'SET_ITEMS':
return { ...state, items: action.payload };
case 'SELECT_ITEM':
return { ...state, selectedItem: action.payload };
case 'SET_LOADING':
return { ...state, loading: action.payload };
default:
return state;
}
}
const ItemContext = createContext<
| {
state: State;
dispatch: Dispatch<Action>;
}
| undefined
>(undefined);
export function ItemProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, {
items: [],
selectedItem: null,
loading: false,
});
return <ItemContext.Provider value={{ state, dispatch }}>{children}</ItemContext.Provider>;
}
export function useItems() {
const context = useContext(ItemContext);
if (!context) throw new Error('useItems must be used within ItemProvider');
return context;
}
```
### When to Reach for Zustand / Redux Toolkit
- **`useState`** — local component state
- **`useReducer` + `useContext`** — small/medium app state, well-typed actions
- **Zustand** — medium app state, less boilerplate, simple store API
- **Redux Toolkit** — large app state, time-travel debugging, RTK Query needed
- **TanStack Query / SWR** — server state (cache, revalidation, mutations)
Don't reach for global state for everything. Server data belongs in a query cache, not Redux.
## Performance Optimization
### Memoization
```tsx
// useMemo for expensive computations
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => b.priority - a.priority);
}, [items]);
// useCallback for functions passed to memoized children
const handleSearch = useCallback((query: string) => {
setSearchQuery(query);
}, []);
// React.memo for pure components that re-render too often
export const ItemCard = React.memo<ItemCardProps>(({ item }) => {
return (
<div className="item-card">
<h3>{item.name}</h3>
<p>{item.description}</p>
</div>
);
});
```
> Don't memoize everything. Memoization has a cost — only apply it after profiling shows a real bottleneck or when passing callbacks/objects to `React.memo`'d children.
### Code Splitting & Lazy Loading
```tsx
import { lazy, Suspense } from 'react';
// Lazy load heavy components
const HeavyChart = lazy(() => import('./HeavyChart'));
const ThreeJsBackground = lazy(() => import('./ThreeJsBackground'));
export function Dashboard() {
return (
<div>
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart data={data} />
</Suspense>
<Suspense fallback={null}>
<ThreeJsBackground />
</Suspense>
</div>
);
}
```
### Virtualization for Long Lists
```tsx
import { useVirtualizer } from '@tanstack/react-virtual';
export function VirtualItemList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 100, // Estimated row height
overscan: 5, // Extra items to render
});
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.index}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
<ItemCard item={items[virtualRow.index]} />
</div>
))}
</div>
</div>
);
}
```
## Form Handling Patterns
### Controlled Form with Validation
```tsx
interface FormData {
name: string;
description: string;
endDate: string;
}
interface FormErrors {
name?: string;
description?: string;
endDate?: string;
}
export function CreateItemForm() {
const [formData, setFormData] = useState<FormData>({
name: '',
description: '',
endDate: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const validate = (): boolean => {
const newErrors: FormErrors = {};
if (!formData.name.trim()) {
newErrors.name = 'Name is required';
} else if (formData.name.length > 200) {
newErrors.name = 'Name must be under 200 characters';
}
if (!formData.description.trim()) {
newErrors.description = 'Description is required';
}
if (!formData.endDate) {
newErrors.endDate = 'End date is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validate()) return;
try {
await createItem(formData);
// Success handling
} catch (error) {
// Error handling
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={formData.name}
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
placeholder="Item name"
/>
{errors.name && <span className="error">{errors.name}</span>}
{/* Other fields */}
<button type="submit">Create</button>
</form>
);
}
```
> For real forms, prefer **react-hook-form + Zod** (or **TanStack Form**). The hand-rolled version above is shown for understanding the pattern.
### react-hook-form + Zod Sketch
```tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(1, 'Name is required').max(200),
description: z.string().min(1, 'Description is required'),
endDate: z.string().min(1, 'End date is required'),
});
type FormData = z.infer<typeof schema>;
export function CreateItemForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(schema),
});
const onSubmit = async (data: FormData) => {
await createItem(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} placeholder="Item name" />
{errors.name && <span className="error">{errors.name.message}</span>}
{/* Other fields */}
<button type="submit">Create</button>
</form>
);
}
```
## Error Boundary Pattern
```tsx
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback?: React.ReactNode },
ErrorBoundaryState
> {
state: ErrorBoundaryState = {
hasError: false,
error: null,
};
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error boundary caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
if (this.props.fallback) return this.props.fallback;
return (
<div className="error-fallback">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>Try again</button>
</div>
);
}
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<App />
</ErrorBoundary>;
```
> In Next.js App Router, use `error.tsx` files instead of class boundaries for route-level errors. Use the class pattern only for non-route boundaries or when you need granular control.
## Animation Patterns
### Framer Motion: List Animations
```tsx
import { motion, AnimatePresence } from 'framer-motion';
export function AnimatedItemList({ items }: { items: Item[] }) {
return (
<AnimatePresence>
{items.map((item) => (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
<ItemCard item={item} />
</motion.div>
))}
</AnimatePresence>
);
}
```
### Framer Motion: Modal Animations
```tsx
export function Modal({ isOpen, onClose, children }: ModalProps) {
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
className="modal-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
<motion.div
className="modal-content"
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
>
{children}
</motion.div>
</>
)}
</AnimatePresence>
);
}
```
## Accessibility Patterns
### Keyboard Navigation
```tsx
export function Dropdown({ options, onSelect }: DropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);
const handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setActiveIndex((i) => Math.min(i + 1, options.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setActiveIndex((i) => Math.max(i - 1, 0));
break;
case 'Enter':
e.preventDefault();
onSelect(options[activeIndex]);
setIsOpen(false);
break;
case 'Escape':
setIsOpen(false);
break;
}
};
return (
<div role="combobox" aria-expanded={isOpen} aria-haspopup="listbox" onKeyDown={handleKeyDown}>
{/* Dropdown implementation */}
</div>
);
}
```
### Focus Management
```tsx
export function Modal({ isOpen, onClose, children }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (isOpen) {
// Save currently focused element
previousFocusRef.current = document.activeElement as HTMLElement;
// Focus modal
modalRef.current?.focus();
} else {
// Restore focus when closing
previousFocusRef.current?.focus();
}
}, [isOpen]);
return isOpen ? (
<div
ref={modalRef}
role="dialog"
aria-modal="true"
tabIndex={-1}
onKeyDown={(e) => e.key === 'Escape' && onClose()}
>
{children}
</div>
) : null;
}
```
> For modals/dialogs in production, prefer **Radix UI** or **Headless UI** primitives — they handle focus trap, scroll lock, ARIA, and keyboard semantics correctly. Roll your own only when you need full control.
## Quick Reference
| Pattern | Use When |
| ---------------------------------------- | -------------------------------------------------- |
| Composition / compound components | Building flexible, slot-based component APIs |
| Render props / hooks | Sharing logic across unrelated components |
| `useMemo` / `useCallback` / `React.memo` | Profiled bottleneck — never speculatively |
| `React.lazy` + `Suspense` | Splitting heavy/rarely-used routes or components |
| `@tanstack/react-virtual` | Lists over a few hundred rows |
| `useReducer` + Context | Small/medium app state with explicit actions |
| Zustand / Redux Toolkit | Larger app state or DevTools need |
| TanStack Query / SWR | Server state, caching, revalidation |
| react-hook-form + Zod | Forms with validation and type-safe schemas |
| Radix / Headless UI | Accessible primitives for dialogs, popovers, menus |
**Remember**: Modern frontend patterns enable maintainable, performant user interfaces. Choose patterns that fit your project complexity — and reach for proven libraries before hand-rolling primitives.