SOLID Principles in JavaScript and React
1. Single Responsibility Principle (SRP)
A class/component should have only one reason to change.
❌ Bad Example
class UserProfile {
constructor(user) {
this.user = user;
}
saveToDatabase() {
// Database logic here
}
formatUserData() {
// Data formatting logic
}
renderUserProfile() {
// UI rendering logic
}
}
✅ Good Example
// Data Service
class UserService {
saveUser(user) {
// Database logic here
}
}
// Data Formatter
class UserFormatter {
format(user) {
// Data formatting logic
}
}
// React Component
const UserProfile = ({ user }) => {
const userService = new UserService();
const userFormatter = new UserFormatter();
const handleSave = () => {
const formattedData = userFormatter.format(user);
userService.saveUser(formattedData);
};
return (
<div>
<h1>{user.name}</h1>
<button onClick={handleSave}>Save</button>
</div>
);
};
2. Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
❌ Bad Example
class Button {
constructor(type) {
this.type = type;
}
render() {
if (this.type === 'primary') {
return '<button class="primary">Click</button>';
} else if (this.type === 'secondary') {
return '<button class="secondary">Click</button>';
}
// Adding new button types requires modifying existing code
}
}
✅ Good Example
// Abstract base class
class ButtonBase {
getStyles() {
throw new Error('Must implement getStyles');
}
render() {
return <button className={this.getStyles()}>{this.props.children}</button>;
}
}
// Extended button types
class PrimaryButton extends ButtonBase {
getStyles() {
return 'bg-blue-500 text-white';
}
}
class SecondaryButton extends ButtonBase {
getStyles() {
return 'bg-gray-500 text-white';
}
}
// New button types can be added without modifying existing code
class DangerButton extends ButtonBase {
getStyles() {
return 'bg-red-500 text-white';
}
}
3. Liskov Substitution Principle (LSP)
Derived classes must be substitutable for their base classes.
❌ Bad Example
class Bird {
fly() {
return "I can fly!";
}
}
class Penguin extends Bird {
fly() {
throw new Error("I can't fly!"); // Violates LSP
}
}
✅ Good Example
class Bird {
move() {
throw new Error('Must implement move');
}
}
class FlyingBird extends Bird {
move() {
return "I can fly!";
}
}
class SwimmingBird extends Bird {
move() {
return "I can swim!";
}
}
// React Example
const BirdComponent = ({ bird }) => {
return <div>{bird.move()}</div>;
};
4. Interface Segregation Principle (ISP)
A client should not be forced to depend on interfaces it doesn't use.
❌ Bad Example
class UserActions {
login(user) { /* ... */ }
logout() { /* ... */ }
updateProfile(data) { /* ... */ }
deleteAccount() { /* ... */ }
}
// Component forced to implement all methods
class UserProfile extends React.Component {
userActions = new UserActions();
// Must include all methods even if not needed
}
✅ Good Example
// Separate interfaces
class AuthService {
login(user) { /* ... */ }
logout() { /* ... */ }
}
class ProfileService {
updateProfile(data) { /* ... */ }
}
class AccountService {
deleteAccount() { /* ... */ }
}
// Components can use only what they need
const LoginComponent = () => {
const authService = new AuthService();
return <button onClick={() => authService.login()}>Login</button>;
};
const ProfileEditor = () => {
const profileService = new ProfileService();
return <button onClick={() => profileService.updateProfile()}>Update</button>;
};
5. Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
❌ Bad Example
class NotificationService {
constructor() {
this.emailSender = new EmailSender(); // Direct dependency
}
sendNotification(message) {
this.emailSender.send(message);
}
}
✅ Good Example
// Abstract interface
class NotificationSender {
send(message) {
throw new Error('Must implement send');
}
}
// Concrete implementations
class EmailSender extends NotificationSender {
send(message) {
// Send email
}
}
class SMSSender extends NotificationSender {
send(message) {
// Send SMS
}
}
// React Component using dependency injection
const NotificationComponent = ({ notificationSender }) => {
const sendNotification = (message) => {
notificationSender.send(message);
};
return (
<button onClick={() => sendNotification('Hello!')}>
Send Notification
</button>
);
};
// Usage
const App = () => {
const emailSender = new EmailSender();
const smsSender = new SMSSender();
return (
<>
<NotificationComponent notificationSender={emailSender} />
<NotificationComponent notificationSender={smsSender} />
</>
);
};
Practical React Example Combining All Principles
// 1. Single Responsibility Principle
const useUserData = () => {
// Data management hook
};
const useUserValidation = () => {
// Validation logic hook
};
// 2. Open/Closed Principle
const FormField = ({ validator, renderer, ...props }) => {
// Extensible form field component
};
// 3. Liskov Substitution Principle
const Input = ({ type = 'text', ...props }) => {
// Base input component
};
const EmailInput = (props) => <Input type="email" {...props} />;
const PasswordInput = (props) => <Input type="password" {...props} />;
// 4. Interface Segregation Principle
const useAuth = () => {
// Authentication hook
};
const useProfile = () => {
// Profile management hook
};
// 5. Dependency Inversion Principle
const UserForm = ({ onSubmit, validation, transformation }) => {
const handleSubmit = async (data) => {
const isValid = await validation(data);
if (isValid) {
const transformed = transformation(data);
onSubmit(transformed);
}
};
return <form onSubmit={handleSubmit}>{/* Form fields */}</form>;
};
// Complete Example
const UserProfile = () => {
const { user, updateUser } = useUserData();
const { validateUser } = useUserValidation();
const { transformUserData } = useUserTransformation();
return (
<UserForm
onSubmit={updateUser}
validation={validateUser}
transformation={transformUserData}
>
<EmailInput name="email" />
<PasswordInput name="password" />
</UserForm>
);
};
Benefits of Following SOLID Principles
-
Maintainability
- Easier to understand and modify code
- Reduced technical debt
- Better organization of components
-
Testability
- Isolated components are easier to test
- Clear dependencies make mocking simpler
- Better unit test coverage
-
Flexibility
- Easier to extend functionality
- Better adaptation to changing requirements
- Improved reusability of components
-
Scalability
- Better handling of growing complexity
- Easier team collaboration
- More sustainable codebase