visit
“A module should be responsible to one, and only one, actor.” — Wikipedia.
// ❌ Bad Practice: Component with Multiple Responsibilities
const Products = () => {
return (
<div className="products">
{products.map((product) => (
<div key={product?.id} className="product">
<h3>{product?.name}</h3>
<p>${product?.price}</p>
</div>
))}
</div>
);
};
In the above example, the Products
component violates the Single Responsibility Principle by taking on multiple responsibilities. It manages the iteration of products and handles the UI rendering for each product. This can make the component challenging to understand, maintain, and test in the future.
// ✅ Good Practice: Separating Responsibilities into Smaller Components
import Product from './Product';
import products from '../../data/products.json';
const Products = () => {
return (
<div className="products">
{products.map((product) => (
<Product key={product?.id} product={product} />
))}
</div>
);
};
// Product.js
// Separate component responsible for rendering the product details
const Product = ({ product }) => {
return (
<div className="product">
<h3>{product?.name}</h3>
<p>${product?.price}</p>
</div>
);
};
“software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.” — Wikipedia.
// ❌ Bad Practice: Violating the Open-Closed Principle
// Button.js
// Existing Button component
const Button = ({ text, onClick }) => {
return (
<button onClick={onClick}>
{text}
</button>
);
}
// Button.js
// Modified Existing Button component with additional icon prop (modification)
const Button = ({ text, onClick, icon }) => {
return (
<button onClick={onClick}>
<i className={icon} />
<span>{text}</span>
</button>
);
}
// Home.js
// 👇 Avoid: Modified existing component prop
const Home = () => {
const handleClick= () => {};
return (
<div>
{/* ❌ Avoid this */}
<Button text="Submit" onClick={handleClick} icon="fas fa-arrow-right" />
</div>
);
}
In the above example, we modify the existing Button
component by adding an icon
prop. Altering an existing component to accommodate new requirements violates the Open-Closed Principle. These changes make the component more fragile and introduce the risk of unintended side effects when used in different contexts.
// ✅ Good Practice: Open-Closed Principle
// Button.js
// Existing Button functional component
const Button = ({ text, onClick }) => {
return (
<button onClick={onClick}>
{text}
</button>
);
}
// IconButton.js
// IconButton component
// ✅ Good: You have not modified anything here.
const IconButton = ({ text, icon, onClick }) => {
return (
<button onClick={onClick}>
<i className={icon} />
<span>{text}</span>
</button>
);
}
const Home = () => {
const handleClick = () => {
// Handle button click event
}
return (
<div>
<Button text="Submit" onClick={handleClick} />
{/*
<IconButton text="Submit" icon="fas fa-heart" onClick={handleClick} />
</div>
);
}
In the above example, we create a separate IconButton
functional component. The IconButton
component encapsulates the rendering of an icon button without modifying the existing Button
component. It adheres to the Open-Closed Principle by extending the functionality through composition rather than modification.
“Subtype objects should be substitutable for supertype objects” — Wikipedia.
// ⚠️ Bad Practice
// This approach violates the Liskov Substitution Principle as it modifies
// the behavior of the derived component, potentially resulting in unforeseen
// problems when substituting it for the base Select component.
const BadCustomSelect = ({ value, iconClassName, handleChange }) => {
return (
<div>
<i className={iconClassName}></i>
<select value={value} onChange={handleChange}>
<options value={1}>One</options>
<options value={2}>Two</options>
<options value={3}>Three</options>
</select>
</div>
);
};
const LiskovSubstitutionPrinciple = () => {
const [value, setValue] = useState(1);
const handleChange = (event) => {
setValue(event.target.value);
};
return (
<div>
{/** ❌ Avoid this */}
{/** Below Custom Select doesn't have the characteristics of base `select` element */}
<BadCustomSelect value={value} handleChange={handleChange} />
</div>
);
In the above example, we have a BadCustomSelect
component intended to serve as a custom select input in React. However, it violates the Liskov Substitution Principle (LSP) because it restricts the behavior of the base select
element.
// ✅ Good Practice
// This component follows the Liskov Substitution Principle and allows the use of select's characteristics.
const CustomSelect = ({ value, iconClassName, handleChange, ...props }) => {
return (
<div>
<i className={iconClassName}></i>
<select value={value} onChange={handleChange} {...props}>
<options value={1}>One</options>
<options value={2}>Two</options>
<options value={3}>Three</options>
</select>
</div>
);
};
const LiskovSubstitutionPrinciple = () => {
const [value, setValue] = useState(1);
const handleChange = (event) => {
setValue(event.target.value);
};
return (
<div>
{/* ✅ This CustomSelect component follows the Liskov Substitution Principle */}
<CustomSelect
value={value}
handleChange={handleChange}
defaultValue={1}
/>
</div>
);
};
In the revised code, we have a CustomSelect
component intended to extend the functionality of the standard select
element in React. The component accepts props such as value
, iconClassName
, handleChange
, and additional props using the spread operator ...props
. By allowing the use of the select
element's characteristics and accepting additional props, the CustomSelect
component follows the Liskov Substitution Principle (LSP).
“No code should be forced to depend on methods it does not use.” — Wikipedia.
// ❌ Avoid: disclose unnecessary information for this component
// This introduces unnecessary dependencies and complexity for the component
const ProductThumbnailURL = ({ product }) => {
return (
<div>
<img src={product.imageURL} alt={product.name} />
</div>
);
};
// ❌ Bad Practice
const Product = ({ product }) => {
return (
<div>
<ProductThumbnailURL product={product} />
<h4>{product?.name}</h4>
<p>{product?.description}</p>
<p>{product?.price}</p>
</div>
);
};
const Products = () => {
return (
<div>
{products.map((product) => (
<Product key={product.id} product={product} />
))}
</div>
);
}
In the above example, we pass the entire product details to the ProductThumbnailURL
component, even though it doesn’t require it. It adds unnecessary risks and complexity to the component and violates the Interface Segregation Principle (ISP).
// ✅ Good: reducing unnecessary dependencies and making
// the codebase more maintainable and scalable.
const ProductThumbnailURL = ({ imageURL, alt }) => {
return (
<div>
<img src={imageURL} alt={alt} />
</div>
);
};
// ✅ Good Practice
const Product = ({ product }) => {
return (
<div>
<ProductThumbnailURL imageURL={product.imageURL} alt={product.name} />
<h4>{product?.name}</h4>
<p>{product?.description}</p>
<p>{product?.price}</p>
</div>
);
};
const Products = () => {
return (
<div>
{products.map((product) => (
<Product key={product.id} product={product} />
))}
</div>
);
};
In the revised code, the ProductThumbnailURL
component only receives the required information instead of the entire product details. It prevents unnecessary risks and fosters the Interface Segregation Principle (ISP).
“One entity should depend upon abstractions, not concretions” — Wikipedia.
// ❌ Bad Practice
// This component follows concretion instead of abstraction and
// breaks Dependency Inversion Principle
const CustomForm = ({ children }) => {
const handleSubmit = () => {
// submit operations
};
return <form onSubmit={handleSubmit}>{children}</form>;
};
const DependencyInversionPrinciple = () => {
const [email, setEmail] = useState();
const handleChange = (event) => {
setEmail(event.target.value);
};
const handleFormSubmit = (event) => {
// submit business logic here
};
return (
<div>
{/** ❌ Avoid: tightly coupled and hard to change */}
<BadCustomForm>
<input
type="email"
value={email}
onChange={handleChange}
name="email"
/>
</BadCustomForm>
</div>
);
};
The CustomForm
component is tightly coupled to its children, preventing flexibility and making it challenging to change or extend its behavior.
// ✅ Good Practice
// This component follows abstraction and promotes Dependency Inversion Principle
const AbstractForm = ({ children, onSubmit }) => {
const handleSubmit = (event) => {
event.preventDefault();
onSubmit();
};
return <form onSubmit={handleSubmit}>{children}</form>;
};
const DependencyInversionPrinciple = () => {
const [email, setEmail] = useState();
const handleChange = (event) => {
setEmail(event.target.value);
};
const handleFormSubmit = () => {
// submit business logic here
};
return (
<div>
{/** ✅ Use the abstraction instead */}
<AbstractForm onSubmit={handleFormSubmit}>
<input
type="email"
value={email}
onChange={handleChange}
name="email"
/>
<button type="submit">Submit</button>
</AbstractForm>
</div>
);
};
In the revised code, we introduce the AbstractForm
component, which acts as an abstraction for the form. It receives the onSubmit
function as a prop and handles the form submission. This approach allows us to easily swap out or extend the form behavior without modifying the higher-level component.
Stay curious; keep coding!
Reference:
Also published .