After years of building production React applications, I've learned that following best practices isn't just about writing clean code—it's about creating maintainable, scalable, and performant applications that teams can work with effectively.
Component Architecture
Well-structured components are the foundation of maintainable React applications:
Single Responsibility Principle
Each component should have one clear purpose. If a component is doing too much, break it down into smaller, focused components. This improves readability, testability, and reusability.
// ❌ Bad: Component doing too much
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Fetch user
fetchUser(userId).then(setUser);
// Fetch posts
fetchPosts(userId).then(setPosts);
setLoading(false);
}, [userId]);
return (
<div>
<UserHeader user={user} />
<UserStats user={user} />
<PostList posts={posts} />
</div>
);
}
// ✅ Good: Separated concerns
function UserProfile({ userId }: { userId: string }) {
return (
<div>
<UserHeader userId={userId} />
<UserStats userId={userId} />
<UserPosts userId={userId} />
</div>
);
}Component Composition
Prefer composition over inheritance. Build complex UIs by combining simple components. Use children props and render props patterns for flexibility.
// ✅ Composition pattern
function Card({ children, title }: { children: React.ReactNode; title: string }) {
return (
<div className="card">
<h2>{title}</h2>
{children}
</div>
);
}
function App() {
return (
<Card title="User Profile">
<UserInfo />
<UserActions />
</Card>
);
}State Management
Effective state management is crucial:
// ✅ Local state with useState
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// ✅ Shared state with Context
const ThemeContext = createContext<'light' | 'dark'>('light');
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}
// ✅ Server state with React Query
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
if (isLoading) return <Spinner />;
return <div>{data.name}</div>;
}Performance Optimization
Memoization
Use React.memo, useMemo, and useCallback judiciously. Don't over-optimize, but identify expensive computations and re-renders that can be prevented.
// ✅ Memoizing expensive computations
function ExpensiveComponent({ items }: { items: Item[] }) {
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.price - b.price);
}, [items]);
return <ItemList items={sortedItems} />;
}
// ✅ Memoizing callbacks
function Parent({ userId }: { userId: string }) {
const handleClick = useCallback(() => {
updateUser(userId);
}, [userId]);
return <Child onClick={handleClick} />;
}
// ✅ Memoizing components
const ExpensiveChild = React.memo(({ data }: { data: Data }) => {
return <ComplexVisualization data={data} />;
});Code Splitting
Implement route-based and component-based code splitting to reduce initial bundle size. Use React.lazy and Suspense for dynamic imports.
// ✅ Route-based code splitting
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Router>
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</Router>
);
}TypeScript Integration
TypeScript adds type safety and improves developer experience:
// ✅ Proper type definitions
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
interface UserCardProps {
user: User;
onEdit?: (user: User) => void;
showActions?: boolean;
}
function UserCard({ user, onEdit, showActions = false }: UserCardProps) {
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
{showActions && onEdit && (
<button onClick={() => onEdit(user)}>Edit</button>
)}
</div>
);
}
// ✅ Generic components
function List<T>({
items,
renderItem
}: {
items: T[];
renderItem: (item: T) => React.ReactNode;
}) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{renderItem(item)}</li>
))}
</ul>
);
}Error Handling
Robust error handling improves user experience:
// ✅ Error Boundary
class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean }
> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <ErrorFallback />;
}
return this.props.children;
}
}
// ✅ Async error handling
function UserProfile({ userId }: { userId: string }) {
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetchUser(userId)
.then(setUser)
.catch((err) => {
setError(err);
logError(err);
});
}, [userId]);
if (error) return <ErrorMessage error={error} />;
return <UserDetails />;
}Conclusion
These practices have helped me build production applications that are maintainable, performant, and scalable. The key is to apply them consistently and adapt them to your specific project needs. Remember, best practices evolve, so stay updated with the React ecosystem and community.