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