Engineering in Frontend
Frontend code should follow proven software engineering principles to stay readable, maintainable, and scalable as the project grows. Two core principles are SOLID and DRY.
SOLID principles
1. Single Responsibility Principle (SRP)
- Each component or function should do one thing only.
- This reduces complexity and makes testing easier.
Bad example
tsx
// ❌ Handles UI + API fetch + modal
function UserProfile() {
const [user, setUser] = useState(null);
const [open, setOpen] = useState(false);
useEffect(() => {
fetch("/api/user").then(res => res.json()).then(setUser);
}, []);
return (
<>
<div onClick={() => setOpen(true)}>{user?.name}</div>
{open && <div className="modal">User details</div>}
</>
);
}Good example
tsx
// ✅ Split concerns
function UserProfileView({ user, onOpen }: { user: User; onOpen: () => void }) {
return <div onClick={onOpen}>{user.name}</div>;
}
function useUser() {
return useQuery(["user"], () => fetch("/api/user").then(res => res.json()));
}
function UserProfile() {
const { data: user } = useUser();
const { isOpen, open, close } = useModal();
return (
<>
{user && <UserProfileView user={user} onOpen={open} />}
{isOpen && <UserModal onClose={close} />}
</>
);
}2. Open/Closed Principle (OCP)
- Code should be open for extension but closed for modification.
- Prefer configurable props and composition instead of rewriting components.
Bad example
tsx
// ❌ New requirement: add "secondary" button → rewrite component
function Button({ label, primary }: { label: string; primary?: boolean }) {
return (
<button className={primary ? "bg-blue-500" : "bg-gray-500"}>
{label}
</button>
);
}Good example
tsx
// ✅ Extendable via props/variants
function Button({ label, variant = "primary" }: { label: string; variant?: "primary" | "secondary" }) {
const classes = {
primary: "bg-blue-500",
secondary: "bg-green-500",
};
return <button className={classes[variant]}>{label}</button>;
}3. Liskov Substitution Principle (LSP)
- Components or functions should be replaceable with subtypes without breaking behavior.
Example
tsx
interface InputProps {
value: string;
onChange: (val: string) => void;
}
// ✅ Both TextInput and PasswordInput respect InputProps
function TextInput(props: InputProps) {
return <input type="text" {...props} />;
}
function PasswordInput(props: InputProps) {
return <input type="password" {...props} />;
}4. Interface Segregation Principle (ISP)
- Prefer smaller, focused props/interfaces instead of giant ones.
Bad example
tsx
// ❌ Bloated props interface
interface FormProps {
name: string;
email: string;
phone: string;
age: number;
address: string;
country: string;
}Good example
tsx
// ✅ Smaller interfaces, composed together
interface ContactInfo {
name: string;
email: string;
}
interface LocationInfo {
country: string;
address: string;
}
type FormProps = ContactInfo & LocationInfo;5. Dependency Inversion Principle (DIP)
- Components should not directly depend on low-level details (like specific APIs or styling). They should depend on abstractions so they can easily switch implementations.
Bad example
tsx
// ❌ Component tied directly to raw HTML & styling
function SubmitButton() {
return <button className="bg-blue-500 text-white">Submit</button>;
}Good example
tsx
// ✅ Component depends on an abstract UI wrapper
import { Button } from "@/components/ui/button";
function SubmitButton() {
return <Button variant="primary">Submit</Button>;
}Here, the component depends on a reusable Button abstraction. If the team switches from Tailwind to MUI or another library, only the Button implementation changes, not every usage.
DRY (Don't Repeat Yourself)
Avoid duplicating code. Extract shared UI patterns into reusable components.
Bad example
tsx
// ❌ Card UI duplicated in multiple places
function ProductCard({ product }: { product: Product }) {
return (
<div className="border rounded p-4 shadow">
<h2>{product.name}</h2>
<p>{product.price}</p>
</div>
);
}
function OrderCard({ order }: { order: Order }) {
return (
<div className="border rounded p-4 shadow">
<h2>{order.title}</h2>
<p>{order.total}</p>
</div>
);
}Good example
tsx
// ✅ Shared reusable Card component
function Card({ title, subtitle }: { title: string; subtitle: string }) {
return (
<div className="border rounded p-4 shadow">
<h2>{title}</h2>
<p>{subtitle}</p>
</div>
);
}
function ProductCard({ product }: { product: Product }) {
return <Card title={product.name} subtitle={String(product.price)} />;
}
function OrderCard({ order }: { order: Order }) {
return <Card title={order.title} subtitle={String(order.total)} />;
}