Skip to content

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)} />;
}