Anoopfolio
HomeBlogsSOLID Principle...

SOLID Principle in React.js & Next.js

https://cdn.sanity.io/images/u35myvmc/production/4d502af5af43ef1e6d41d5c6456bd0a50aaec8a9-2816x1536.png

The SOLID principles are a set of five design principles in object-oriented programming, introduced by Robert C. Martin (Uncle Bob), that help make software designs more understandable, flexible, and maintainable.

Single Responsibility Principle (SRP)

  • A class should only do one thing — one responsibility.
  • Helps in keeping code modular and easier to test.
One class should have one, and only one, reason to change.
// Bad: One class doing two things (auth + logging)
class UserManager {
  login() {}
  logToFile() {}
}

// Good: Split responsibilities
class AuthService {
  login() {}
}
class Logger {
  logToFile() {}
}

SRP in React

  • 🔴 Bad (Too many responsibilities)
// Handles fetching, state, and UI in one component
const UserProfile = () => {
  const [user, setUser] = useState(null)

  useEffect(() => {
    fetch('/api/user')
      .then(res => res.json())
      .then(setUser)
  }, [])

  return <div>{user?.name}</div>
}
  • 🟢 Good (Split into services and UI)
// services/userService.ts
export const fetchUser = async () => {
  const res = await fetch('/api/user')
  return res.json()
}
// components/UserProfile.tsx
import { useUser } from '@/hooks/useUser'

const UserProfile = () => {
  const user = useUser()
  return <div>{user?.name}</div>
}
// hooks/useUser.ts
import { useEffect, useState } from 'react'
import { fetchUser } from '@/services/userService'

export const useUser = () => {
  const [user, setUser] = useState<any>(null)
  useEffect(() => {
    fetchUser().then(setUser)
  }, [])
  return user
}

Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modification.
  • You should be able to add new functionality without changing existing code.
  • Achieved using abstraction (e.g., interfaces or base classes).
interface Shape {
  getArea(): number;
}

class Circle implements Shape {
  constructor(public radius: number) {}
  getArea() {
    return Math.PI * this.radius * this.radius;
  }
}

class Square implements Shape {
  constructor(public side: number) {}
  getArea() {
    return this.side * this.side;
  }
}

// We can add more shapes without modifying existing ones

OCP in React

  • 🟢 Good: Extend UI functionality with props or components instead of editing core logic No need to modify Button to add new styles — just extend with new props.
// Button.tsx - open for extension via props
type ButtonProps = {
  label: string
  onClick: () => void
  variant?: 'primary' | 'secondary'
}

export const Button = ({ label, onClick, variant = 'primary' }: ButtonProps) => {
  const className = variant === 'primary' ? 'bg-blue-500' : 'bg-gray-300'
  return <button className={className} onClick={onClick}>{label}</button>
}

Liskov Substitution Principle (LSP)

Subclasses should be substitutable for their base classes.
  • A child class must not break the behavior of the parent class.
  • Clients using the base class should not notice any difference if a subclass is used instead.

LSP in React

🟢 Good: Create base components/interfaces that can be replaced with extended ones

// components/Notification.tsx
type NotificationProps = {
  message: string
}

export const Notification = ({ message }: NotificationProps) => (
  <div className="p-2 bg-green-100">{message}</div>
)


// components/ErrorNotification.tsx
export const ErrorNotification = ({ message }: NotificationProps) => (
  <div className="p-2 bg-red-100">{message}</div>
)

You can substitute <Notification /> with <ErrorNotification /> without breaking anything — same props, consistent behavior.

Interface Segregation Principle (ISP)

Clients should not be forced to depend on methods they do not use.
  • Break large interfaces into smaller, more specific ones.
  • Helps in avoiding "fat" interfaces.
interface Printer {
  print(): void;
}

interface Scanner {
  scan(): void;
}

class MultiFunctionPrinter implements Printer, Scanner {
  print() {}
  scan() {}
}

ISP in React

  • 🔴 Bad - Avoid creating "fat" props or context:
type DashboardProps = {
  user: any
  analytics: any
  settings: any
  fetchUsers: () => void
  updateSettings: () => void
}
  • 🟢 Good - Split into smaller interfaces:
type UserProps = {
  user: any
}

type AnalyticsProps = {
  analytics: any
}

type SettingsProps = {
  settings: any
  updateSettings: () => void
}

Dependency Inversion Principle (DIP)

Depend on abstractions, not on concretions.
  • High-level modules shouldn't depend on low-level modules. Both should depend on abstractions.
  • Makes code more flexible and testable.
interface Database {
  save(data: string): void;
}

class MySQLDatabase implements Database {
  save(data: string) {}
}

class DataService {
  constructor(private db: Database) {}

  saveData(data: string) {
    this.db.save(data);
  }
}

DCP in React

  • 🟢 Good: Use abstraction (hooks, interfaces, context) instead of direct implementation You can now swap DefaultApiClient with a mock or test implementation without changing useUser.
// services/apiClient.ts
export interface ApiClient {
  fetchUser(): Promise<any>
}

export class DefaultApiClient implements ApiClient {
  async fetchUser() {
    const res = await fetch('/api/user')
    return res.json()
  }
}
// context/ApiClientContext.tsx
import { createContext, useContext } from 'react'
import { ApiClient, DefaultApiClient } from '@/services/apiClient'

const ApiClientContext = createContext<ApiClient>(new DefaultApiClient())

export const useApiClient = () => useContext(ApiClientContext)
// hooks/useUser.ts
import { useEffect, useState } from 'react'
import { useApiClient } from '@/context/ApiClientContext'

export const useUser = () => {
  const apiClient = useApiClient()
  const [user, setUser] = useState(null)

  useEffect(() => {
    apiClient.fetchUser().then(setUser)
  }, [apiClient])

  return user
}

Summary Table

PrincipleSummaryExample in React/Next
SRPOne class = One jobSeparate data fetching, UI, and logic
OCPExtend without modifyingExtend via props/hooks/components
LSPSubtypes must behave like their parentUse interchangeable components
ISPUse small, specific interfacesKeep props/interfaces small and specific
DIPDepend on abstractions, not implementationsInject services via context/hooks