Introduction
SOLID is a mnemonic for key design principles that lead to understandable, flexible and maintainable code. Applying these principles to React apps promotes loose coupling, encapsulation and separation of concerns.
In this guide, you’ll learn what each React SOLID principles means and how to put them into practice in your React components and applications. Following these practices will level up your React architecture.
What is SOLID?
SOLID stands for:
- S – Single Responsibility Principle
- O – Open/Closed Principle
- L – Liskov Substitution Principle
- I – Interface Segregation Principle
- D – Dependency Inversion Principle
These five principles encourage building software out of small, simple, reusable parts that work together. Let’s explore each one applied to React.
React SOLID Principles:
React Single Responsibility Principle
The Single Responsibility Principle states:
A component/class should have only one reason to change.
In other words, a component should focus on a single functionality or piece of state.
For example, a Profile component that handles fetching user data, displaying it, and updating settings violates single responsibility. We can split it up:
// ProfileData.js
export default function ProfileData() {
// Fetch user data
}
// ProfileSettings.js
export default function ProfileSettings() {
// UI for updating settings
}
// ProfilePage.js
import ProfileData from './ProfileData';
import ProfileSettings from './ProfileSettings';
function ProfilePage() {
return (
<>
<ProfileData />
<ProfileSettings />
</>
)
}
Now each focuses solely on one task. This isolation leads to more reusable components.
React Open Closed Principle
The Open/Closed Principle states:
A component should be open for extension but closed for modification.
In other words, it should allow adding new features without changing existing source code.
For example, say we have a logger component:
// Bad
function Logger(props) {
if (props.logType === 'file') {
// File logging
} else if (props.logType === 'database') {
// Database logging
}
}
To add a new logger, we have to modify this component. Instead we can use the factory pattern:
// Loggers
class FileLogger {
// logging implementation
}
class DatabaseLogger {
// logging implementation
}
// LoggerFactory.js
export function createLogger(type) {
switch(type) {
case 'file': return new FileLogger();
case 'database': return new DatabaseLogger();
}
}
// Usage
import { createLogger } from './LoggerFactory';
const logger = createLogger('file');
logger.log('hello world');
Now we can create new logger classes and add cases to the factory without touching existing code. This follows open/closed principle.
React Liskov Substitution Principle
This principle states:
Child components should be substitutable for parent components.
In other words, subclasses should conform to the contracts laid out by parent classes.
Violating this principle can lead to brittle class hierarchies.
For example, say we have:
class Vehicle {
travelTo(destination) {
// Vehicle logic
}
}
class Bus extends Vehicle {
travelTo(destination) {
// Throw error if destination is cross-country
}
}
This forces consumers of Bus
to handle errors differently than other Vehicle
types. Instead, Bus
should accept cross-country destinations and handle long trips differently without disrupting the base interface.
Subclasses like Bus
should honor the contracts of parents like Vehicle
.
React Interface Segregation Principle
This principle states:
Classes should not be forced to depend on methods they don’t use.
In other words, if a class only needs a subset of a contract, it’s better to break the contract into multiple smaller contracts.
For example, say we have an EmailHandler
interface:
interface EmailHandler {
sendEmail();
receiveEmail();
encryptEmail();
decryptEmail();
}
A GmailClient
may only care about sendEmail() and receiveEmail(). By segregating the interface into multiple roles, we decouple clients from unnecessary methods:
interface EmailSender {
sendEmail();
}
interface EmailReceiver {
receiveEmail();
}
interface SecureEmail {
encryptEmail();
decryptEmail();
}
class GmailClient implements EmailSender, EmailReceiver {
// Implements only send and receive
}
Now GmailClient
implements only the capabilities it needs without forcing it to provide unrelated features.
React Dependency Inversion Principle
This principle states:
Depend upon abstractions rather than concrete implementations.
In other words, modules should depend only on generic interfaces not concrete types to keep them decoupled.
For example, say we have:
// Bad
class NewsReporter {
constructor(private reporter: NYTimesReporter) {}
public reportNews() {
this.reporter.writeNews();
}
}
// Bound specifically to NYTimesReporter
Instead, we can depend on an abstract reporter interface:
interface NewsReporter {
writeNews();
}
class NYTimesReporter implements NewsReporter {
// implementation
}
class NewsReportingClient {
constructor(private reporter: NewsReporter) {}
// Decoupled through interface
reportNews() {
this.reporter.writeNews();
}
}
Now NewsReportingClient
isn’t coupled to one specific reporter class. This inversion of control is powerful.
Conclusion
Applying the SOLID principles leads to React code that’s:
- Modular – Small components with single purposes
- Encapsulated – Reduce external dependencies
- Reusable – Extract common functionality into utility functions
- Testable – Components depend on abstractions easy to mock/configure
- Maintainable – Isolated components that reduce rippling changes
While not always achievable 100%, striving for React SOLID principles will improve your React architectures. They encourage building apps from many decoupled, interchangeable parts – ideal for scaling UIs.
Frequently Asked Questions
Q: Are React SOLID principles required for all React apps?
A: Not strictly, but they enable building large, sustainable React codebases. Use appropriate principles where they provide high value.
Q: Does following React SOLID principles hurt developer velocity and productivity?
A: Potentially in the short term. But long term, SOLID leads to faster, parallel development and less regressions by isolating components.
Q: When should React components be broken apart?
A: Extract components whenever you need reusable UI (buttons, widgets), logical grouping (sidebars), or decoupled business logic/data.
Q: What is the recommended file structure for React apps with SOLID principles?
A: Group by feature rather than role. Keep related UI, business logic, API clients together rather than separate directories.
Q: Are there any React libraries that help enforce SOLID patterns?
A: Utility libraries like Bit help extract reusable components. Frameworks like Redux enforce uni-directional data flow. But SOLID mostly relies on discipline.