Reusable Code in React
When working with React applications, especially in larger projects like an e-commerce app, you'll often encounter situations where components share similar functionality or structure but differ in logic or JSX. To improve code maintainability, avoid repetition, and ensure reusability, we can break down these scenarios and discuss possible solutions with code samples. 1. Problem: Two Components Have Different Logic, but Nearly Identical JSX Scenario: You have two components that look nearly identical (i.e., similar JSX structure), but their logic is different. For example, a component that displays a product list and a component that displays featured products. They share a similar layout but have different logic for fetching the products. Solution: In this case, you can extract the shared JSX into a reusable component and allow each parent component to pass its unique logic as props (such as the data to display or specific actions to trigger). Example: // ProductCard.js - Shared JSX component import React from 'react'; const ProductCard = ({ product, onAddToCart }) => { return ( {product.name} {product.description} ${product.price} onAddToCart(product)}>Add to Cart ); }; export default ProductCard; Now, let's create two parent components: ProductList and FeaturedProducts, which have different logic but reuse ProductCard for their JSX. // ProductList.js - Fetching all products import React, { useEffect, useState } from 'react'; import ProductCard from './ProductCard'; const ProductList = () => { const [products, setProducts] = useState([]); useEffect(() => { // Fetch all products (this is just an example) fetch('/api/products') .then((res) => res.json()) .then((data) => setProducts(data)); }, []); const handleAddToCart = (product) => { // Add to cart logic console.log('Added to cart:', product.name); }; return ( All Products {products.map((product) => ( ))} ); }; export default ProductList; FeaturedProducts.js // FeaturedProducts.js - Fetching only featured products import React, { useEffect, useState } from 'react'; import ProductCard from './ProductCard'; const FeaturedProducts = () => { const [featuredProducts, setFeaturedProducts] = useState([]); useEffect(() => { // Fetch featured products (this is just an example) fetch('/api/featured-products') .then((res) => res.json()) .then((data) => setFeaturedProducts(data)); }, []); const handleAddToCart = (product) => { // Add to cart logic console.log('Featured Product added to cart:', product.name); }; return ( Featured Products {featuredProducts.map((product) => ( ))} ); }; export default FeaturedProducts; Both components (ProductList and FeaturedProducts) share similar JSX but have different logic for fetching products. By using a reusable ProductCard component, you avoid code duplication. 2. Two Components Have the Same Logic, but Different JSX Scenario: You have two components that share the same logic (e.g., fetching products), but they display the data in different layouts. For example, a grid view for products and a list view for products. Solution: In this case, you can extract the common logic into a custom hook or a shared context and pass the layout-specific JSX (or rendering logic) as props to a reusable presentational component. Example: // useProducts.js - Shared hook for fetching products import { useState, useEffect } from 'react'; const useProducts = () => { const [products, setProducts] = useState([]); useEffect(() => { // Fetch products fetch('/api/products') .then((res) => res.json()) .then((data) => setProducts(data)); }, []); return products; }; export default useProducts; Now, we can create two components that share the logic but have different rendering (layout) styles. // GridView.js - Grid layout for products import React from 'react'; import useProducts from './useProducts'; import ProductCard from './ProductCard'; const GridView = () => { const products = useProducts(); return ( {products.map((product) => ( ))} ); }; export default GridView; ListView.js // ListView.js - List layout for products import React from 'react'; import useProducts from './useProducts'; import ProductCard from './ProductCard'; const ListView = () => { const products = useProducts(); return ( {products.map((product) => ( ))} ); }; export default ListView; Both GridView and ListView share the same logic for fetching products (via the useProducts hook) but have different layouts. By separating the logic (with a custom hook) and reusing the ProductCard component, you maintain clean code while supporting different visual layouts. 3. Problem: Two Components Have Similar Lo
When working with React applications, especially in larger projects like an e-commerce app, you'll often encounter situations where components share similar functionality or structure but differ in logic or JSX. To improve code maintainability, avoid repetition, and ensure reusability, we can break down these scenarios and discuss possible solutions with code samples.
1. Problem: Two Components Have Different Logic, but Nearly Identical JSX
Scenario:
You have two components that look nearly identical (i.e., similar JSX structure), but their logic is different. For example, a component that displays a product list and a component that displays featured products. They share a similar layout but have different logic for fetching the products.
Solution:
In this case, you can extract the shared JSX into a reusable component and allow each parent component to pass its unique logic as props (such as the data to display or specific actions to trigger).
Example:
// ProductCard.js - Shared JSX component
import React from 'react';
const ProductCard = ({ product, onAddToCart }) => {
return (
{product.name}
{product.description}
${product.price}
);
};
export default ProductCard;
Now, let's create two parent components: ProductList and FeaturedProducts, which have different logic but reuse ProductCard for their JSX.
// ProductList.js - Fetching all products
import React, { useEffect, useState } from 'react';
import ProductCard from './ProductCard';
const ProductList = () => {
const [products, setProducts] = useState([]);
useEffect(() => {
// Fetch all products (this is just an example)
fetch('/api/products')
.then((res) => res.json())
.then((data) => setProducts(data));
}, []);
const handleAddToCart = (product) => {
// Add to cart logic
console.log('Added to cart:', product.name);
};
return (
All Products
{products.map((product) => (
))}
);
};
export default ProductList;
FeaturedProducts.js
// FeaturedProducts.js - Fetching only featured products
import React, { useEffect, useState } from 'react';
import ProductCard from './ProductCard';
const FeaturedProducts = () => {
const [featuredProducts, setFeaturedProducts] = useState([]);
useEffect(() => {
// Fetch featured products (this is just an example)
fetch('/api/featured-products')
.then((res) => res.json())
.then((data) => setFeaturedProducts(data));
}, []);
const handleAddToCart = (product) => {
// Add to cart logic
console.log('Featured Product added to cart:', product.name);
};
return (
Featured Products
{featuredProducts.map((product) => (
))}
);
};
export default FeaturedProducts;
Both components (ProductList and FeaturedProducts) share similar JSX but have different logic for fetching products. By using a reusable ProductCard component, you avoid code duplication.
2. Two Components Have the Same Logic, but Different JSX
Scenario:
You have two components that share the same logic (e.g., fetching products), but they display the data in different layouts. For example, a grid view for products and a list view for products.
Solution:
In this case, you can extract the common logic into a custom hook or a shared context and pass the layout-specific JSX (or rendering logic) as props to a reusable presentational component.
Example:
// useProducts.js - Shared hook for fetching products
import { useState, useEffect } from 'react';
const useProducts = () => {
const [products, setProducts] = useState([]);
useEffect(() => {
// Fetch products
fetch('/api/products')
.then((res) => res.json())
.then((data) => setProducts(data));
}, []);
return products;
};
export default useProducts;
Now, we can create two components that share the logic but have different rendering (layout) styles.
// GridView.js - Grid layout for products
import React from 'react';
import useProducts from './useProducts';
import ProductCard from './ProductCard';
const GridView = () => {
const products = useProducts();
return (
{products.map((product) => (
))}
);
};
export default GridView;
ListView.js
// ListView.js - List layout for products
import React from 'react';
import useProducts from './useProducts';
import ProductCard from './ProductCard';
const ListView = () => {
const products = useProducts();
return (
{products.map((product) => (
))}
);
};
export default ListView;
Both GridView and ListView share the same logic for fetching products (via the useProducts hook) but have different layouts. By separating the logic (with a custom hook) and reusing the ProductCard component, you maintain clean code while supporting different visual layouts.
3. Problem: Two Components Have Similar Logic and JSX
Scenario:
You have two components with both similar logic and nearly identical JSX. For example, you have a product details page and a checkout page that both require displaying a list of items in a cart, and the logic for adding/removing items is almost identical.
Solution:
In this case, you can abstract the shared code into a reusable component and wrap it in a higher-order component (HOC) or a shared state management solution (like Context API) to further eliminate redundancy.
Example:
// CartItem.js - Shared component for displaying a cart item
import React from 'react';
const CartItem = ({ product, onRemove }) => (
{product.name}
${product.price}
);
export default CartItem;
Now, we can create two components: CartPage and CheckoutPage, which use the shared CartItem component.
// CartPage.js - Cart page showing cart items
import React, { useState } from 'react';
import CartItem from './CartItem';
const CartPage = () => {
const [cartItems, setCartItems] = useState([
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Smartphone', price: 599 },
]);
const handleRemoveItem = (id) => {
setCartItems(cartItems.filter((item) => item.id !== id));
};
return (
Your Cart
{cartItems.map((item) => (
))}
);
};
export default CartPage;
checkoutPage.js
// CheckoutPage.js - Checkout page showing cart items (same logic)
import React, { useState } from 'react';
import CartItem from './CartItem';
const CheckoutPage = () => {
const [cartItems, setCartItems] = useState([
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Smartphone', price: 599 },
]);
const handleRemoveItem = (id) => {
setCartItems(cartItems.filter((item) => item.id !== id));
};
return (
Checkout
{cartItems.map((item) => (
))}
);
};
export default CheckoutPage;
Both CartPage and CheckoutPage share the same logic for displaying and removing items from the cart. By using a shared CartItem component, you can eliminate redundancy and keep the code clean, even when the JSX structure and logic are similar.
By following these strategies—whether it’s breaking down logic into custom hooks, creating shared components, or utilizing context—React developers can significantly reduce redundancy and build applications that are both easier to maintain and extend.