SOLID Principle in React.js & Next.js

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 changinguseUser
.
// 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
Principle | Summary | Example in React/Next |
SRP | One class = One job | Separate data fetching, UI, and logic |
OCP | Extend without modifying | Extend via props/hooks/components |
LSP | Subtypes must behave like their parent | Use interchangeable components |
ISP | Use small, specific interfaces | Keep props/interfaces small and specific |
DIP | Depend on abstractions, not implementations | Inject services via context/hooks |