React with TypeScript
Build production-ready React applications using TypeScript with modern patterns and best practices.
Instructions
-
Always use TypeScript - Define proper types for props, state, and API responses
-
Prefer functional components - Use hooks over class components
-
Follow component composition - Build small, reusable, single-responsibility components
-
Implement proper error boundaries - Wrap critical UI sections
-
Use React 18+ features - Concurrent features, Suspense, transitions when appropriate
Component Patterns
Basic Component Structure
import { useState, useCallback } from 'react';
interface ButtonProps { label: string; onClick: () => void; variant?: 'primary' | 'secondary' | 'danger'; disabled?: boolean; loading?: boolean; }
export function Button({ label, onClick, variant = 'primary', disabled = false, loading = false, }: ButtonProps) { const handleClick = useCallback(() => { if (!disabled && !loading) { onClick(); } }, [disabled, loading, onClick]);
return (
<button
type="button"
onClick={handleClick}
disabled={disabled || loading}
className={btn btn-${variant}}
aria-busy={loading}
>
{loading ? <Spinner /> : label}
</button>
);
}
Custom Hooks Pattern
import { useState, useEffect, useCallback } from 'react';
interface UseApiResult<T> { data: T | null; loading: boolean; error: Error | null; refetch: () => void; }
function useApi<T>(url: string): UseApiResult<T> { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) throw new Error(HTTP ${response.status});
const json = await response.json();
setData(json);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
} finally {
setLoading(false);
}
}, [url]);
useEffect(() => { fetchData(); }, [fetchData]);
return { data, loading, error, refetch: fetchData }; }
Compound Component Pattern
import { createContext, useContext, useState, ReactNode } from 'react';
interface AccordionContextType { openItems: Set<string>; toggle: (id: string) => void; }
const AccordionContext = createContext<AccordionContextType | null>(null);
function useAccordion() { const context = useContext(AccordionContext); if (!context) throw new Error('Must be used within Accordion'); return context; }
interface AccordionProps { children: ReactNode; allowMultiple?: boolean; }
export function Accordion({ children, allowMultiple = false }: AccordionProps) { const [openItems, setOpenItems] = useState<Set<string>>(new Set());
const toggle = (id: string) => { setOpenItems(prev => { const next = new Set(allowMultiple ? prev : []); if (prev.has(id)) { next.delete(id); } else { next.add(id); } return next; }); };
return ( <AccordionContext.Provider value={{ openItems, toggle }}> <div role="region">{children}</div> </AccordionContext.Provider> ); }
Accordion.Item = function AccordionItem({ id, title, children }: { id: string; title: string; children: ReactNode; }) { const { openItems, toggle } = useAccordion(); const isOpen = openItems.has(id);
return (
<div>
<button
onClick={() => toggle(id)}
aria-expanded={isOpen}
aria-controls={panel-${id}}
>
{title}
</button>
{isOpen && <div id={panel-${id}}>{children}</div>}
</div>
);
};
State Management
Zustand (Recommended for most cases)
import { create } from 'zustand'; import { persist } from 'zustand/middleware';
interface UserState { user: User | null; isAuthenticated: boolean; login: (user: User) => void; logout: () => void; }
export const useUserStore = create<UserState>()( persist( (set) => ({ user: null, isAuthenticated: false, login: (user) => set({ user, isAuthenticated: true }), logout: () => set({ user: null, isAuthenticated: false }), }), { name: 'user-storage' } ) );
React Query for Server State
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function useTodos() { return useQuery({ queryKey: ['todos'], queryFn: () => fetch('/api/todos').then(r => r.json()), staleTime: 5 * 60 * 1000, // 5 minutes }); }
function useAddTodo() { const queryClient = useQueryClient();
return useMutation({ mutationFn: (newTodo: Todo) => fetch('/api/todos', { method: 'POST', body: JSON.stringify(newTodo), }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }); }, }); }
Project Structure
src/ ├── components/ │ ├── ui/ # Reusable UI components │ ├── features/ # Feature-specific components │ └── layouts/ # Layout components ├── hooks/ # Custom hooks ├── stores/ # State management ├── services/ # API services ├── types/ # TypeScript types ├── utils/ # Utility functions └── pages/ # Page components (if not using router)
Best Practices
-
Type everything - No any types, use unknown if needed
-
Memoize appropriately - Use useMemo , useCallback , React.memo for expensive operations
-
Handle loading/error states - Every async operation needs these states
-
Use proper keys - Never use array index as key for dynamic lists
-
Avoid prop drilling - Use context or state management for deep prop passing
-
Code split - Use React.lazy() for route-level code splitting
-
Test components - Write unit tests with React Testing Library
When to Use
-
Building React applications with TypeScript
-
Creating reusable component libraries
-
Implementing complex state management
-
Setting up new React projects
-
Refactoring class components to functional
Notes
-
Requires React 18+ for latest features
-
Use Vite for new projects (faster than CRA)
-
Consider Next.js for SSR/SSG requirements
-
Always include proper accessibility attributes