The Single Responsibility Principle (SRP) is one of the fundamental principles of good software design. It’s part of the SOLID principles, a set of guidelines that help us write better, more maintainable code. Today, let’s explore how we can apply SRP to our React components.
What is Single Responsibility Principle?
The principle states that a module (or in our case, a component) should have only one reason to change. In other words, a component should do one thing, and do it well.
The Problem: A Common Anti-Pattern
Let’s look at a typical React component that’s trying to do too much:
const ProductCard = () => {
const [product, setProduct] = useState<Product | null>(null);
const [inCart, setInCart] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetchProduct();
}, []);
const fetchProduct = async () => {
try {
const response = await fetch("/api/product/123");
const data = await response.json();
setProduct(data);
checkIfInCart(data.id);
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
};
const checkIfInCart = async (productId: string) => {
const response = await fetch(`/api/cart/${productId}`);
const data = await response.json();
setInCart(data.inCart);
};
const addToCart = async () => {
try {
await fetch("/api/cart", {
method: "POST",
body: JSON.stringify({ productId: product?.id }),
});
setInCart(true);
} catch (e) {
setError(e as Error);
}
};
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!product) return <div>Product not found</div>;
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h2>{product.name}</h2>
<p>{product.description}</p>
<div className="price">${product.price}</div>
<button
onClick={addToCart}
disabled={inCart}
>
{inCart ? 'In Cart' : 'Add to Cart'}
</button>
</div>
);
};
This component is doing too many things:
- Fetching product data
- Managing cart state
- Handling loading and error states
- Rendering the product UI
- Managing cart interactions
A Better Approach: Separation of Concerns
Let’s break this down into focused components and hooks:
// Data fetching hook
const useProduct = (productId: string) => {
const [product, setProduct] = useState<Product | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetchProduct();
}, [productId]);
const fetchProduct = async () => {
try {
const response = await fetch(`/api/product/${productId}`);
const data = await response.json();
setProduct(data);
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
};
return { product, loading, error };
};
// Cart management hook
const useCart = (productId: string) => {
const [inCart, setInCart] = useState(false);
useEffect(() => {
checkIfInCart();
}, [productId]);
const checkIfInCart = async () => {
const response = await fetch(`/api/cart/${productId}`);
const data = await response.json();
setInCart(data.inCart);
};
const addToCart = async () => {
await fetch("/api/cart", {
method: "POST",
body: JSON.stringify({ productId }),
});
setInCart(true);
};
return { inCart, addToCart };
};
// Presentation component
const ProductDisplay = ({
product,
inCart,
onAddToCart,
}: {
product: Product;
inCart: boolean;
onAddToCart: () => void;
}) => (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h2>{product.name}</h2>
<p>{product.description}</p>
<div className="price">${product.price}</div>
<button
onClick={onAddToCart}
disabled={inCart}
>
{inCart ? 'In Cart' : 'Add to Cart'}
</button>
</div>
);
// Container component
const ProductCard = ({ productId }: { productId: string }) => {
const { product, loading, error } = useProduct(productId);
const { inCart, addToCart } = useCart(productId);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
if (!product) return <NotFound message="Product not found" />;
return (
<ProductDisplay
product={product}
inCart={inCart}
onAddToCart={addToCart}
/>
);
};
Benefits of This Approach
- Separation of Concerns: Each piece has a clear, single responsibility
- Reusability: Hooks and components can be reused across the application
- Testability: Each part can be tested in isolation
- Maintainability: Changes to one aspect (like cart logic) don’t affect others
- Flexibility: Easy to modify or replace individual pieces
How to Identify Violations of SRP
Here’s a practical tip: If you’re describing your component and find yourself using “and”, you might be violating SRP. For example:
- “This component fetches product data and manages cart state”
- “This hook handles product loading and cart operations”
These are signs that you should consider splitting your code into more focused pieces.
Conclusion
The Single Responsibility Principle is about creating components that are focused and maintainable. While it might seem like more work initially to split things up, the benefits in terms of maintainability, testability, and reusability are well worth it.
In future posts, we’ll explore other SOLID principles and how they can help us write better React applications. Stay tuned!
Happy coding! 🚀