
React
- 50 installs
- 101 repo stars
- Updated November 28, 2025
- blencorp/claude-code-kit
React is a Claude Code skill that provides core React 19 patterns for hooks, Suspense, lazy loading, and TypeScript component structure.
About
React is a skill that gives Claude core React 19 patterns for hooks, Suspense, lazy loading, component structure, and TypeScript best practices. A developer uses it when creating React components, using hooks, implementing code splitting, or working with Suspense boundaries. It documents React 19 breaking changes such as ref-as-prop replacing forwardRef.
- React 19 hooks, component-structure, and TypeScript patterns
- Lazy loading and code splitting with React.lazy and Suspense
- Documents React 19 changes: ref as a prop, no forwardRef, no React.FC
React by the numbers
- 50 all-time installs (skills.sh)
- Ranked #1,306 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
react capabilities & compatibility
- Capabilities
- frontend · ui design · component patterns
- Use cases
- frontend · ui design
- IDEs
- vscode · cursor ide
What react says it does
Core React 19 patterns including hooks, Suspense, lazy loading, component structure, TypeScript best practices, and performance optimization.
Essential React 19 patterns for building modern applications with hooks, Suspense, lazy loading, and TypeScript.
`forwardRef` no longer needed - pass `ref` as a prop directly
npx skills add https://github.com/blencorp/claude-code-kit --skill reactAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 50 |
|---|---|
| repo stars | ★ 101 |
| Last updated | November 28, 2025 |
| Repository | blencorp/claude-code-kit ↗ |
What it does
Build React 19 components with hooks, Suspense, lazy loading, and typed props.
Who is it for?
Writing React 19 components and hooks with TypeScript
Skip if: Non-React frameworks or backend work
When should I use this skill?
Working with React components, hooks, lazy loading, or Suspense boundaries
What you get
Components follow React 19 conventions with correct hooks, memoization, and Suspense usage.
- React components
- Custom hooks
- Lazy-loaded modules
By the numbers
- 9-item React component checklist
Files
React Core Patterns
Purpose
Essential React 19 patterns for building modern applications with hooks, Suspense, lazy loading, and TypeScript.
Note: React 19 (released December 2024) breaking changes:
forwardRefno longer needed - passrefas a prop directlypropTypesremoved (silently ignored)- New JSX transform required
React.FCtype discouraged - use direct function components instead
When to Use This Skill
- Creating React components
- Using React hooks (useState, useEffect, useCallback, useMemo)
- Implementing lazy loading and code splitting
- Working with Suspense boundaries
- React-specific TypeScript patterns
- Performance optimization with React
---
Quick Start
Component Structure Template
import { useState, useCallback } from 'react';
interface Props {
userId: string;
onUpdate?: (data: UserData) => void;
}
interface UserData {
name: string;
email: string;
}
function UserProfile({ userId, onUpdate }: Props) {
const [data, setData] = useState<UserData | null>(null);
const handleUpdate = useCallback((newData: UserData) => {
setData(newData);
onUpdate?.(newData);
}, [onUpdate]);
return (
<div>
{/* Component content */}
</div>
);
}
export default UserProfile;Component Checklist
Creating a React component? Follow this:
- [ ] Use function components with typed props (not
React.FC) - [ ] Define interfaces for Props and local state
- [ ] Use
useCallbackfor event handlers passed to children - [ ] Use
useMemofor expensive computations - [ ] Lazy load if heavy component:
lazy(() => import()) - [ ] Wrap lazy components in
<Suspense>with fallback - [ ] Default export at bottom
- [ ] No conditional hooks (hooks must be called in same order)
- [ ] Pass
refas a prop (noforwardRefneeded in React 19)
---
Core Hooks Patterns
useState
// Simple state
const [count, setCount] = useState<number>(0);
// Object state
const [user, setUser] = useState<User | null>(null);
// Array state
const [items, setItems] = useState<Item[]>([]);
// Functional updates when depending on previous state
setCount(prev => prev + 1);
setItems(prev => [...prev, newItem]);useCallback
// Wrap functions passed to child components
const handleClick = useCallback((id: string) => {
console.log('Clicked:', id);
}, []); // Empty deps if no dependencies
// With dependencies
const handleUpdate = useCallback((data: FormData) => {
apiCall(userId, data);
}, [userId]); // Re-create when userId changesuseMemo
// Expensive computation
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.score - b.score);
}, [items]);
// Derived state
const totalPrice = useMemo(() => {
return cart.reduce((sum, item) => sum + item.price, 0);
}, [cart]);useEffect
// Run once on mount
useEffect(() => {
fetchData();
}, []);
// Run when dependency changes
useEffect(() => {
if (userId) {
loadUserData(userId);
}
}, [userId]);
// Cleanup
useEffect(() => {
const subscription = subscribe(userId);
return () => subscription.unsubscribe();
}, [userId]);---
Lazy Loading & Code Splitting
Basic Lazy Loading
import React, { Suspense } from 'react';
// Lazy load heavy component
const HeavyChart = React.lazy(() => import('./HeavyChart'));
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading chart...</div>}>
<HeavyChart />
</Suspense>
</div>
);
}Multiple Lazy Components
const AdminPanel = React.lazy(() => import('./AdminPanel'));
const UserSettings = React.lazy(() => import('./UserSettings'));
const Reports = React.lazy(() => import('./Reports'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/admin" element={<AdminPanel />} />
<Route path="/settings" element={<UserSettings />} />
<Route path="/reports" element={<Reports />} />
</Routes>
</Suspense>
);
}Feature-Based Code Splitting
// features/auth/index.tsx
export { default } from './AuthFeature';
// Lazy load entire feature
const AuthFeature = React.lazy(() => import('~/features/auth'));
<Suspense fallback={<FeatureLoader />}>
<AuthFeature />
</Suspense>---
Suspense Patterns
Suspense Boundaries
// Wrap data-fetching components
<Suspense fallback={<Skeleton />}>
<UserProfile userId={id} />
</Suspense>
// Nested Suspense for granular loading
<Suspense fallback={<PageLoader />}>
<Header />
<Suspense fallback={<ContentSkeleton />}>
<MainContent />
</Suspense>
<Footer />
</Suspense>Error Boundaries with Suspense
import { ErrorBoundary } from 'react-error-boundary';
<ErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
</ErrorBoundary>---
TypeScript Patterns
Component Props
// Basic props
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
}
// Props with children
interface CardProps {
title: string;
children: React.ReactNode;
}
// Props with specific child types
interface ListProps {
children: React.ReactElement<ItemProps> | React.ReactElement<ItemProps>[];
}
// Props with event handlers
interface FormProps {
onSubmit: (data: FormData) => void;
onChange?: (field: string, value: unknown) => void;
}Hooks TypeScript
// useState with type
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<Item[]>([]);
// useRef with type
const inputRef = useRef<HTMLInputElement>(null);
const timerRef = useRef<number | null>(null);
// Custom hook with return type
function useUser(id: string): { user: User | null; loading: boolean } {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
// ... implementation
return { user, loading };
}---
Performance Optimization
React.memo
// Memoize component to prevent unnecessary re-renders
const UserCard = React.memo<UserCardProps>(({ user, onUpdate }) => {
return (
<div>
<h3>{user.name}</h3>
<button onClick={() => onUpdate(user.id)}>Update</button>
</div>
);
});
// Custom comparison function
const UserCard = React.memo(UserCardComponent, (prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id;
});Avoiding Re-renders
// ❌ Bad: Creates new function on every render
function Parent() {
return <Child onClick={() => console.log('clicked')} />;
}
// ✅ Good: Stable function reference
function Parent() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <Child onClick={handleClick} />;
}---
Common Patterns
Conditional Rendering
// Ternary operator
{isLoading ? <Spinner /> : <Content />}
// Logical AND
{error && <ErrorMessage error={error} />}
// Nullish coalescing
{user ?? <GuestView />}
// Early return for loading states
function Component() {
const { data } = useSomeHook();
// ❌ Avoid early returns for loading - breaks hooks rules
// Use Suspense instead
return <div>{data.map(...)}</div>;
}Lists and Keys
// Always use stable keys
{items.map(item => (
<ItemCard key={item.id} item={item} />
))}
// Never use index as key if list can reorder
// ❌ Bad
{items.map((item, index) => (
<ItemCard key={index} item={item} />
))}---
File Organization
Feature-Based Structure
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── types/
│ │ └── index.tsx
│ └── posts/
│ ├── components/
│ ├── hooks/
│ ├── types/
│ └── index.tsx
├── components/ # Shared components
├── hooks/ # Shared hooks
└── types/ # Shared typesComponent Co-location
features/posts/
├── components/
│ ├── PostCard.tsx
│ ├── PostList.tsx
│ └── PostForm.tsx
├── hooks/
│ ├── usePost.ts
│ └── usePosts.ts
├── types/
│ └── post.ts
└── index.tsx # Public API---
Common Mistakes to Avoid
1. Conditional Hooks
// ❌ Never do this
function Component({ condition }) {
if (condition) {
const [state, setState] = useState(0); // Breaks rules of hooks
}
}
// ✅ Do this
function Component({ condition }) {
const [state, setState] = useState(0);
// Use state conditionally, not the hook
}2. Missing Dependencies
// ❌ Bad: Missing dependency
useEffect(() => {
fetchUser(userId);
}, []); // userId should be in deps
// ✅ Good: All dependencies listed
useEffect(() => {
fetchUser(userId);
}, [userId]);3. Mutating State
// ❌ Bad: Mutating state directly
const handleAdd = () => {
items.push(newItem); // Don't mutate
setItems(items);
};
// ✅ Good: Create new array
const handleAdd = () => {
setItems([...items, newItem]);
};---
Additional Resources
For more detailed patterns, see:
- component-patterns.md - Advanced component patterns
- performance.md - Performance optimization techniques
- typescript-patterns.md - TypeScript best practices
- hooks-patterns.md - Custom hooks and advanced patterns
Advanced Component Patterns
Compound Components
Pattern where components work together to form a cohesive UI:
// Parent provides context
export function Tabs({ children, defaultValue }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultValue);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
{children}
</TabsContext.Provider>
);
}
// Children consume context
export function Tab({ value, children }: TabProps) {
const { activeTab, setActiveTab } = useTabsContext();
const isActive = activeTab === value;
return (
<button
onClick={() => setActiveTab(value)}
className={isActive ? 'active' : ''}
>
{children}
</button>
);
}
// Usage
<Tabs defaultValue="profile">
<Tab value="profile">Profile</Tab>
<Tab value="settings">Settings</Tab>
</Tabs>Render Props
Share code between components using a prop whose value is a function:
interface DataFetcherProps<T> {
url: string;
children: (data: T | null, loading: boolean, error: Error | null) => ReactNode;
}
function DataFetcher<T>({ url, children }: DataFetcherProps<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
<DataFetcher<User> url="/api/user">
{(user, loading, error) => {
if (loading) return <Loading />;
if (error) return <Error message={error.message} />;
return <UserProfile user={user} />;
}}
</DataFetcher>Higher-Order Components (HOCs)
Function that takes a component and returns a new component:
function withAuth<P extends object>(
Component: ComponentType<P>
): ComponentType<P> {
return function AuthenticatedComponent(props: P) {
const { user, loading } = useAuth();
if (loading) return <Loading />;
if (!user) return <Navigate to="/login" />;
return <Component {...props} />;
};
}
// Usage
const ProtectedDashboard = withAuth(Dashboard);Custom Hooks Pattern
Extract complex logic into reusable hooks:
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback((value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error('Error saving to localStorage:', error);
}
}, [key, storedValue]);
return [storedValue, setValue] as const;
}
// Usage
const [theme, setTheme] = useLocalStorage('theme', 'light');Controlled vs Uncontrolled Components
Controlled
function ControlledInput() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}Uncontrolled
function UncontrolledInput() {
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = () => {
console.log(inputRef.current?.value);
};
return <input ref={inputRef} />;
}When to use:
- Controlled: Form validation, conditional rendering, dynamic behavior
- Uncontrolled: Simple forms, file inputs, integrating with non-React code
Container/Presentational Pattern
Separate data fetching from presentation:
// Container - handles logic and data
function UserProfileContainer({ userId }: { userId: string }) {
const { data: user, isLoading } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
if (isLoading) return <UserProfileSkeleton />;
return <UserProfilePresentation user={user} />;
}
// Presentation - pure UI component
interface UserProfilePresentationProps {
user: User;
}
function UserProfilePresentation({ user }: UserProfilePresentationProps) {
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<Avatar src={user.avatar} />
</div>
);
}Error Boundaries
Handle errors gracefully in component trees:
class ErrorBoundary extends Component<
{ children: ReactNode; fallback: ComponentType<{ error: Error }> },
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError && this.state.error) {
return <this.props.fallback error={this.state.error} />;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary fallback={ErrorFallback}>
<App />
</ErrorBoundary>Custom Hooks and Advanced Patterns
useToggle
function useToggle(initialValue = false): [boolean, () => void, (value: boolean) => void] {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []);
const set = useCallback((newValue: boolean) => setValue(newValue), []);
return [value, toggle, set];
}
// Usage
const [isOpen, toggleOpen, setOpen] = useToggle(false);usePrevious
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// Usage
const [count, setCount] = useState(0);
const previousCount = usePrevious(count);useDebounce
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// Usage
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearch = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearch) {
searchAPI(debouncedSearch);
}
}, [debouncedSearch]);useEventListener
function useEventListener<K extends keyof WindowEventMap>(
eventName: K,
handler: (event: WindowEventMap[K]) => void,
element: Window | HTMLElement = window
) {
const savedHandler = useRef(handler);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const eventListener = (event: Event) => savedHandler.current(event as WindowEventMap[K]);
element.addEventListener(eventName, eventListener);
return () => element.removeEventListener(eventName, eventListener);
}, [eventName, element]);
}
// Usage
useEventListener('resize', () => console.log('Window resized'));
useEventListener('click', (e) => console.log('Clicked', e.target), buttonRef.current);useMediaQuery
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(() => window.matchMedia(query).matches);
useEffect(() => {
const mediaQuery = window.matchMedia(query);
const handler = (event: MediaQueryListEvent) => setMatches(event.matches);
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, [query]);
return matches;
}
// Usage
const isMobile = useMediaQuery('(max-width: 768px)');
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');useIntersectionObserver
function useIntersectionObserver(
elementRef: RefObject<Element>,
options?: IntersectionObserverInit
): IntersectionObserverEntry | null {
const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
useEffect(() => {
const element = elementRef.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => setEntry(entry),
options
);
observer.observe(element);
return () => observer.disconnect();
}, [elementRef, options]);
return entry;
}
// Usage - Infinite scroll
const loadMoreRef = useRef<HTMLDivElement>(null);
const entry = useIntersectionObserver(loadMoreRef, { threshold: 0.5 });
useEffect(() => {
if (entry?.isIntersecting) {
loadMore();
}
}, [entry]);useAsync
interface AsyncState<T> {
loading: boolean;
error: Error | null;
data: T | null;
}
function useAsync<T>(
asyncFunction: () => Promise<T>,
dependencies: DependencyList = []
): AsyncState<T> {
const [state, setState] = useState<AsyncState<T>>({
loading: true,
error: null,
data: null
});
useEffect(() => {
setState({ loading: true, error: null, data: null });
asyncFunction()
.then(data => setState({ loading: false, error: null, data }))
.catch(error => setState({ loading: false, error, data: null }));
}, dependencies);
return state;
}
// Usage
const { loading, error, data } = useAsync(
() => fetchUser(userId),
[userId]
);useInterval
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
// Usage
const [count, setCount] = useState(0);
const [isRunning, setIsRunning] = useState(true);
useInterval(
() => setCount(count + 1),
isRunning ? 1000 : null
);useClickOutside
function useClickOutside<T extends HTMLElement>(
ref: RefObject<T>,
handler: (event: MouseEvent | TouchEvent) => void
) {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
const el = ref.current;
if (!el || el.contains(event.target as Node)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
}
// Usage - Close dropdown when clicking outside
const dropdownRef = useRef<HTMLDivElement>(null);
useClickOutside(dropdownRef, () => setIsOpen(false));useCopyToClipboard
function useCopyToClipboard(): [
boolean,
(text: string) => Promise<void>
] {
const [copied, setCopied] = useState(false);
const copy = useCallback(async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (error) {
console.error('Failed to copy:', error);
setCopied(false);
}
}, []);
return [copied, copy];
}
// Usage
const [copied, copy] = useCopyToClipboard();
<button onClick={() => copy(codeSnippet)}>
{copied ? 'Copied!' : 'Copy'}
</button>useFormInput
function useFormInput<T extends string | number>(initialValue: T) {
const [value, setValue] = useState(initialValue);
const handleChange = useCallback((
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setValue(e.target.value as T);
}, []);
const reset = useCallback(() => setValue(initialValue), [initialValue]);
return {
value,
onChange: handleChange,
reset
};
}
// Usage
const email = useFormInput('');
const password = useFormInput('');
<input type="email" {...email} />
<input type="password" {...password} />useWindowSize
interface WindowSize {
width: number;
height: number;
}
function useWindowSize(): WindowSize {
const [size, setSize] = useState<WindowSize>({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// Usage
const { width, height } = useWindowSize();
const isMobile = width < 768;useOnlineStatus
function useOnlineStatus(): boolean {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
// Usage
const isOnline = useOnlineStatus();
{!isOnline && <OfflineBanner />}useTimeout
function useTimeout(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setTimeout(() => savedCallback.current(), delay);
return () => clearTimeout(id);
}, [delay]);
}
// Usage - Auto-save draft after 3 seconds of inactivity
useTimeout(() => saveDraft(content), hasUnsavedChanges ? 3000 : null);useFetch
function useFetch<T>(url: string, options?: RequestInit) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const abortController = new AbortController();
setLoading(true);
fetch(url, { ...options, signal: abortController.signal })
.then(res => res.json())
.then(data => {
setData(data);
setError(null);
})
.catch(err => {
if (err.name !== 'AbortError') {
setError(err);
}
})
.finally(() => setLoading(false));
return () => abortController.abort();
}, [url]);
return { data, loading, error };
}
// Usage
const { data, loading, error } = useFetch<User>('/api/user/123');Hook Composition
Combine multiple hooks for complex functionality:
function useAuthenticatedFetch<T>(url: string) {
const { token } = useAuth();
const { data, loading, error } = useFetch<T>(url, {
headers: {
Authorization: `Bearer ${token}`
}
});
return { data, loading, error };
}
// Or build upon simpler hooks
function useForm(initialValues: Record<string, any>) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState<Record<string, string>>({});
const [touched, setTouched] = useState<Record<string, boolean>>({});
const handleChange = useCallback((field: string, value: any) => {
setValues(prev => ({ ...prev, [field]: value }));
}, []);
const handleBlur = useCallback((field: string) => {
setTouched(prev => ({ ...prev, [field]: true }));
}, []);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
}, [initialValues]);
return {
values,
errors,
touched,
handleChange,
handleBlur,
reset
};
}Performance Optimization
React.memo
Prevent unnecessary re-renders of expensive components:
const ExpensiveComponent = React.memo(
({ data }: { data: ComplexData }) => {
// Expensive rendering logic
return <div>{/* complex UI */}</div>;
},
(prevProps, nextProps) => {
// Custom comparison function
return prevProps.data.id === nextProps.data.id;
}
);useMemo for Expensive Calculations
function DataGrid({ items, filters }: Props) {
// Only recalculate when items or filters change
const filteredItems = useMemo(() => {
return items.filter(item =>
Object.entries(filters).every(([key, value]) =>
item[key] === value
)
);
}, [items, filters]);
return <Grid items={filteredItems} />;
}useCallback for Event Handlers
function TodoList({ todos }: { todos: Todo[] }) {
// Stable function reference across re-renders
const handleDelete = useCallback((id: string) => {
deleteTodo(id);
}, [deleteTodo]);
return (
<>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onDelete={handleDelete} // Same reference each render
/>
))}
</>
);
}Code Splitting with React.lazy
// Split code at route level
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
const Profile = lazy(() => import('./Profile'));
function App() {
return (
<Suspense fallback={<LoadingScreen />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
);
}Virtualization for Long Lists
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
overscan: 5
});
return (
<div ref={parentRef} style={{ height: '500px', 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%',
transform: `translateY(${virtualRow.start}px)`
}}
>
<Item data={items[virtualRow.index]} />
</div>
))}
</div>
</div>
);
}Avoid Inline Object/Array Creation
// ❌ Bad: New object created on every render
<Component style={{ margin: 10 }} />
<List items={items.filter(i => i.active)} />
// ✅ Good: Stable references
const style = { margin: 10 };
const activeItems = useMemo(
() => items.filter(i => i.active),
[items]
);
<Component style={style} />
<List items={activeItems} />Debounce Expensive Operations
function SearchBox() {
const [query, setQuery] = useState('');
// Debounce the actual search API call
const debouncedSearch = useMemo(
() => debounce((q: string) => {
searchAPI(q);
}, 300),
[]
);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
debouncedSearch(e.target.value);
};
return <input value={query} onChange={handleChange} />;
}Use Transition API for Non-Urgent Updates
import { useTransition } from 'react';
function TabContainer() {
const [tab, setTab] = useState('posts');
const [isPending, startTransition] = useTransition();
const handleTabChange = (newTab: string) => {
startTransition(() => {
setTab(newTab);
});
};
return (
<>
<TabList onChange={handleTabChange} />
{isPending && <LoadingSpinner />}
<TabContent tab={tab} />
</>
);
}useDeferredValue for Expensive Renders
function SearchResults({ query }: { query: string }) {
// Defer the expensive filtering operation
const deferredQuery = useDeferredValue(query);
const results = useMemo(
() => items.filter(item => item.name.includes(deferredQuery)),
[deferredQuery]
);
return (
<>
{deferredQuery !== query && <LoadingIndicator />}
<ResultsList results={results} />
</>
);
}Optimize Context Usage
// ❌ Bad: Single context for everything
const AppContext = createContext({ user, theme, settings, notifications });
// ✅ Good: Separate contexts to prevent unnecessary re-renders
const UserContext = createContext(user);
const ThemeContext = createContext(theme);
const SettingsContext = createContext(settings);
// Even better: Use context selectors
function useUserName() {
const user = useContext(UserContext);
return user.name; // Only re-render when name changes
}Profile with React DevTools
// Wrap expensive components to measure render time
import { Profiler } from 'react';
function onRenderCallback(
id: string,
phase: 'mount' | 'update',
actualDuration: number
) {
console.log(`${id} ${phase} took ${actualDuration}ms`);
}
<Profiler id="Dashboard" onRender={onRenderCallback}>
<Dashboard />
</Profiler>Key Metrics to Monitor
- Time to Interactive (TTI): When page becomes fully interactive
- First Contentful Paint (FCP): When first content appears
- Largest Contentful Paint (LCP): When main content is visible
- Cumulative Layout Shift (CLS): Visual stability
- Component render time: Use React DevTools Profiler
Performance Checklist
- [ ] Use React.memo for expensive pure components
- [ ] Implement code splitting at route boundaries
- [ ] Virtualize long lists (>100 items)
- [ ] Debounce/throttle input handlers
- [ ] Use useMemo for expensive calculations
- [ ] Use useCallback for event handlers passed to child components
- [ ] Avoid inline object/array creation in JSX
- [ ] Use Suspense for data fetching
- [ ] Optimize bundle size (analyze with webpack-bundle-analyzer)
- [ ] Use production builds in deployment
TypeScript Patterns for React
Component Props with TypeScript
// Basic props interface
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
function Button({ label, onClick, variant = 'primary', disabled = false }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled} className={variant}>
{label}
</button>
);
}Extending HTML Element Props
// Extend native button props
interface CustomButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
loading?: boolean;
icon?: ReactNode;
}
function CustomButton({ loading, icon, children, ...buttonProps }: CustomButtonProps) {
return (
<button {...buttonProps} disabled={loading || buttonProps.disabled}>
{loading ? <Spinner /> : icon}
{children}
</button>
);
}Generic Components
interface ListProps<T> {
items: T[];
renderItem: (item: T) => ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map(item => (
<li key={keyExtractor(item)}>
{renderItem(item)}
</li>
))}
</ul>
);
}
// Usage with type inference
<List
items={users}
renderItem={(user) => <UserCard user={user} />}
keyExtractor={(user) => user.id}
/>Discriminated Unions for Component States
type DataState<T> =
| { status: 'loading' }
| { status: 'error'; error: Error }
| { status: 'success'; data: T };
function DataDisplay<T>({ state }: { state: DataState<T> }) {
switch (state.status) {
case 'loading':
return <Loading />;
case 'error':
return <Error message={state.error.message} />;
case 'success':
return <Data data={state.data} />;
}
}Typed Event Handlers
interface FormProps {
onSubmit: (data: FormData) => void;
}
function Form({ onSubmit }: FormProps) {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
onSubmit(formData);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
// Handle enter key
}
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleInputChange} onKeyPress={handleKeyPress} />
</form>
);
}Ref Types
// DOM element refs
function TextInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
}
// Component refs (React 19: ref as prop, no forwardRef needed)
interface InputProps {
placeholder: string;
ref?: React.Ref<HTMLInputElement>;
}
function Input({ placeholder, ref }: InputProps) {
return <input ref={ref} placeholder={placeholder} />;
}Context with TypeScript
interface AuthContextValue {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const value: AuthContextValue = {
user,
login: async (email, password) => {
const user = await loginAPI(email, password);
setUser(user);
},
logout: () => setUser(null)
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}Utility Types for Props
// Pick specific props from another component
type IconProps = Pick<ButtonProps, 'size' | 'color'>;
// Omit specific props
type ButtonWithoutClick = Omit<ButtonProps, 'onClick'>;
// Make all props optional
type PartialButtonProps = Partial<ButtonProps>;
// Make all props required
type RequiredButtonProps = Required<ButtonProps>;
// Extract prop types from a component
type MyButtonProps = ComponentProps<typeof Button>;Render Props with TypeScript
interface RenderPropProps<T> {
data: T;
render: (data: T) => ReactNode;
}
function DataRenderer<T>({ data, render }: RenderPropProps<T>) {
return <>{render(data)}</>;
}
// Usage
<DataRenderer
data={user}
render={(user) => <UserProfile user={user} />}
/>As Prop Pattern
interface BoxProps<T extends ElementType = 'div'> {
as?: T;
children: ReactNode;
}
type PolymorphicProps<T extends ElementType> = BoxProps<T> &
Omit<ComponentPropsWithoutRef<T>, keyof BoxProps>;
function Box<T extends ElementType = 'div'>({
as,
children,
...props
}: PolymorphicProps<T>) {
const Component = as || 'div';
return <Component {...props}>{children}</Component>;
}
// Usage - full type safety!
<Box as="button" onClick={() => {}} /> // onClick is typed
<Box as="a" href="/">Link</Box> // href is typedControlled Component Types
interface ControlledInputProps {
value: string;
onChange: (value: string) => void;
}
function ControlledInput({ value, onChange }: ControlledInputProps) {
return (
<input
value={value}
onChange={(e) => onChange(e.target.value)}
/>
);
}
// With generic for any value type
interface ControlledFieldProps<T> {
value: T;
onChange: (value: T) => void;
}Type-Safe Form Handling
interface LoginForm {
email: string;
password: string;
}
function LoginForm() {
const [form, setForm] = useState<LoginForm>({
email: '',
password: ''
});
const handleChange = <K extends keyof LoginForm>(
field: K,
value: LoginForm[K]
) => {
setForm(prev => ({ ...prev, [field]: value }));
};
return (
<form>
<input
value={form.email}
onChange={(e) => handleChange('email', e.target.value)}
/>
<input
value={form.password}
onChange={(e) => handleChange('password', e.target.value)}
type="password"
/>
</form>
);
}Children Prop Patterns
// ReactNode (most common)
interface ContainerProps {
children: ReactNode;
}
// Render function
interface RenderChildrenProps {
children: (data: Data) => ReactNode;
}
// Specific component type
interface LayoutProps {
children: ReactElement<HeaderProps> | ReactElement<HeaderProps>[];
}
// Array of specific elements
interface TabsProps {
children: ReactElement<TabProps>[];
}Type Guards for Components
function isButtonElement(
element: ReactElement
): element is ReactElement<ButtonProps> {
return element.type === Button;
}
function Container({ children }: { children: ReactNode }) {
const buttons = Children.toArray(children).filter(
(child): child is ReactElement<ButtonProps> =>
isValidElement(child) && isButtonElement(child)
);
return <div>{buttons}</div>;
}{
"react": {
"type": "domain",
"enforcement": "suggest",
"priority": "medium",
"promptTriggers": {
"keywords": [
"react",
"useState",
"useEffect",
"useCallback",
"useMemo",
"useRef",
"useContext",
"useReducer",
"useTransition",
"useDeferredValue",
"useId",
"useSyncExternalStore",
"useImperativeHandle",
"useLayoutEffect",
"React.lazy",
"React.memo",
"React.forwardRef",
"React.createContext",
"createContext",
"forwardRef",
"Suspense",
"ErrorBoundary",
"StrictMode",
"startTransition",
"flushSync",
"createPortal",
"cloneElement"
],
"intentPatterns": [
"use.*react.*hook",
"implement.*(useState|useEffect|useCallback|useMemo|useContext|useReducer)",
"add.*react.*(lazy|suspense|memo)",
"create.*react.*context",
"optimize.*react.*component",
"memoize.*react.*component",
"use.*(useTransition|useDeferredValue)",
"create.*error.*boundary",
"implement.*react.*(forwardRef|memo)",
"add.*react.*portal"
]
},
"fileTriggers": {
"pathPatterns": [
"**/*.tsx",
"**/*.jsx",
"**/components/**/*.ts",
"**/features/**/*.tsx",
"**/hooks/**/*.ts",
"**/hooks/**/*.tsx"
],
"contentPatterns": [
"import.*from ['\"]react['\"]",
"import.*React.*from ['\"]react['\"]",
"useState\\(",
"useEffect\\(",
"useCallback\\(",
"useMemo\\(",
"useContext\\(",
"useReducer\\(",
"React\\.memo\\(",
"React\\.lazy\\(",
"forwardRef\\("
]
}
}
}
Related skills
FAQ
Which React version does this cover?
React 19, released December 2024, including its breaking changes.
Is forwardRef still needed?
No, in React 19 you pass ref as a prop directly instead of using forwardRef.