reactjavascriptclean-codebest-practicesfrontend
The Art of Writing Clean React Components
Practical patterns for building maintainable React components that your future self (and teammates) will thank you for.
December 12, 20259 min read
The Art of Writing Clean React Components
After reviewing hundreds of React codebases, I've noticed patterns that separate maintainable code from spaghetti. Here's what I've learned.
The Single Responsibility Principle
A component should do one thing well.
tsx
// ❌ Bad: This component does too much
function UserDashboard() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [notifications, setNotifications] = useState([]);
const [isEditing, setIsEditing] = useState(false);
// 200 lines of mixed logic...
}
// ✅ Good: Split into focused components
function UserDashboard() {
return (
<DashboardLayout>
<UserProfile />
<UserPosts />
<NotificationPanel />
</DashboardLayout>
);
}Custom Hooks for Logic
Extract business logic into custom hooks:
tsx
// ❌ Bad: Logic mixed with UI
function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [filter, setFilter] = useState("");
useEffect(() => {
fetchProducts()
.then(setProducts)
.catch(setError)
.finally(() => setLoading(false));
}, []);
const filteredProducts = products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase())
);
// ... render logic
}
// ✅ Good: Logic in a custom hook
function useProducts() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [filter, setFilter] = useState("");
useEffect(() => {
fetchProducts()
.then(setProducts)
.catch(setError)
.finally(() => setLoading(false));
}, []);
const filteredProducts = useMemo(() =>
products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase())
),
[products, filter]
);
return { products: filteredProducts, loading, error, setFilter };
}
function ProductList() {
const { products, loading, error, setFilter } = useProducts();
if (loading) return <Skeleton />;
if (error) return <Error message={error} />;
return <ProductGrid products={products} onFilter={setFilter} />;
}Composition Over Configuration
tsx
// ❌ Bad: Prop explosion
<Card
title="Hello"
subtitle="World"
showHeader={true}
showFooter={true}
headerClassName="bg-blue"
footerClassName="bg-gray"
onHeaderClick={() => {}}
footerContent={<Button>Save</Button>}
/>
// ✅ Good: Composition
<Card>
<Card.Header className="bg-blue" onClick={() => {}}>
<Card.Title>Hello</Card.Title>
<Card.Subtitle>World</Card.Subtitle>
</Card.Header>
<Card.Body>
{/* content */}
</Card.Body>
<Card.Footer className="bg-gray">
<Button>Save</Button>
</Card.Footer>
</Card>Props Interface Patterns
tsx
// Define clear interfaces
interface ButtonProps {
variant: "primary" | "secondary" | "ghost";
size?: "sm" | "md" | "lg";
isLoading?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
children: React.ReactNode;
onClick?: () => void;
}
// Use discriminated unions for complex states
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };Error Boundaries
Always wrap risky components:
tsx
function App() {
return (
<ErrorBoundary fallback={<ErrorPage />}>
<Suspense fallback={<Loading />}>
<Routes />
</Suspense>
</ErrorBoundary>
);
}My Component Checklist
- Does it have a single responsibility?
- Is the logic extracted into hooks?
- Are props well-typed?
- Is it wrapped in error boundaries where needed?
- Does it handle loading/error states?
- Is it accessible (keyboard nav, ARIA labels)?
- Would I understand this in 6 months?
Conclusion
Clean components aren't about following rules blindly—they're about making code that's easy to understand, modify, and debug. Start with these patterns, but always prioritize readability over cleverness.
What patterns do you follow? Let me know on Twitter or in the comments below.