4 min readProgramming
Building Modern React Components: Patterns and Best Practices
Learn how to create scalable, maintainable React components using modern patterns, hooks, and TypeScript for better developer experience and performance.
- #react
- #typescript
- #components
- #hooks
- #frontend
Building Modern React Components
Creating well-structured, maintainable React components is crucial for building scalable applications. Here are modern patterns and best practices for better React code.
Component Architecture Principles
Single Responsibility
Each component should have one clear purpose:
// Good: Single responsibility
function UserAvatar({ user, size = 'medium' }: UserAvatarProps) {
return (
<img
src={user.avatar}
alt={user.name}
className={`avatar avatar-${size}`}
/>
);
}
// Good: Separate concerns
function UserProfile({ user }: UserProfileProps) {
return (
<div className="user-profile">
<UserAvatar user={user} size="large" />
<UserDetails user={user} />
<UserActions user={user} />
</div>
);
}
Composition Over Inheritance
Use composition for flexible components:
// Compound component pattern
function Card({ children, className = '' }: CardProps) {
return <div className={`card ${className}`}>{children}</div>;
}
function CardHeader({ children }: CardHeaderProps) {
return <header className="card-header">{children}</header>;
}
function CardBody({ children }: CardBodyProps) {
return <div className="card-body">{children}</div>;
}
function CardFooter({ children }: CardFooterProps) {
return <footer className="card-footer">{children}</footer>;
}
// Usage
<Card>
<CardHeader>
<h2>Article Title</h2>
</CardHeader>
<CardBody>
<p>Article content goes here...</p>
</CardBody>
<CardFooter>
<button>Read More</button>
</CardFooter>
</Card>;
TypeScript Integration
Strong Typing for Props
Define clear interfaces for your components:
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
size: 'small' | 'medium' | 'large';
disabled?: boolean;
loading?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
function Button({
variant,
size,
disabled = false,
loading = false,
onClick,
children,
}: ButtonProps) {
const className = `btn btn-${variant} btn-${size}`;
return (
<button
className={className}
disabled={disabled || loading}
onClick={onClick}
>
{loading ? <Spinner /> : children}
</button>
);
}
Generic Components
Create reusable components with generics:
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// Usage
<List
items={users}
renderItem={(user) => <UserCard user={user} />}
keyExtractor={(user) => user.id}
/>;
Modern Hook Patterns
Custom Hooks for Business Logic
Extract complex logic into custom hooks:
function useApi<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
}
fetchData();
}, [url]);
return { data, loading, error };
}
// Usage in component
function UserList() {
const { data: users, loading, error } = useApi<User[]>('/api/users');
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
if (!users) return <EmptyState />;
return (
<List
items={users}
renderItem={(user) => <UserCard user={user} />}
keyExtractor={(user) => user.id}
/>
);
}
State Management with useReducer
For complex state logic, prefer useReducer:
type FormState = {
values: Record<string, string>;
errors: Record<string, string>;
isSubmitting: boolean;
};
type FormAction =
| { type: 'SET_FIELD'; field: string; value: string }
| { type: 'SET_ERROR'; field: string; error: string }
| { type: 'SET_SUBMITTING'; isSubmitting: boolean }
| { type: 'RESET' };
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
values: { ...state.values, [action.field]: action.value },
errors: { ...state.errors, [action.field]: '' },
};
case 'SET_ERROR':
return {
...state,
errors: { ...state.errors, [action.field]: action.error },
};
case 'SET_SUBMITTING':
return { ...state, isSubmitting: action.isSubmitting };
case 'RESET':
return { values: {}, errors: {}, isSubmitting: false };
default:
return state;
}
}
function useForm(initialValues: Record<string, string> = {}) {
const [state, dispatch] = useReducer(formReducer, {
values: initialValues,
errors: {},
isSubmitting: false,
});
const setField = (field: string, value: string) => {
dispatch({ type: 'SET_FIELD', field, value });
};
const setError = (field: string, error: string) => {
dispatch({ type: 'SET_ERROR', field, error });
};
return { state, setField, setError, dispatch };
}
Performance Optimization
Memoization Strategies
Use React.memo, useMemo, and useCallback strategically:
// Memo for expensive components
const ExpensiveComponent = React.memo(function ExpensiveComponent({
data,
onUpdate,
}: ExpensiveComponentProps) {
// Expensive computation
const processedData = useMemo(() => {
return data.map((item) => ({
...item,
computed: expensiveCalculation(item),
}));
}, [data]);
// Stable callback reference
const handleClick = useCallback(
(id: string) => {
onUpdate(id);
},
[onUpdate],
);
return (
<div>
{processedData.map((item) => (
<ItemComponent key={item.id} item={item} onClick={handleClick} />
))}
</div>
);
});
Lazy Loading
Implement lazy loading for better performance:
import { lazy, Suspense } from 'react';
const LazyModal = lazy(() => import('./Modal'));
const LazyChart = lazy(() => import('./Chart'));
function Dashboard() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<h1>Dashboard</h1>
{showModal && (
<Suspense fallback={<div>Loading modal...</div>}>
<LazyModal onClose={() => setShowModal(false)} />
</Suspense>
)}
<Suspense fallback={<div>Loading chart...</div>}>
<LazyChart data={chartData} />
</Suspense>
</div>
);
}
Testing Patterns
Component Testing
Write comprehensive tests for your components:
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
describe('Button', () => {
it('renders with correct text', () => {
render(
<Button variant="primary" size="medium">
Click me
</Button>,
);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(
<Button variant="primary" size="medium" onClick={handleClick}>
Click me
</Button>,
);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('shows loading state', () => {
render(
<Button variant="primary" size="medium" loading>
Click me
</Button>,
);
expect(screen.getByRole('button')).toBeDisabled();
});
});
Conclusion
Building modern React components requires attention to:
- Architecture: Clear separation of concerns and composition
- TypeScript: Strong typing for better developer experience
- Performance: Strategic memoization and lazy loading
- Testing: Comprehensive test coverage
By following these patterns, you'll create components that are maintainable, reusable, performant, and testable.
AI has been used to correct sentence flow and improve readability.