React TypeScript Best Practices: Advanced Component Development
React with TypeScript provides excellent type safety and developer experience for building scalable frontend applications. This comprehensive guide covers advanced component patterns, custom hooks, performance optimization, and production-ready development practices.
Why Use React with TypeScript?
- Type Safety: Catch errors at compile time with strict typing
- Better Developer Experience: Enhanced IntelliSense and autocomplete
- Refactoring Confidence: Safe code refactoring with type checking
- Self-Documenting Code: Types serve as inline documentation
- Team Collaboration: Consistent interfaces across team members
Step 1: Project Setup and Configuration
Set up a production-ready React TypeScript project:
# Create React app with TypeScript template
npx create-react-app react-typescript-app --template typescript
cd react-typescript-app
# Install additional dependencies
npm install @types/react @types/react-dom
npm install react-router-dom @types/react-router-dom
npm install @tanstack/react-query axios
npm install react-hook-form @hookform/resolvers zod
npm install @headlessui/react @heroicons/react
npm install clsx tailwind-merge
# Install development dependencies
npm install -D @typescript-eslint/eslint-plugin @typescript-eslint/parser
npm install -D prettier eslint-config-prettier
npm install -D @testing-library/react @testing-library/jest-dom
npm install -D @storybook/react @storybook/addon-essentials
Configure TypeScript for strict type checking:
{
"compilerOptions": {
"target": "ES2020",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "src",
"paths": {
"@/*": ["*"],
"@/components/*": ["components/*"],
"@/hooks/*": ["hooks/*"],
"@/utils/*": ["utils/*"],
"@/types/*": ["types/*"],
"@/services/*": ["services/*"]
}
},
"include": ["src"]
}
tsconfig.json
Step 2: Advanced Type Definitions
Create comprehensive type definitions:
// Base types
export interface BaseEntity {
id: string;
createdAt: string;
updatedAt: string;
}
// User types
export interface User extends BaseEntity {
email: string;
username: string;
firstName: string;
lastName: string;
avatar?: string;
role: UserRole;
isActive: boolean;
preferences: UserPreferences;
}
export type UserRole = "admin" | "user" | "moderator";
export interface UserPreferences {
theme: "light" | "dark" | "system";
language: string;
notifications: {
email: boolean;
push: boolean;
sms: boolean;
};
privacy: {
profileVisibility: "public" | "friends" | "private";
showEmail: boolean;
showPhone: boolean;
};
}
// API response types
export interface ApiResponse<T = any> {
success: boolean;
data?: T;
message?: string;
errors?: Record<string, string[]>;
meta?: {
pagination?: {
page: number;
limit: number;
total: number;
totalPages: number;
};
timestamp: string;
requestId: string;
};
}
export interface PaginatedResponse<T> {
items: T[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
}
// Form types
export interface FormField<T = any> {
name: keyof T;
label: string;
type:
| "text"
| "email"
| "password"
| "number"
| "select"
| "textarea"
| "checkbox"
| "radio";
placeholder?: string;
required?: boolean;
options?: Array<{ value: string; label: string }>;
validation?: {
min?: number;
max?: number;
pattern?: string;
custom?: (value: any) => string | undefined;
};
}
// Component props types
export interface BaseComponentProps {
className?: string;
children?: React.ReactNode;
"data-testid"?: string;
}
export interface ButtonProps extends BaseComponentProps {
variant?: "primary" | "secondary" | "danger" | "ghost";
size?: "sm" | "md" | "lg";
disabled?: boolean;
loading?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
type?: "button" | "submit" | "reset";
icon?: React.ReactNode;
iconPosition?: "left" | "right";
}
export interface InputProps extends BaseComponentProps {
name: string;
type?: "text" | "email" | "password" | "number" | "tel" | "url";
placeholder?: string;
value?: string;
defaultValue?: string;
disabled?: boolean;
required?: boolean;
error?: string;
label?: string;
helpText?: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
}
// Hook types
export interface UseApiOptions<T> {
initialData?: T;
enabled?: boolean;
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
retry?: number | boolean;
retryDelay?: number;
staleTime?: number;
cacheTime?: number;
}
export interface UseFormOptions<T> {
initialValues?: Partial<T>;
validationSchema?: any;
onSubmit: (values: T) => Promise<void> | void;
validateOnBlur?: boolean;
validateOnChange?: boolean;
}
// Utility types
export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
export type RequiredFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
export type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
// Event handler types
export type EventHandler<T = Element> = (
event: React.SyntheticEvent<T>
) => void;
export type ChangeHandler<T = HTMLInputElement> = (
event: React.ChangeEvent<T>
) => void;
export type ClickHandler<T = HTMLButtonElement> = (
event: React.MouseEvent<T>
) => void;
export type SubmitHandler<T = HTMLFormElement> = (
event: React.FormEvent<T>
) => void;
// Generic component types
export type ComponentWithAs<P = {}> = <C extends React.ElementType = "div">(
props: P & {
as?: C;
} & Omit<React.ComponentPropsWithoutRef<C>, keyof P | "as">
) => React.ReactElement | null;
export type PolymorphicRef<C extends React.ElementType> =
React.ComponentPropsWithRef<C>["ref"];
export type PolymorphicComponentProp<
C extends React.ElementType,
Props = {},
> = React.PropsWithChildren<
Props & {
as?: C;
}
> &
Omit<React.ComponentPropsWithoutRef<C>, keyof Props>;
export type PolymorphicComponentPropWithRef<
C extends React.ElementType,
Props = {},
> = PolymorphicComponentProp<C, Props> & { ref?: PolymorphicRef<C> };
src/types/index.ts
Step 3: Advanced Component Patterns
Create reusable components with advanced TypeScript patterns:
import React, { forwardRef } from "react";
import { clsx } from "clsx";
import { ButtonProps } from "@/types";
// Button variants configuration
const buttonVariants = {
primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
secondary: "bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500",
danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
ghost: "bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500",
} as const;
const buttonSizes = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-base",
lg: "px-6 py-3 text-lg",
} as const;
// Button component with forwarded ref
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = "primary",
size = "md",
disabled = false,
loading = false,
children,
className,
onClick,
type = "button",
icon,
iconPosition = "left",
"data-testid": testId,
...rest
},
ref
) => {
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
if (disabled || loading) return;
onClick?.(event);
};
const buttonClasses = clsx(
// Base styles
"inline-flex items-center justify-center font-medium rounded-md transition-colors duration-200",
"focus:outline-none focus:ring-2 focus:ring-offset-2",
"disabled:opacity-50 disabled:cursor-not-allowed",
// Variant styles
buttonVariants[variant],
// Size styles
buttonSizes[size],
// Loading state
loading && "cursor-wait",
// Custom className
className
);
const iconElement = icon && (
<span
className={clsx("flex-shrink-0", {
"mr-2": iconPosition === "left" && children,
"ml-2": iconPosition === "right" && children,
})}
>
{icon}
</span>
);
const loadingSpinner = loading && (
<svg
className={clsx("h-4 w-4 animate-spin", {
"mr-2": children,
})}
fill="none"
viewBox="0 0 24 24"
>
<circle
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
className="opacity-25"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
);
return (
<button
ref={ref}
type={type}
className={buttonClasses}
disabled={disabled || loading}
onClick={handleClick}
data-testid={testId}
aria-disabled={disabled || loading}
aria-label={loading ? "Loading..." : undefined}
{...rest}
>
{loading && loadingSpinner}
{!loading && iconPosition === "left" && iconElement}
{children}
{!loading && iconPosition === "right" && iconElement}
</button>
);
}
);
Button.displayName = "Button";
export default Button;
src/components/Button/Button.tsx
import React from "react";
import {
useController,
Control,
FieldPath,
FieldValues,
} from "react-hook-form";
import { clsx } from "clsx";
import { InputProps } from "@/types";
// Generic form field component
interface FormFieldProps<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> extends Omit<InputProps, "name"> {
name: TName;
control: Control<TFieldValues>;
rules?: {
required?: boolean | string;
minLength?: number | { value: number; message: string };
maxLength?: number | { value: number; message: string };
pattern?: { value: RegExp; message: string };
validate?: (value: any) => string | boolean | undefined;
};
}
export function FormField<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
name,
control,
rules,
label,
helpText,
className,
type = "text",
placeholder,
disabled = false,
required = false,
"data-testid": testId,
...rest
}: FormFieldProps<TFieldValues, TName>) {
const {
field,
fieldState: { error, invalid },
} = useController({
name,
control,
rules: {
...rules,
required: required
? typeof rules?.required === "string"
? rules.required
: `${label} is required`
: false,
},
});
const inputId = `field-${String(name)}`;
const errorId = `${inputId}-error`;
const helpId = `${inputId}-help`;
const inputClasses = clsx(
"block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm",
"placeholder-gray-400 text-gray-900",
"focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
"disabled:bg-gray-50 disabled:text-gray-500 disabled:cursor-not-allowed",
{
"border-red-300 focus:ring-red-500 focus:border-red-500": invalid,
"pr-10": invalid, // Space for error icon
},
className
);
return (
<div className="space-y-1">
{label && (
<label
htmlFor={inputId}
className="block text-sm font-medium text-gray-700"
>
{label}
{required && <span className="ml-1 text-red-500">*</span>}
</label>
)}
<div className="relative">
<input
{...field}
{...rest}
id={inputId}
type={type}
placeholder={placeholder}
disabled={disabled}
className={inputClasses}
aria-invalid={invalid}
aria-describedby={clsx({
[errorId]: invalid,
[helpId]: helpText,
})}
data-testid={testId}
/>
{invalid && (
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<svg
className="h-5 w-5 text-red-500"
fill="currentColor"
viewBox="0 0 20 20"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
</div>
)}
</div>
{error && (
<p id={errorId} className="text-sm text-red-600" role="alert">
{error.message}
</p>
)}
{helpText && !error && (
<p id={helpId} className="text-sm text-gray-500">
{helpText}
</p>
)}
</div>
);
}
src/components/Form/FormField.tsx
Step 4: Advanced Custom Hooks
Create powerful custom hooks with TypeScript:
import { useState, useEffect, useCallback, useRef } from "react";
import { UseApiOptions } from "@/types";
interface UseApiState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
interface UseApiReturn<T> extends UseApiState<T> {
refetch: () => Promise<void>;
mutate: (data: T) => void;
}
// Generic API hook with TypeScript
export function useApi<T = any>(
fetcher: () => Promise<T>,
options: UseApiOptions<T> = {}
): UseApiReturn<T> {
const {
initialData = null,
enabled = true,
onSuccess,
onError,
retry = 3,
retryDelay = 1000,
staleTime = 0,
cacheTime = 5 * 60 * 1000, // 5 minutes
} = options;
const [state, setState] = useState<UseApiState<T>>({
data: initialData,
loading: false,
error: null,
});
const retryCountRef = useRef(0);
const fetchTimestampRef = useRef<number>(0);
const cacheRef = useRef<{ data: T; timestamp: number } | null>(null);
const isDataStale = useCallback(() => {
if (!cacheRef.current) return true;
return Date.now() - cacheRef.current.timestamp > staleTime;
}, [staleTime]);
const fetchData = useCallback(async (): Promise<void> => {
// Return cached data if not stale
if (cacheRef.current && !isDataStale()) {
setState(prev => ({
...prev,
data: cacheRef.current!.data,
loading: false,
error: null,
}));
return;
}
setState(prev => ({ ...prev, loading: true, error: null }));
const fetchStart = Date.now();
fetchTimestampRef.current = fetchStart;
try {
const result = await fetcher();
// Ignore result if newer request exists
if (fetchTimestampRef.current !== fetchStart) return;
// Cache the result
cacheRef.current = { data: result, timestamp: Date.now() };
setState(prev => ({
...prev,
data: result,
loading: false,
error: null,
}));
retryCountRef.current = 0;
onSuccess?.(result);
} catch (error) {
// Ignore error if newer request exists
if (fetchTimestampRef.current !== fetchStart) return;
const apiError =
error instanceof Error ? error : new Error("Unknown error");
// Retry logic
if (retryCountRef.current < (typeof retry === "number" ? retry : 3)) {
retryCountRef.current += 1;
setTimeout(() => {
if (fetchTimestampRef.current === fetchStart) {
fetchData();
}
}, retryDelay * retryCountRef.current);
return;
}
setState(prev => ({ ...prev, loading: false, error: apiError }));
onError?.(apiError);
}
}, [fetcher, onSuccess, onError, retry, retryDelay, isDataStale]);
const refetch = useCallback(async (): Promise<void> => {
// Clear cache to force fresh fetch
cacheRef.current = null;
await fetchData();
}, [fetchData]);
const mutate = useCallback((data: T): void => {
// Update cache and state
cacheRef.current = { data, timestamp: Date.now() };
setState(prev => ({ ...prev, data, error: null }));
}, []);
useEffect(() => {
if (enabled) {
fetchData();
}
}, [enabled, fetchData]);
// Cleanup cache when component unmounts
useEffect(() => {
return () => {
const timeout = setTimeout(() => {
cacheRef.current = null;
}, cacheTime);
return () => clearTimeout(timeout);
};
}, [cacheTime]);
return {
...state,
refetch,
mutate,
};
}
src/hooks/useApi.ts
import { useState, useEffect, useCallback } from "react";
type SetValue<T> = T | ((prev: T) => T);
interface UseLocalStorageReturn<T> {
value: T;
setValue: (value: SetValue<T>) => void;
remove: () => void;
}
// Type-safe local storage hook
export function useLocalStorage<T>(
key: string,
initialValue: T,
options: {
serialize?: (value: T) => string;
deserialize?: (value: string) => T;
} = {}
): UseLocalStorageReturn<T> {
const { serialize = JSON.stringify, deserialize = JSON.parse } = options;
// Get value from localStorage
const getStoredValue = useCallback((): T => {
try {
const item = window.localStorage.getItem(key);
return item ? deserialize(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
}, [key, initialValue, deserialize]);
const [value, setValue] = useState<T>(getStoredValue);
// Set value in localStorage
const setStoredValue = useCallback(
(newValue: SetValue<T>) => {
try {
const valueToStore =
typeof newValue === "function"
? (newValue as (prev: T) => T)(value)
: newValue;
setValue(valueToStore);
if (valueToStore === undefined) {
window.localStorage.removeItem(key);
} else {
window.localStorage.setItem(key, serialize(valueToStore));
}
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
},
[key, serialize, value]
);
// Remove value from localStorage
const removeValue = useCallback(() => {
try {
window.localStorage.removeItem(key);
setValue(initialValue);
} catch (error) {
console.error(`Error removing localStorage key "${key}":`, error);
}
}, [key, initialValue]);
// Listen for changes in localStorage
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === key && e.newValue !== serialize(value)) {
setValue(e.newValue ? deserialize(e.newValue) : initialValue);
}
};
window.addEventListener("storage", handleStorageChange);
return () => window.removeEventListener("storage", handleStorageChange);
}, [key, value, initialValue, serialize, deserialize]);
return {
value,
setValue: setStoredValue,
remove: removeValue,
};
}
src/hooks/useLocalStorage.ts
import { useState, useEffect } from "react";
// Debounce hook with TypeScript
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;
}
// Debounced callback hook
export function useDebouncedCallback<T extends (...args: any[]) => any>(
callback: T,
delay: number,
deps: React.DependencyList = []
): T {
const [debouncedCallback, setDebouncedCallback] = useState<T>(callback);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedCallback(() => callback);
}, delay);
return () => {
clearTimeout(handler);
};
}, [callback, delay, ...deps]);
return debouncedCallback;
}
src/hooks/useDebounce.ts
Step 5: Performance Optimization Components
Create performance-optimized components:
import React, {
useMemo,
useCallback,
useState,
useEffect,
useRef,
} from "react";
interface VirtualListProps<T> {
items: T[];
itemHeight: number;
containerHeight: number;
renderItem: (item: T, index: number) => React.ReactNode;
overscan?: number;
className?: string;
onScroll?: (scrollTop: number) => void;
}
// High-performance virtual list component
export function VirtualList<T>({
items,
itemHeight,
containerHeight,
renderItem,
overscan = 5,
className,
onScroll,
}: VirtualListProps<T>) {
const [scrollTop, setScrollTop] = useState(0);
const scrollElementRef = useRef<HTMLDivElement>(null);
// Calculate visible range
const { startIndex, endIndex, totalHeight } = useMemo(() => {
const start = Math.floor(scrollTop / itemHeight);
const visibleCount = Math.ceil(containerHeight / itemHeight);
const end = Math.min(items.length - 1, start + visibleCount);
return {
startIndex: Math.max(0, start - overscan),
endIndex: Math.min(items.length - 1, end + overscan),
totalHeight: items.length * itemHeight,
};
}, [scrollTop, itemHeight, containerHeight, items.length, overscan]);
// Get visible items
const visibleItems = useMemo(() => {
return items.slice(startIndex, endIndex + 1).map((item, index) => ({
item,
index: startIndex + index,
}));
}, [items, startIndex, endIndex]);
// Handle scroll
const handleScroll = useCallback(
(event: React.UIEvent<HTMLDivElement>) => {
const newScrollTop = event.currentTarget.scrollTop;
setScrollTop(newScrollTop);
onScroll?.(newScrollTop);
},
[onScroll]
);
// Scroll to index
const scrollToIndex = useCallback(
(index: number) => {
if (scrollElementRef.current) {
const scrollTop = index * itemHeight;
scrollElementRef.current.scrollTop = scrollTop;
setScrollTop(scrollTop);
}
},
[itemHeight]
);
return (
<div
ref={scrollElementRef}
className={className}
style={{ height: containerHeight, overflow: "auto" }}
onScroll={handleScroll}
>
<div style={{ height: totalHeight, position: "relative" }}>
<div
style={{
transform: `translateY(${startIndex * itemHeight}px)`,
}}
>
{visibleItems.map(({ item, index }) => (
<div
key={index}
style={{
height: itemHeight,
display: "flex",
alignItems: "center",
}}
>
{renderItem(item, index)}
</div>
))}
</div>
</div>
</div>
);
}
src/components/VirtualList/VirtualList.tsx
import React, { useState, useRef, useEffect, useCallback } from "react";
import { clsx } from "clsx";
interface LazyImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
src: string;
alt: string;
placeholder?: string;
fallback?: string;
threshold?: number;
rootMargin?: string;
className?: string;
onLoad?: () => void;
onError?: () => void;
}
// Lazy loading image component with intersection observer
export function LazyImage({
src,
alt,
placeholder,
fallback,
threshold = 0.1,
rootMargin = "50px",
className,
onLoad,
onError,
...props
}: LazyImageProps) {
const [imageSrc, setImageSrc] = useState<string | undefined>(placeholder);
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const [isInView, setIsInView] = useState(false);
const imgRef = useRef<HTMLImageElement>(null);
// Intersection Observer
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
observer.disconnect();
}
},
{ threshold, rootMargin }
);
const currentImg = imgRef.current;
if (currentImg) {
observer.observe(currentImg);
}
return () => {
if (currentImg) {
observer.unobserve(currentImg);
}
};
}, [threshold, rootMargin]);
// Load image when in view
useEffect(() => {
if (isInView && src) {
const imageLoader = new Image();
imageLoader.onload = () => {
setImageSrc(src);
setIsLoading(false);
setHasError(false);
onLoad?.();
};
imageLoader.onerror = () => {
setHasError(true);
setIsLoading(false);
if (fallback) {
setImageSrc(fallback);
}
onError?.();
};
imageLoader.src = src;
}
}, [isInView, src, fallback, onLoad, onError]);
const imageClasses = clsx(
"transition-opacity duration-300",
{
"opacity-0": isLoading,
"opacity-100": !isLoading,
},
className
);
return (
<div className="relative overflow-hidden">
<img
ref={imgRef}
src={imageSrc}
alt={alt}
className={imageClasses}
{...props}
/>
{isLoading && (
<div className="absolute inset-0 flex animate-pulse items-center justify-center bg-gray-200">
<svg
className="h-6 w-6 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</div>
)}
{hasError && !fallback && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-100">
<div className="text-center text-gray-500">
<svg
className="mx-auto mb-2 h-8 w-8"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<p className="text-sm">Failed to load image</p>
</div>
</div>
)}
</div>
);
}
src/components/LazyImage/LazyImage.tsx
Step 6: Advanced Error Boundary
Create a comprehensive error boundary with TypeScript:
import React, { Component, ErrorInfo, ReactNode } from "react";
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: (error: Error, errorInfo: ErrorInfo) => ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
resetKeys?: Array<string | number>;
resetOnPropsChange?: boolean;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
export class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
return {
hasError: true,
error,
};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.setState({
error,
errorInfo,
});
// Log error to external service
this.props.onError?.(error, errorInfo);
// Log to console in development
if (process.env.NODE_ENV === "development") {
console.error("ErrorBoundary caught an error:", error, errorInfo);
}
}
componentDidUpdate(prevProps: ErrorBoundaryProps) {
const { resetKeys, resetOnPropsChange } = this.props;
const { hasError } = this.state;
if (
hasError &&
resetOnPropsChange &&
prevProps.children !== this.props.children
) {
this.resetErrorBoundary();
}
if (hasError && resetKeys) {
const hasResetKeyChanged = resetKeys.some(
(key, idx) => prevProps.resetKeys?.[idx] !== key
);
if (hasResetKeyChanged) {
this.resetErrorBoundary();
}
}
}
resetErrorBoundary = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
});
};
render() {
const { hasError, error, errorInfo } = this.state;
const { fallback, children } = this.props;
if (hasError && error) {
if (fallback) {
return fallback(error, errorInfo!);
}
return (
<DefaultErrorFallback
error={error}
resetError={this.resetErrorBoundary}
/>
);
}
return children;
}
}
// Default error fallback component
interface DefaultErrorFallbackProps {
error: Error;
resetError: () => void;
}
function DefaultErrorFallback({
error,
resetError,
}: DefaultErrorFallbackProps) {
return (
<div className="flex min-h-screen flex-col justify-center bg-gray-50 py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white px-4 py-8 shadow sm:rounded-lg sm:px-10">
<div className="text-center">
<svg
className="mx-auto h-12 w-12 text-red-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
vectorEffect="non-scaling-stroke"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.268 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
<h2 className="mt-4 text-lg font-medium text-gray-900">
Something went wrong
</h2>
<p className="mt-2 text-sm text-gray-600">
We're sorry, but something unexpected happened. Please try again.
</p>
{process.env.NODE_ENV === "development" && (
<details className="mt-4 text-left">
<summary className="cursor-pointer text-sm text-gray-500 hover:text-gray-700">
Error details (development only)
</summary>
<pre className="mt-2 whitespace-pre-wrap text-xs text-red-600">
{error.message}
{error.stack}
</pre>
</details>
)}
<div className="mt-6">
<button
type="button"
onClick={resetError}
className="flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Try again
</button>
</div>
</div>
</div>
</div>
</div>
);
}
// Hook for error boundary
export function useErrorHandler() {
const [error, setError] = React.useState<Error | null>(null);
const resetError = React.useCallback(() => {
setError(null);
}, []);
const captureError = React.useCallback((error: Error) => {
setError(error);
}, []);
React.useEffect(() => {
if (error) {
throw error;
}
}, [error]);
return { captureError, resetError };
}
src/components/ErrorBoundary/ErrorBoundary.tsx
Best Practices Summary
- Use strict TypeScript configuration for maximum type safety
- Implement proper component patterns with forwarded refs and generic types
- Create reusable custom hooks with proper TypeScript generics
- Optimize performance with virtual lists and lazy loading
- Handle errors gracefully with comprehensive error boundaries
- Use proper form handling with type-safe validation
- Implement accessibility features in all components
- Write comprehensive tests for components and hooks
- Document component APIs with proper TypeScript interfaces
- Follow React best practices for state management and effects
Development Commands
# Start development server
npm start
# Run type checking
npm run type-check
# Build for production
npm run build
# Run tests
npm test
# Run Storybook
npm run storybook
Your React TypeScript application is now ready for production with advanced component patterns, type safety, performance optimizations, and comprehensive error handling!