DEV Community

padmajothi Athimoolam
padmajothi Athimoolam

Posted on

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 (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>{product.description}</p>
      <p>${product.price}</p>
      <button onClick={() => onAddToCart(product)}>Add to Cart</button>
    </div>
  );
};

export default ProductCard;

Enter fullscreen mode Exit fullscreen mode

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 (
    <div>
      <h2>All Products</h2>
      {products.map((product) => (
        <ProductCard key={product.id} product={product} onAddToCart={handleAddToCart} />
      ))}
    </div>
  );
};

export default ProductList;

Enter fullscreen mode Exit fullscreen mode

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 (
    <div>
      <h2>Featured Products</h2>
      {featuredProducts.map((product) => (
        <ProductCard key={product.id} product={product} onAddToCart={handleAddToCart} />
      ))}
    </div>
  );
};

export default FeaturedProducts;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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 (
    <div className="product-grid">
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
};

export default GridView;

Enter fullscreen mode Exit fullscreen mode

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 (
    <div className="product-list">
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
};

export default ListView;

Enter fullscreen mode Exit fullscreen mode

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 }) => (
  <div className="cart-item">
    <h3>{product.name}</h3>
    <p>${product.price}</p>
    <button onClick={() => onRemove(product.id)}>Remove</button>
  </div>
);

export default CartItem;

Enter fullscreen mode Exit fullscreen mode

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 (
    <div>
      <h2>Your Cart</h2>
      {cartItems.map((item) => (
        <CartItem key={item.id} product={item} onRemove={handleRemoveItem} />
      ))}
    </div>
  );
};

export default CartPage;

Enter fullscreen mode Exit fullscreen mode

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 (
    <div>
      <h2>Checkout</h2>
      {cartItems.map((item) => (
        <CartItem key={item.id} product={item} onRemove={handleRemoveItem} />
      ))}

    </div>
  );
};

export default CheckoutPage;

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)