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.

Learn how to create scalable, maintainable React components using modern patterns, hooks, and TypeScript for better developer experience and performance.
Creating well-structured, maintainable React components is crucial for building scalable applications. Here are modern patterns and best practices for better React code.
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>
);
}
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>;
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>
);
}
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}
/>;
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}
/>
);
}
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 };
}
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>
);
});
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>
);
}
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();
});
});
Building modern React components requires attention to:
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.