Introduction: Revolutionizing Front-End Development with SOLID Principles
The Critical Role of SOLID in User Experience:
Front-end development serves as the bridge between users and software functionality. In this realm, the visual and interactive aspects of web applications play a pivotal role in defining user experience. Implementing SOLID design principles in front-end development is not just about writing good code; it's about crafting an environment where functionality and user experience coexist harmoniously. These principles guide developers in creating a more structured, scalable, and maintainable codebase, which directly translates to a smoother, more engaging user interface.
Adapting SOLID for Front-End Challenges:
While traditionally associated with back-end architecture, SOLID principles are equally vital in the front-end landscape. The dynamic nature of front-end technologies, with frequent updates and evolving frameworks, demands a robust set of guidelines. SOLID principles offer this stability, providing a foundation upon which developers can build adaptable and resilient web applications.
Single Responsibility Principle (SRP): A Pillar of Modular Design
SRP in Front-End Architecture:
The Single Responsibility Principle asserts that each module or component should have one, and only one, reason to change. In the context of front-end development, this translates to creating components with a singular focus. For instance, a navigation component should not handle user authentication. This separation enhances the maintainability and scalability of the application.
Vanilla JavaScript Example
Violating SRP:
// This class does too much: handles data, UI updates, and API calls
class UserDashboard {
constructor(userId) {
this.userId = userId;
this.userData = null;
}
async fetchUserData() {
const response = await fetch(`/api/users/${this.userId}`);
this.userData = await response.json();
}
validateUserData() {
return this.userData && this.userData.email.includes('@');
}
render() {
const container = document.getElementById('dashboard');
container.innerHTML = `
<h1>${this.userData.name}</h1>
<p>${this.userData.email}</p>
`;
}
saveToLocalStorage() {
localStorage.setItem('user', JSON.stringify(this.userData));
}
}
Following SRP:
// Separate concerns into focused classes
// 1. Data fetching service
class UserApiService {
async fetchUser(userId) {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
}
// 2. Data validation
class UserValidator {
validate(userData) {
return userData &&
userData.email &&
userData.email.includes('@');
}
}
// 3. Storage management
class UserStorage {
save(userData) {
localStorage.setItem('user', JSON.stringify(userData));
}
load() {
const data = localStorage.getItem('user');
return data ? JSON.parse(data) : null;
}
}
// 4. UI rendering
class UserDashboardView {
constructor(containerId) {
this.container = document.getElementById(containerId);
}
render(userData) {
this.container.innerHTML = `
<div class="user-profile">
<h1>${userData.name}</h1>
<p>${userData.email}</p>
</div>
`;
}
}
// 5. Controller to coordinate
class UserDashboardController {
constructor(userId) {
this.apiService = new UserApiService();
this.validator = new UserValidator();
this.storage = new UserStorage();
this.view = new UserDashboardView('dashboard');
this.userId = userId;
}
async initialize() {
const userData = await this.apiService.fetchUser(this.userId);
if (this.validator.validate(userData)) {
this.storage.save(userData);
this.view.render(userData);
}
}
}
React.js Example
Violating SRP:
// Component doing too much
function UserProfile() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [theme, setTheme] = useState('light');
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(setUser);
fetch('/api/posts')
.then(res => res.json())
.then(setPosts);
}, []);
const handleThemeToggle = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
document.body.className = theme;
};
return (
<div>
<button onClick={handleThemeToggle}>Toggle Theme</button>
<h1>{user?.name}</h1>
<img src={user?.avatar} alt="avatar" />
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
Following SRP:
// Custom hooks for data fetching
function useUser() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(setUser)
.finally(() => setLoading(false));
}, []);
return { user, loading };
}
function usePosts() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/posts')
.then(res => res.json())
.then(setPosts)
.finally(() => setLoading(false));
}, []);
return { posts, loading };
}
// Separate components with single responsibilities
function UserAvatar({ src, alt }) {
return <img src={src} alt={alt} className="user-avatar" />;
}
function UserHeader({ name }) {
return <h1 className="user-name">{name}</h1>;
}
function PostList({ posts }) {
return (
<ul className="post-list">
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
function ThemeToggle({ theme, onToggle }) {
return (
<button onClick={onToggle}>
{theme === 'light' ? '🌙' : '☀️'} Toggle Theme
</button>
);
}
// Main component just orchestrates
function UserProfile() {
const { user, loading: userLoading } = useUser();
const { posts, loading: postsLoading } = usePosts();
const [theme, setTheme] = useState('light');
const handleThemeToggle = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
if (userLoading) return <div>Loading...</div>;
return (
<div className={`profile ${theme}`}>
<ThemeToggle theme={theme} onToggle={handleThemeToggle} />
<UserHeader name={user.name} />
<UserAvatar src={user.avatar} alt={user.name} />
{!postsLoading && <PostList posts={posts} />}
</div>
);
}
Open/Closed Principle (OCP): Building Extensible Interfaces
Extensibility Without Modification:
The Open/Closed Principle dictates that software entities should be open for extension but closed for modification. For front-end developers, this means designing components that can adapt to new requirements without altering existing code.
Vanilla JavaScript Example
Violating OCP:
// Adding new notification types requires modifying the class
class NotificationManager {
show(type, message) {
const container = document.getElementById('notifications');
if (type === 'success') {
container.innerHTML = `<div class="success">${message}</div>`;
} else if (type === 'error') {
container.innerHTML = `<div class="error">${message}</div>`;
} else if (type === 'warning') {
container.innerHTML = `<div class="warning">${message}</div>`;
}
// Adding 'info' type requires modifying this method
}
}
Following OCP:
// Base notification class
class Notification {
constructor(message) {
this.message = message;
}
render() {
throw new Error('render() must be implemented');
}
show() {
const container = document.getElementById('notifications');
container.appendChild(this.render());
}
}
// Extend without modifying the base class
class SuccessNotification extends Notification {
render() {
const el = document.createElement('div');
el.className = 'notification success';
el.textContent = `✓ ${this.message}`;
return el;
}
}
class ErrorNotification extends Notification {
render() {
const el = document.createElement('div');
el.className = 'notification error';
el.textContent = `✗ ${this.message}`;
return el;
}
}
class WarningNotification extends Notification {
render() {
const el = document.createElement('div');
el.className = 'notification warning';
el.textContent = `⚠ ${this.message}`;
return el;
}
}
// Easy to add new types without modifying existing code
class InfoNotification extends Notification {
render() {
const el = document.createElement('div');
el.className = 'notification info';
el.textContent = `ℹ ${this.message}`;
return el;
}
}
// Usage
const notification = new SuccessNotification('Profile updated!');
notification.show();
Alternative Pattern - Strategy Pattern:
class NotificationRenderer {
constructor() {
this.strategies = new Map();
}
register(type, renderFn) {
this.strategies.set(type, renderFn);
}
render(type, message) {
const renderFn = this.strategies.get(type);
if (!renderFn) {
throw new Error(`Unknown notification type: ${type}`);
}
return renderFn(message);
}
}
// Setup
const renderer = new NotificationRenderer();
renderer.register('success', (msg) => {
const el = document.createElement('div');
el.className = 'notification success';
el.textContent = `✓ ${msg}`;
return el;
});
renderer.register('error', (msg) => {
const el = document.createElement('div');
el.className = 'notification error';
el.textContent = `✗ ${msg}`;
return el;
});
// Extend by registering new strategies
renderer.register('info', (msg) => {
const el = document.createElement('div');
el.className = 'notification info';
el.textContent = `ℹ ${msg}`;
return el;
});
React.js Example
Violating OCP:
function Button({ type, onClick, children }) {
let className = 'btn';
if (type === 'primary') {
className += ' btn-primary';
} else if (type === 'secondary') {
className += ' btn-secondary';
} else if (type === 'danger') {
className += ' btn-danger';
}
// Adding new button types requires modifying this component
return (
<button className={className} onClick={onClick}>
{children}
</button>
);
}
Following OCP:
// Base button component
function Button({ className = '', onClick, children, ...props }) {
return (
<button
className={`btn ${className}`}
onClick={onClick}
{...props}
>
{children}
</button>
);
}
// Extend with specific button types
function PrimaryButton({ children, ...props }) {
return (
<Button className="btn-primary" {...props}>
{children}
</Button>
);
}
function SecondaryButton({ children, ...props }) {
return (
<Button className="btn-secondary" {...props}>
{children}
</Button>
);
}
function DangerButton({ children, ...props }) {
return (
<Button className="btn-danger" {...props}>
{children}
</Button>
);
}
// Easy to add new variations
function IconButton({ icon, children, ...props }) {
return (
<Button className="btn-icon" {...props}>
<span className="icon">{icon}</span>
{children}
</Button>
);
}
// Higher-order component pattern
function withLoadingState(Component) {
return function ButtonWithLoading({ loading, children, ...props }) {
return (
<Component disabled={loading} {...props}>
{loading ? 'Loading...' : children}
</Component>
);
};
}
const PrimaryButtonWithLoading = withLoadingState(PrimaryButton);
Liskov Substitution Principle (LSP): Ensuring Interchangeable Components
Seamless Substitution in Component Hierarchies:
The Liskov Substitution Principle ensures that objects of a superclass can be replaced with objects of its subclasses without affecting the application's correctness. In front-end frameworks like React or Angular, this principle advocates for designing components and services that are interchangeable within their hierarchy.
Vanilla JavaScript Example
Violating LSP:
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width; // Violates LSP: changes unexpected behavior
}
setHeight(height) {
this.width = height; // Violates LSP: changes unexpected behavior
this.height = height;
}
}
// This breaks when using Square
function resizeRectangle(rectangle) {
rectangle.setWidth(5);
rectangle.setHeight(10);
console.log(rectangle.getArea()); // Expected: 50, but Square gives 100
}
Following LSP:
// Abstract base for shapes
class Shape {
getArea() {
throw new Error('getArea() must be implemented');
}
getPerimeter() {
throw new Error('getPerimeter() must be implemented');
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
setDimensions(width, height) {
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
getPerimeter() {
return 2 * (this.width + this.height);
}
}
class Square extends Shape {
constructor(side) {
super();
this.side = side;
}
setSide(side) {
this.side = side;
}
getArea() {
return this.side * this.side;
}
getPerimeter() {
return 4 * this.side;
}
}
// Works with any Shape
function displayShapeInfo(shape) {
console.log(`Area: ${shape.getArea()}`);
console.log(`Perimeter: ${shape.getPerimeter()}`);
}
const rect = new Rectangle(5, 10);
const square = new Square(5);
displayShapeInfo(rect); // Works correctly
displayShapeInfo(square); // Works correctly
Front-End Specific Example:
// Input field base class
class InputField {
constructor(containerId, name) {
this.container = document.getElementById(containerId);
this.name = name;
this.value = '';
}
setValue(value) {
this.value = value;
}
getValue() {
return this.value;
}
render() {
throw new Error('render() must be implemented');
}
validate() {
return true; // Base validation
}
}
class TextInput extends InputField {
render() {
this.container.innerHTML = `
<input
type="text"
name="${this.name}"
value="${this.value}"
class="input-field"
/>
`;
this.bindEvents();
}
bindEvents() {
const input = this.container.querySelector('input');
input.addEventListener('input', (e) => {
this.setValue(e.target.value);
});
}
}
class EmailInput extends InputField {
render() {
this.container.innerHTML = `
<input
type="email"
name="${this.name}"
value="${this.value}"
class="input-field"
/>
`;
this.bindEvents();
}
bindEvents() {
const input = this.container.querySelector('input');
input.addEventListener('input', (e) => {
this.setValue(e.target.value);
});
}
validate() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(this.value);
}
}
// Form can work with any InputField subclass
class Form {
constructor(fields) {
this.fields = fields;
}
validateAll() {
return this.fields.every(field => field.validate());
}
getValues() {
return this.fields.reduce((acc, field) => {
acc[field.name] = field.getValue();
return acc;
}, {});
}
}
// Usage - all input types are interchangeable
const form = new Form([
new TextInput('name-container', 'name'),
new EmailInput('email-container', 'email'),
new TextInput('message-container', 'message')
]);
React.js Example
Violating LSP:
function BaseModal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return (
<div className="modal">
<button onClick={onClose}>Close</button>
{children}
</div>
);
}
function AlertModal({ isOpen, children }) {
// Violates LSP: removed onClose, can't be used where BaseModal is expected
if (!isOpen) return null;
return (
<div className="modal alert">
{children}
</div>
);
}
Following LSP:
// Base modal with consistent interface
function Modal({ isOpen, onClose, children, className = '' }) {
if (!isOpen) return null;
return (
<div className={`modal ${className}`}>
<div className="modal-content">
{children}
</div>
{onClose && (
<button className="modal-close" onClick={onClose}>
×
</button>
)}
</div>
);
}
// All variants maintain the same interface
function ConfirmModal({ isOpen, onClose, onConfirm, message, children }) {
return (
<Modal isOpen={isOpen} onClose={onClose} className="modal-confirm">
<p>{message}</p>
{children}
<div className="modal-actions">
<button onClick={onClose}>Cancel</button>
<button onClick={onConfirm}>Confirm</button>
</div>
</Modal>
);
}
function AlertModal({ isOpen, onClose, message, children }) {
return (
<Modal isOpen={isOpen} onClose={onClose} className="modal-alert">
<p>{message}</p>
{children}
<button onClick={onClose}>OK</button>
</Modal>
);
}
function FormModal({ isOpen, onClose, onSubmit, children }) {
return (
<Modal isOpen={isOpen} onClose={onClose} className="modal-form">
<form onSubmit={onSubmit}>
{children}
<div className="modal-actions">
<button type="button" onClick={onClose}>Cancel</button>
<button type="submit">Submit</button>
</div>
</form>
</Modal>
);
}
// Modal manager works with any modal type
function ModalManager({ activeModal, modalProps, onClose }) {
const modalTypes = {
confirm: ConfirmModal,
alert: AlertModal,
form: FormModal
};
const ModalComponent = modalTypes[activeModal];
if (!ModalComponent) return null;
return (
<ModalComponent
isOpen={true}
onClose={onClose}
{...modalProps}
/>
);
}
Interface Segregation Principle (ISP): Tailoring Component Interfaces
Focused and Efficient Interfaces:
The Interface Segregation Principle promotes the creation of narrow, specific interfaces. In front-end development, this principle can be applied by designing components and services that expose only the necessary methods and properties, reducing the complexity and improving usability.
Vanilla JavaScript Example
Violating ISP:
// Bloated interface forces implementations to have methods they don't need
class MediaPlayer {
play() {
throw new Error('Not implemented');
}
pause() {
throw new Error('Not implemented');
}
stop() {
throw new Error('Not implemented');
}
record() {
throw new Error('Not implemented');
}
stream() {
throw new Error('Not implemented');
}
download() {
throw new Error('Not implemented');
}
}
// VideoPlayer doesn't need record() but must implement it
class VideoPlayer extends MediaPlayer {
play() { console.log('Playing video'); }
pause() { console.log('Paused'); }
stop() { console.log('Stopped'); }
record() { throw new Error('Video player cannot record'); }
stream() { console.log('Streaming'); }
download() { console.log('Downloading'); }
}
// AudioRecorder doesn't need download() or stream()
class AudioRecorder extends MediaPlayer {
play() { console.log('Playing audio'); }
pause() { console.log('Paused'); }
stop() { console.log('Stopped'); }
record() { console.log('Recording'); }
stream() { throw new Error('Audio recorder cannot stream'); }
download() { throw new Error('Audio recorder cannot download'); }
}
Following ISP:
// Segregated interfaces
class Playable {
play() {
throw new Error('Not implemented');
}
pause() {
throw new Error('Not implemented');
}
stop() {
throw new Error('Not implemented');
}
}
class Recordable {
startRecording() {
throw new Error('Not implemented');
}
stopRecording() {
throw new Error('Not implemented');
}
}
class Streamable {
startStream() {
throw new Error('Not implemented');
}
stopStream() {
throw new Error('Not implemented');
}
}
class Downloadable {
download() {
throw new Error('Not implemented');
}
}
// Compose only needed interfaces
class VideoPlayer extends Playable {
constructor() {
super();
this.streamable = new VideoStreamer();
this.downloadable = new VideoDownloader();
}
play() { console.log('Playing video'); }
pause() { console.log('Paused'); }
stop() { console.log('Stopped'); }
startStream() { return this.streamable.startStream(); }
download() { return this.downloadable.download(); }
}
class VideoStreamer extends Streamable {
startStream() { console.log('Streaming video'); }
stopStream() { console.log('Stream stopped'); }
}
class VideoDownloader extends Downloadable {
download() { console.log('Downloading video'); }
}
class AudioRecorder {
constructor() {
this.playable = new AudioPlayback();
this.recordable = new AudioRecording();
}
play() { return this.playable.play(); }
pause() { return this.playable.pause(); }
stop() { return this.playable.stop(); }
startRecording() { return this.recordable.startRecording(); }
stopRecording() { return this.recordable.stopRecording(); }
}
class AudioPlayback extends Playable {
play() { console.log('Playing audio'); }
pause() { console.log('Paused'); }
stop() { console.log('Stopped'); }
}
class AudioRecording extends Recordable {
startRecording() { console.log('Recording audio'); }
stopRecording() { console.log('Recording stopped'); }
}
API Service Example:
// Fat interface
class UserApi {
getUser() {}
updateUser() {}
deleteUser() {}
getUserPosts() {}
getUserComments() {}
getUserFriends() {}
getUserSettings() {}
updateUserSettings() {}
getUserNotifications() {}
}
// Segregated interfaces
class UserProfileApi {
getUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json());
}
updateUser(id, data) {
return fetch(`/api/users/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
}).then(r => r.json());
}
}
class UserContentApi {
getUserPosts(userId) {
return fetch(`/api/users/${userId}/posts`).then(r => r.json());
}
getUserComments(userId) {
return fetch(`/api/users/${userId}/comments`).then(r => r.json());
}
}
class UserSettingsApi {
getSettings(userId) {
return fetch(`/api/users/${userId}/settings`).then(r => r.json());
}
updateSettings(userId, settings) {
return fetch(`/api/users/${userId}/settings`, {
method: 'PUT',
body: JSON.stringify(settings)
}).then(r => r.json());
}
}
class UserSocialApi {
getFriends(userId) {
return fetch(`/api/users/${userId}/friends`).then(r => r.json());
}
getNotifications(userId) {
return fetch(`/api/users/${userId}/notifications`).then(r => r.json());
}
}
// Components use only what they need
class ProfileComponent {
constructor() {
this.api = new UserProfileApi(); // Only profile methods
}
async loadProfile(userId) {
const user = await this.api.getUser(userId);
this.render(user);
}
}
class SettingsComponent {
constructor() {
this.api = new UserSettingsApi(); // Only settings methods
}
async loadSettings(userId) {
const settings = await this.api.getSettings(userId);
this.render(settings);
}
}
React.js Example
Violating ISP:
// Component forces consumers to handle all props
function DataTable({
data,
onSort,
onFilter,
onPaginate,
onExport,
onDelete,
onEdit,
onRefresh,
onSelect
}) {
// All features in one component
return (
<div>
{/* Complex implementation */}
</div>
);
}
// Consumer forced to handle everything even if not needed
function SimpleUserList() {
return (
<DataTable
data={users}
onSort={handleSort}
onFilter={handleFilter}
onPaginate={() => {}} // Don't need pagination
onExport={() => {}} // Don't need export
onDelete={() => {}} // Don't need delete
onEdit={() => {}} // Don't need edit
onRefresh={() => {}} // Don't need refresh
onSelect={() => {}} // Don't need select
/>
);
}
Following ISP:
// Base table with minimal interface
function Table({ data, columns, renderRow }) {
return (
<table>
<thead>
<tr>
{columns.map(col => (
<th key={col.key}>{col.label}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, idx) => renderRow(row, idx))}
</tbody>
</table>
);
}
// Composable features
function withSorting(TableComponent) {
return function SortableTable({ data, columns, onSort, ...props }) {
const [sortKey, setSortKey] = useState(null);
const [sortOrder, setSortOrder] = useState('asc');
const sortedData = sortKey
? [...data].sort((a, b) => {
const order = sortOrder === 'asc' ? 1 : -1;
return a[sortKey] > b[sortKey] ? order : -order;
})
: data;
const enhancedColumns = columns.map(col => ({
...col,
onClick: () => {
setSortKey(col.key);
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
onSort?.(col.key, sortOrder);
}
}));
return <TableComponent data={sortedData} columns={enhancedColumns} {...props} />;
};
}
function withPagination(TableComponent) {
return function PaginatedTable({ data, pageSize = 10, ...props }) {
const [page, setPage] = useState(0);
const paginatedData = data.slice(
page * pageSize,
(page + 1) * pageSize
);
return (
<div>
<TableComponent data={paginatedData} {...props} />
<div className="pagination">
<button onClick={() => setPage(p => Math.max(0, p - 1))}>
Previous
</button>
<span>Page {page + 1}</span>
<button onClick={() => setPage(p => p + 1)}>
Next
</button>
</div>
</div>
);
};
}
function withSelection(TableComponent) {
return function SelectableTable({ data, onSelectionChange, ...props }) {
const [selected, setSelected] = useState(new Set());
const toggleSelection = (id) => {
const newSelected = new Set(selected);
if (newSelected.has(id)) {
newSelected.delete(id);
} else {
newSelected.add(id);
}
setSelected(newSelected);
onSelectionChange?.(Array.from(newSelected));
};
const enhancedRenderRow = (row, idx) => (
<tr key={idx}>
<td>
<input
type="checkbox"
checked={selected.has(row.id)}
onChange={() => toggleSelection(row.id)}
/>
</td>
{props.renderRow(row, idx)}
</tr>
);
return <TableComponent data={data} renderRow={enhancedRenderRow} {...props} />;
};
}
// Compose only needed features
const SortableTable = withSorting(Table);
const PaginatedTable = withPagination(Table);
const SortablePaginatedTable = withPagination(withSorting(Table));
const SelectableSortableTable = withSelection(withSorting(Table));
// Usage - each component uses only what it needs
function SimpleUserList() {
return (
<Table
data={users}
columns={[
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' }
]}
renderRow={(user) => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.email}</td>
</tr>
)}
/>
);
}
function AdvancedUserList() {
return (
<SortablePaginatedTable
data={users}
pageSize={20}
columns={[
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' }
]}
onSort={(key, order) => console.log('Sort:', key, order)}
renderRow={(user) => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{user.role}</td>
</tr>
)}
/>
);
}
Hook-Based ISP Example:
// One hook doing everything
function useUserManagement() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [friends, setFriends] = useState([]);
const [settings, setSettings] = useState({});
// Forces all consumers to handle all features
return { user, posts, friends, settings, setUser, setPosts, setFriends, setSettings };
}
// Segregated hooks
function useUser(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser)
.finally(() => setLoading(false));
}, [userId]);
return { user, loading };
}
function useUserPosts(userId) {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}/posts`)
.then(r => r.json())
.then(setPosts)
.finally(() => setLoading(false));
}, [userId]);
return { posts, loading };
}
function useUserSettings(userId) {
const [settings, setSettings] = useState({});
const [loading, setLoading] = useState(true);
const updateSettings = async (newSettings) => {
await fetch(`/api/users/${userId}/settings`, {
method: 'PUT',
body: JSON.stringify(newSettings)
});
setSettings(newSettings);
};
useEffect(() => {
fetch(`/api/users/${userId}/settings`)
.then(r => r.json())
.then(setSettings)
.finally(() => setLoading(false));
}, [userId]);
return { settings, updateSettings, loading };
}
// Components use only what they need
function UserProfile({ userId }) {
const { user, loading } = useUser(userId); // Only user data
if (loading) return <div>Loading...</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
function UserDashboard({ userId }) {
const { user } = useUser(userId);
const { posts } = useUserPosts(userId);
const { settings, updateSettings } = useUserSettings(userId);
// Component has access to all features it needs
return (
<div>
<h1>{user?.name}'s Dashboard</h1>
<PostList posts={posts} />
<Settings settings={settings} onUpdate={updateSettings} />
</div>
);
}
Dependency Inversion Principle (DIP): Abstracting Component Dependencies
High-Level Abstractions for Flexibility:
The Dependency Inversion Principle emphasizes that high-level modules should not depend on low-level modules, but on abstractions. This principle is crucial in front-end development for creating loosely coupled components that can be easily tested and maintained.
Vanilla JavaScript Example
Violating DIP:
// High-level module depends directly on low-level implementation
class UserDashboard {
constructor() {
// Tightly coupled to specific implementations
this.api = new XMLHttpRequest();
this.storage = window.localStorage;
}
async loadUser(userId) {
// Direct dependency on XMLHttpRequest
this.api.open('GET', `/api/users/${userId}`);
this.api.send();
this.api.onload = () => {
const user = JSON.parse(this.api.responseText);
// Direct dependency on localStorage
this.storage.setItem('user', JSON.stringify(user));
this.render(user);
};
}
render(user) {
document.getElementById('dashboard').innerHTML = `
<h1>${user.name}</h1>
`;
}
}
Following DIP:
// Abstract interfaces
class HttpClient {
get(url) {
throw new Error('get() must be implemented');
}
post(url, data) {
throw new Error('post() must be implemented');
}
}
class StorageProvider {
get(key) {
throw new Error('get() must be implemented');
}
set(key, value) {
throw new Error('set() must be implemented');
}
remove(key) {
throw new Error('remove() must be implemented');
}
}
// Concrete implementations
class FetchHttpClient extends HttpClient {
async get(url) {
const response = await fetch(url);
return response.json();
}
async post(url, data) {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json();
}
}
class LocalStorageProvider extends StorageProvider {
get(key) {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : null;
}
set(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
remove(key) {
localStorage.removeItem(key);
}
}
class SessionStorageProvider extends StorageProvider {
get(key) {
const value = sessionStorage.getItem(key);
return value ? JSON.parse(value) : null;
}
set(key, value) {
sessionStorage.setItem(key, JSON.stringify(value));
}
remove(key) {
sessionStorage.removeItem(key);
}
}
class MemoryStorageProvider extends StorageProvider {
constructor() {
super();
this.storage = new Map();
}
get(key) {
return this.storage.get(key);
}
set(key, value) {
this.storage.set(key, value);
}
remove(key) {
this.storage.delete(key);
}
}
// High-level module depends on abstractions
class UserDashboard {
constructor(httpClient, storage) {
// Dependencies injected through constructor
this.httpClient = httpClient;
this.storage = storage;
}
async loadUser(userId) {
try {
const user = await this.httpClient.get(`/api/users/${userId}`);
this.storage.set('user', user);
this.render(user);
} catch (error) {
console.error('Failed to load user:', error);
}
}
render(user) {
document.getElementById('dashboard').innerHTML = `
<div class="user-dashboard">
<h1>${user.name}</h1>
<p>${user.email}</p>
</div>
`;
}
}
// Easy to swap implementations
const dashboard1 = new UserDashboard(
new FetchHttpClient(),
new LocalStorageProvider()
);
const dashboard2 = new UserDashboard(
new FetchHttpClient(),
new SessionStorageProvider()
);
// Easy to test with mock implementations
class MockHttpClient extends HttpClient {
constructor(mockData) {
super();
this.mockData = mockData;
}
async get(url) {
return Promise.resolve(this.mockData);
}
async post(url, data) {
return Promise.resolve({ success: true });
}
}
const testDashboard = new UserDashboard(
new MockHttpClient({ id: 1, name: 'Test User', email: 'test@example.com' }),
new MemoryStorageProvider()
);
Real-World Example - Form Validation:
// Abstract validator interface
class Validator {
validate(value) {
throw new Error('validate() must be implemented');
}
getMessage() {
throw new Error('getMessage() must be implemented');
}
}
// Concrete validators
class EmailValidator extends Validator {
validate(value) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
getMessage() {
return 'Please enter a valid email address';
}
}
class RequiredValidator extends Validator {
validate(value) {
return value !== null && value !== undefined && value.trim() !== '';
}
getMessage() {
return 'This field is required';
}
}
class MinLengthValidator extends Validator {
constructor(minLength) {
super();
this.minLength = minLength;
}
validate(value) {
return value && value.length >= this.minLength;
}
getMessage() {
return `Minimum length is ${this.minLength} characters`;
}
}
// Form field depends on abstraction
class FormField {
constructor(name, validators = []) {
this.name = name;
this.validators = validators; // Array of Validator instances
this.value = '';
this.errors = [];
}
setValue(value) {
this.value = value;
this.validate();
}
validate() {
this.errors = [];
for (const validator of this.validators) {
if (!validator.validate(this.value)) {
this.errors.push(validator.getMessage());
}
}
return this.errors.length === 0;
}
isValid() {
return this.errors.length === 0;
}
getErrors() {
return this.errors;
}
}
// Usage - easy to add new validators
const emailField = new FormField('email', [
new RequiredValidator(),
new EmailValidator()
]);
const passwordField = new FormField('password', [
new RequiredValidator(),
new MinLengthValidator(8)
]);
emailField.setValue('invalid-email');
console.log(emailField.isValid()); // false
console.log(emailField.getErrors()); // ['Please enter a valid email address']
emailField.setValue('user@example.com');
console.log(emailField.isValid()); // true
React.js Example
Violating DIP:
// Component directly depends on concrete API implementation
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
// Tightly coupled to fetch API
fetch('/api/users')
.then(response => response.json())
.then(data => setUsers(data))
.catch(error => console.error(error));
}, []);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Following DIP:
// Create abstract service interface using Context
const ApiServiceContext = createContext(null);
// Abstract service interface (documented in comments for JS)
class ApiService {
async getUsers() {
throw new Error('getUsers() must be implemented');
}
async getUser(id) {
throw new Error('getUser() must be implemented');
}
async createUser(userData) {
throw new Error('createUser() must be implemented');
}
}
// Concrete implementation with fetch
class FetchApiService extends ApiService {
constructor(baseUrl) {
super();
this.baseUrl = baseUrl;
}
async getUsers() {
const response = await fetch(`${this.baseUrl}/users`);
return response.json();
}
async getUser(id) {
const response = await fetch(`${this.baseUrl}/users/${id}`);
return response.json();
}
async createUser(userData) {
const response = await fetch(`${this.baseUrl}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
return response.json();
}
}
// Mock implementation for testing
class MockApiService extends ApiService {
constructor() {
super();
this.users = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];
}
async getUsers() {
return Promise.resolve(this.users);
}
async getUser(id) {
const user = this.users.find(u => u.id === id);
return Promise.resolve(user);
}
async createUser(userData) {
const newUser = { ...userData, id: this.users.length + 1 };
this.users.push(newUser);
return Promise.resolve(newUser);
}
}
// Provider component
function ApiServiceProvider({ service, children }) {
return (
<ApiServiceContext.Provider value={service}>
{children}
</ApiServiceContext.Provider>
);
}
// Custom hook to access the service
function useApiService() {
const service = useContext(ApiServiceContext);
if (!service) {
throw new Error('useApiService must be used within ApiServiceProvider');
}
return service;
}
// Component depends on abstraction through context
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const apiService = useApiService(); // Injected dependency
useEffect(() => {
apiService.getUsers()
.then(setUsers)
.catch(console.error)
.finally(() => setLoading(false));
}, [apiService]);
if (loading) return <div>Loading...</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
function UserDetail({ userId }) {
const [user, setUser] = useState(null);
const apiService = useApiService();
useEffect(() => {
apiService.getUser(userId)
.then(setUser)
.catch(console.error);
}, [userId, apiService]);
if (!user) return <div>Loading...</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
// App setup - production
function App() {
const apiService = new FetchApiService('https://api.example.com');
return (
<ApiServiceProvider service={apiService}>
<UserList />
</ApiServiceProvider>
);
}
// Testing setup - uses mock
function TestApp() {
const mockService = new MockApiService();
return (
<ApiServiceProvider service={mockService}>
<UserList />
</ApiServiceProvider>
);
}
Advanced Pattern - Multiple Dependencies:
// Define multiple service abstractions
class StorageService {
get(key) { throw new Error('Not implemented'); }
set(key, value) { throw new Error('Not implemented'); }
remove(key) { throw new Error('Not implemented'); }
}
class NotificationService {
success(message) { throw new Error('Not implemented'); }
error(message) { throw new Error('Not implemented'); }
info(message) { throw new Error('Not implemented'); }
}
class AnalyticsService {
trackEvent(eventName, data) { throw new Error('Not implemented'); }
trackPageView(pageName) { throw new Error('Not implemented'); }
}
// Concrete implementations
class LocalStorageService extends StorageService {
get(key) {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : null;
}
set(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
remove(key) {
localStorage.removeItem(key);
}
}
class ToastNotificationService extends NotificationService {
success(message) {
// Show success toast
this.showToast(message, 'success');
}
error(message) {
this.showToast(message, 'error');
}
info(message) {
this.showToast(message, 'info');
}
showToast(message, type) {
console.log(`[${type.toUpperCase()}]: ${message}`);
// Actual toast implementation
}
}
class GoogleAnalyticsService extends AnalyticsService {
trackEvent(eventName, data) {
if (window.gtag) {
window.gtag('event', eventName, data);
}
}
trackPageView(pageName) {
if (window.gtag) {
window.gtag('config', 'GA_MEASUREMENT_ID', {
page_path: pageName
});
}
}
}
// Services context
const ServicesContext = createContext(null);
function ServicesProvider({ children }) {
const services = {
storage: new LocalStorageService(),
notifications: new ToastNotificationService(),
analytics: new GoogleAnalyticsService()
};
return (
<ServicesContext.Provider value={services}>
{children}
</ServicesContext.Provider>
);
}
// Custom hooks for each service
function useStorage() {
const services = useContext(ServicesContext);
return services.storage;
}
function useNotifications() {
const services = useContext(ServicesContext);
return services.notifications;
}
function useAnalytics() {
const services = useContext(ServicesContext);
return services.analytics;
}
// Component uses injected dependencies
function UserPreferences() {
const [theme, setTheme] = useState('light');
const storage = useStorage();
const notifications = useNotifications();
const analytics = useAnalytics();
useEffect(() => {
const savedTheme = storage.get('theme');
if (savedTheme) {
setTheme(savedTheme);
}
}, [storage]);
const handleThemeChange = (newTheme) => {
setTheme(newTheme);
storage.set('theme', newTheme);
notifications.success(`Theme changed to ${newTheme}`);
analytics.trackEvent('theme_changed', { theme: newTheme });
};
return (
<div>
<h2>Preferences</h2>
<button onClick={() => handleThemeChange('light')}>
Light Theme
</button>
<button onClick={() => handleThemeChange('dark')}>
Dark Theme
</button>
</div>
);
}
// Easy to test with mocks
class MockStorageService extends StorageService {
constructor() {
super();
this.data = new Map();
}
get(key) {
return this.data.get(key);
}
set(key, value) {
this.data.set(key, value);
}
remove(key) {
this.data.delete(key);
}
}
class MockNotificationService extends NotificationService {
constructor() {
super();
this.notifications = [];
}
success(message) {
this.notifications.push({ type: 'success', message });
}
error(message) {
this.notifications.push({ type: 'error', message });
}
info(message) {
this.notifications.push({ type: 'info', message });
}
}
function TestServicesProvider({ children }) {
const services = {
storage: new MockStorageService(),
notifications: new MockNotificationService(),
analytics: { trackEvent: jest.fn(), trackPageView: jest.fn() }
};
return (
<ServicesContext.Provider value={services}>
{children}
</ServicesContext.Provider>
);
}
Practical Example: Building a Todo Application with SOLID Principles
Let's see how all SOLID principles work together in a complete application.
Vanilla JavaScript Implementation
// === Single Responsibility Principle ===
// Data Model
class Todo {
constructor(id, title, completed = false) {
this.id = id;
this.title = title;
this.completed = completed;
this.createdAt = new Date();
}
}
// Storage abstraction (DIP)
class TodoStorage {
getAll() { throw new Error('Not implemented'); }
save(todos) { throw new Error('Not implemented'); }
}
class LocalTodoStorage extends TodoStorage {
constructor(key = 'todos') {
super();
this.key = key;
}
getAll() {
const data = localStorage.getItem(this.key);
return data ? JSON.parse(data) : [];
}
save(todos) {
localStorage.setItem(this.key, JSON.stringify(todos));
}
}
// Business logic
class TodoService {
constructor(storage) {
this.storage = storage;
this.todos = this.storage.getAll();
}
addTodo(title) {
const todo = new Todo(Date.now(), title);
this.todos.push(todo);
this.storage.save(this.todos);
return todo;
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
this.storage.save(this.todos);
}
return todo;
}
deleteTodo(id) {
this.todos = this.todos.filter(t => t.id !== id);
this.storage.save(this.todos);
}
getTodos() {
return [...this.todos];
}
getActiveTodos() {
return this.todos.filter(t => !t.completed);
}
getCompletedTodos() {
return this.todos.filter(t => t.completed);
}
}
// === Open/Closed Principle ===
// Filter strategy (can add new filters without modifying existing code)
class TodoFilter {
filter(todos) {
throw new Error('Not implemented');
}
}
class AllTodosFilter extends TodoFilter {
filter(todos) {
return todos;
}
}
class ActiveTodosFilter extends TodoFilter {
filter(todos) {
return todos.filter(t => !t.completed);
}
}
class CompletedTodosFilter extends TodoFilter {
filter(todos) {
return todos.filter(t => t.completed);
}
}
// === Interface Segregation Principle ===
// Separate renderers for different parts
class TodoItemRenderer {
render(todo, onToggle, onDelete) {
const li = document.createElement('li');
li.className = todo.completed ? 'completed' : '';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = todo.completed;
checkbox.addEventListener('change', () => onToggle(todo.id));
const span = document.createElement('span');
span.textContent = todo.title;
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.addEventListener('click', () => onDelete(todo.id));
li.appendChild(checkbox);
li.appendChild(span);
li.appendChild(deleteBtn);
return li;
}
}
class TodoListRenderer {
constructor(itemRenderer) {
this.itemRenderer = itemRenderer;
}
render(todos, onToggle, onDelete) {
const ul = document.createElement('ul');
ul.className = 'todo-list';
todos.forEach(todo => {
const item = this.itemRenderer.render(todo, onToggle, onDelete);
ul.appendChild(item);
});
return ul;
}
}
class TodoStatsRenderer {
render(todos) {
const div = document.createElement('div');
div.className = 'todo-stats';
const active = todos.filter(t => !t.completed).length;
const completed = todos.filter(t => t.completed).length;
div.textContent = `${active} active, ${completed} completed`;
return div;
}
}
// === Putting it all together ===
class TodoApp {
constructor(containerId, todoService) {
this.container = document.getElementById(containerId);
this.todoService = todoService;
this.currentFilter = new AllTodosFilter();
this.itemRenderer = new TodoItemRenderer();
this.listRenderer = new TodoListRenderer(this.itemRenderer);
this.statsRenderer = new TodoStatsRenderer();
this.init();
}
init() {
this.renderApp();
}
renderApp() {
this.container.innerHTML = '';
// Input section
const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'What needs to be done?';
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && input.value.trim()) {
this.todoService.addTodo(input.value.trim());
input.value = '';
this.renderApp();
}
});
// Filter buttons
const filters = document.createElement('div');
filters.className = 'filters';
['All', 'Active', 'Completed'].forEach(filterName => {
const btn = document.createElement('button');
btn.textContent = filterName;
btn.addEventListener('click', () => {
this.setFilter(filterName);
});
filters.appendChild(btn);
});
// Render todos
const todos = this.currentFilter.filter(this.todoService.getTodos());
const todoList = this.listRenderer.render(
todos,
(id) => {
this.todoService.toggleTodo(id);
this.renderApp();
},
(id) => {
this.todoService.deleteTodo(id);
this.renderApp();
}
);
// Stats
const stats = this.statsRenderer.render(this.todoService.getTodos());
this.container.appendChild(input);
this.container.appendChild(filters);
this.container.appendChild(todoList);
this.container.appendChild(stats);
}
setFilter(filterName) {
switch (filterName) {
case 'Active':
this.currentFilter = new ActiveTodosFilter();
break;
case 'Completed':
this.currentFilter = new CompletedTodosFilter();
break;
default:
this.currentFilter = new AllTodosFilter();
}
this.renderApp();
}
}
// Initialize the app
const storage = new LocalTodoStorage();
const todoService = new TodoService(storage);
const app = new TodoApp('app', todoService);
React.js Implementation
import { createContext, useContext, useState, useEffect } from 'react';
// === Single Responsibility Principle ===
// Data Model
class Todo {
constructor(id, title, completed = false) {
this.id = id;
this.title = title;
this.completed = completed;
this.createdAt = new Date();
}
}
// === Dependency Inversion Principle ===
// Storage abstraction
class TodoStorage {
getAll() { throw new Error('Not implemented'); }
save(todos) { throw new Error('Not implemented'); }
}
class LocalTodoStorage extends TodoStorage {
constructor(key = 'todos') {
super();
this.key = key;
}
getAll() {
const data = localStorage.getItem(this.key);
return data ? JSON.parse(data) : [];
}
save(todos) {
localStorage.setItem(this.key, JSON.stringify(todos));
}
}
class MemoryTodoStorage extends TodoStorage {
constructor() {
super();
this.todos = [];
}
getAll() {
return [...this.todos];
}
save(todos) {
this.todos = [...todos];
}
}
// Service layer
class TodoService {
constructor(storage) {
this.storage = storage;
}
getAllTodos() {
return this.storage.getAll();
}
addTodo(title) {
const todos = this.getAllTodos();
const newTodo = new Todo(Date.now(), title);
todos.push(newTodo);
this.storage.save(todos);
return newTodo;
}
toggleTodo(id) {
const todos = this.getAllTodos();
const todo = todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
this.storage.save(todos);
}
return todo;
}
deleteTodo(id) {
const todos = this.getAllTodos();
const filtered = todos.filter(t => t.id !== id);
this.storage.save(filtered);
}
}
// Context for dependency injection
const TodoServiceContext = createContext(null);
function TodoServiceProvider({ service, children }) {
return (
<TodoServiceContext.Provider value={service}>
{children}
</TodoServiceContext.Provider>
);
}
function useTodoService() {
const service = useContext(TodoServiceContext);
if (!service) {
throw new Error('useTodoService must be used within TodoServiceProvider');
}
return service;
}
// === Interface Segregation Principle ===
// Separate hooks for different concerns
function useTodos() {
const [todos, setTodos] = useState([]);
const service = useTodoService();
const loadTodos = () => {
setTodos(service.getAllTodos());
};
useEffect(() => {
loadTodos();
}, [service]);
const addTodo = (title) => {
service.addTodo(title);
loadTodos();
};
const toggleTodo = (id) => {
service.toggleTodo(id);
loadTodos();
};
const deleteTodo = (id) => {
service.deleteTodo(id);
loadTodos();
};
return { todos, addTodo, toggleTodo, deleteTodo };
}
function useTodoFilter(todos, filter) {
return todos.filter(todo => {
switch (filter) {
case 'active':
return !todo.completed;
case 'completed':
return todo.completed;
default:
return true;
}
});
}
function useTodoStats(todos) {
const active = todos.filter(t => !t.completed).length;
const completed = todos.filter(t => t.completed).length;
const total = todos.length;
return { active, completed, total };
}
// === Single Responsibility Principle ===
// Each component has one responsibility
function TodoInput({ onAdd }) {
const [value, setValue] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (value.trim()) {
onAdd(value.trim());
setValue('');
}
};
return (
<form onSubmit={handleSubmit} className="todo-input">
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="What needs to be done?"
/>
<button type="submit">Add</button>
</form>
);
}
function TodoItem({ todo, onToggle, onDelete }) {
return (
<li className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.title}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
}
function TodoList({ todos, onToggle, onDelete }) {
return (
<ul className="todo-list">
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
}
function TodoFilters({ currentFilter, onFilterChange }) {
const filters = ['all', 'active', 'completed'];
return (
<div className="todo-filters">
{filters.map(filter => (
<button
key={filter}
className={currentFilter === filter ? 'active' : ''}
onClick={() => onFilterChange(filter)}
>
{filter.charAt(0).toUpperCase() + filter.slice(1)}
</button>
))}
</div>
);
}
function TodoStats({ stats }) {
return (
<div className="todo-stats">
<span>{stats.active} active</span>
<span> • </span>
<span>{stats.completed} completed</span>
<span> • </span>
<span>{stats.total} total</span>
</div>
);
}
// === Open/Closed Principle ===
// Main app component - extensible without modification
function TodoApp() {
const { todos, addTodo, toggleTodo, deleteTodo } = useTodos();
const [filter, setFilter] = useState('all');
const filteredTodos = useTodoFilter(todos, filter);
const stats = useTodoStats(todos);
return (
<div className="todo-app">
<h1>Todo Application</h1>
<TodoInput onAdd={addTodo} />
<TodoFilters currentFilter={filter} onFilterChange={setFilter} />
<TodoList
todos={filteredTodos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
<TodoStats stats={stats} />
</div>
);
}
// App setup
function App() {
const storage = new LocalTodoStorage();
const service = new TodoService(storage);
return (
<TodoServiceProvider service={service}>
<TodoApp />
</TodoServiceProvider>
);
}
export default App;
Conclusion: The Path to Excellence in Front-End Development with SOLID
Embracing SOLID for Superior Web Applications:
Implementing SOLID design principles in front-end development is more than a technical exercise; it's a commitment to excellence. By embracing these principles, developers can create web applications that are not just functional but also efficient, maintainable, and delightful to interact with. The resulting codebase is easier to understand, enhance, and debug, leading to a more robust and user-friendly application.
Key Takeaways:
- Single Responsibility Principle: Keep components focused on one task, making them easier to understand, test, and maintain.
- Open/Closed Principle: Design for extension through composition, inheritance, and plugin architectures without modifying existing code.
- Liskov Substitution Principle: Ensure components can be safely replaced with their variants, maintaining consistent behavior across your application.
- Interface Segregation Principle: Create focused, minimal interfaces that don't force components to depend on methods they don't use.
- Dependency Inversion Principle: Depend on abstractions rather than concrete implementations, making your code more flexible and testable.
Practical Benefits:
- Maintainability: Code organized by SOLID principles is easier to navigate, understand, and modify.
- Testability: Loosely coupled components with injected dependencies are simpler to test in isolation.
- Scalability: Applications built on solid foundations can grow without accumulating technical debt.
- Collaboration: Clear separation of concerns makes it easier for teams to work on different parts of the codebase simultaneously.
- Reusability: Well-designed components can be reused across projects, saving development time.
A Continuous Journey of Growth and Adaptation:
The journey to mastering SOLID principles is ongoing. It involves constant learning, experimentation, and adaptation to new technologies and methodologies. As you integrate SOLID into your front-end practices, you'll experience a transformative impact on both your code and the user experience you deliver. Start small, refactor incrementally, and watch as your front-end development reaches new heights of professionalism and quality.
Remember: SOLID principles are guidelines, not rigid rules. Apply them judiciously, always keeping in mind the specific needs of your project and team. The goal is to write code that is maintainable, testable, and adaptable—code that serves both developers and users well.
Resources & References
- wikipedia.org - SOLID
- digitalocean.com - SOLID: The First 5 Principles of Object Oriented Design
- youtube.com - SOLID Design Principles Explained in a Nutshell
- youtube.com - The right way to write React.js clean code - SOLID
- youtube.com - What are SOLID principles in software architecture
- everydayreact.com - SOLID Principles in React.js
- konstantinlebedev.com - Applying SOLID principle in React.js
- khalilstemmler.com - SOLID Principles: The Software Developer's Framework to Robust & Maintainable Code [with Examples]
- khalilstemmler.com - Domain Knowledge & Interpretation of the Single Responsibility Principle | SOLID Node.js + TypeScript
- staff.cs.utu.fi - Design Patterns and Principles
- youtube.com - Clean Code - Uncle Bob / lesson 1
- stackify - SOLID Design Principles Explained: The Single Responsibility Principle with Code Examples
- stackify - SOLID Design Principles Explained: The Open/Closed Principle with Code Examples
- stackify - SOLID Design Principles Explained: The Liskov Substitution Principle with Code Examples
- stackify - SOLID Design Principles Explained: The Interface Segregation Principle with Code Examples
- stackify - SOLID Design Principles Explained: The Dependency Inversion Principle with Code Examples
- reflectoring.io - Interface Segregation Principle
- What is an implementation detail?
- deviq.com - Single Responsibility Principle
- deviq.com - Open/Closed Principle
- deviq.com - Liskov Substitution Principle
- deviq.com - Interface Segregation Principle
- deviq.com - Dependency Inversion Principle