6 min read

Designing Reusable Component Systems

Designing Reusable Component Systems

Imagine building an orchestra—not just one beautiful melody, but a system that can play any tune, adapt on the fly, and let musicians swap roles seamlessly. In the React world, that’s the dream: crafting reusable components, orchestrating dynamic UIs, and shipping features effortlessly—even when order management gets tricky and requirements shift overnight.

This article guides you through designing a reusable component system that is not only flexible but also production-ready. We’ll move step-by-step from concepts to code, using real-world order management scenarios that you can run locally. We'll focus on practical techniques that emphasize maintainability, scalability, and accessibility.


Why Reusability Matters

Reusable components are about more than just saving lines of code; they are the foundation of a healthy codebase:

  • DRY Code (Don’t Repeat Yourself): Simplifies maintenance and reduces bugs.
  • Rapid Prototyping: Enables faster feature development.
  • Team Collaboration: Provides clear, predictable building blocks for your team.
  • Graceful Changes: Allows for easy adaptation to new requirements.

What Makes a Component “Reusable”?

A truly reusable component:

  • Solves one focused problem: Instead of hardcoding for a special case, it accepts props to adapt its behavior and rendering.
  • Avoids leaking implementation details: Consumers should use the "what," not care about "how."
  • Embraces composability: Works as a self-contained building block, able to nest or mix with others in bigger UIs.
  • Handles variations via props or children: Can customize content, styles, or logic based on incoming data.
  • Documentation and Examples: Is well-documented, with clear example usage.
  • Testing in Isolation: Can be tested in isolation—ideally with an example in App.js.

The Foundation: Creating a “Card” Component

Let’s start with a simple but powerful idea—a Card component that displays any content you give it. This abstract pattern will help you build order previews, notifications, customer info blocks, and more—all with one flexible component.

Step 1: Creating Card.jsx

// src/components/Card.jsx

function Card({ children, title, style }) {
  return (
    <div style={{
      border: '1px solid #ddd',
      borderRadius: '8px',
      padding: '16px',
      boxShadow: '0 2px 10px rgba(0,0,0,0.02)',
      marginBottom: '20px',
      ...style
    }}>
      {title && <h3>{title}</h3>}
      {children}
    </div>
  );
}

export default Card;

How to use/test this locally:

Create App.js in your src/ folder:

// src/App.js

import Card from './components/Card';

function App() {
  return (
    <div style={{ maxWidth: 600, margin: '30px auto' }}>
      <Card title="Order #14321">
        <p>Customer: Sonali</p>
        <p>Status: Processing</p>
        <p>Amount: ₹550</p>
      </Card>
      <Card title="Quick Note" style={{ background: '#f9f6ff' }}>
        <p>This component works for any content—try it with notifications, user details, or anything else!</p>
      </Card>
    </div>
  );
}

export default App;

Step Up: “OrderItem” - A Smart, Composable Component

Now let’s build a reusable OrderItem component that can display different orders, replace its layout or actions via props, and flexibly serve many contexts.

Step 2: Creating OrderItem.jsx

// src/components/OrderItem.jsx

function OrderItem({ order, showActions = true, onStatusChange }) {
  // Defensive fallback
  if (!order) return null;

  return (
    <div style={{
      borderBottom: '1px solid #eee',
      padding: '12px 0'
    }}>
      <div>
        <strong>Order #{order.id}</strong> for <strong>{order.customer}</strong>
      </div>
      <div>
        <span>Total: ₹{order.amount}</span>
        <span style={{ marginLeft: 15 }}>Status: {order.status}</span>
      </div>
      {showActions && (
        <div style={{ marginTop: 9 }}>
          <button
            disabled={order.status === "Delivered"}
            onClick={() => onStatusChange(order.id, "Delivered")}
          >
            Mark as Delivered
          </button>
        </div>
      )}
    </div>
  );
}

export default OrderItem;

How to use/test this in App.js:

// src/App.js

import Card from './components/Card';
import OrderItem from './components/OrderItem';
import { useState } from 'react';

const initialOrders = [
  { id: 14321, customer: 'Sonali', amount: 550, status: 'Processing' },
  { id: 14322, customer: 'Prateek', amount: 1200, status: 'Pending' },
];

function App() {
  const [orders, setOrders] = useState(initialOrders);

  const handleStatusChange = (id, nextStatus) => {
    setOrders(orders =>
      orders.map(order =>
        order.id === id ? { ...order, status: nextStatus } : order
      )
    );
  };

  return (
    <div style={{ maxWidth: 600, margin: '30px auto' }}>
      <Card title="Recent Orders">
        {orders.length > 0 ? (
          orders.map(order => (
            <OrderItem
              key={order.id}
              order={order}
              onStatusChange={handleStatusChange}
            />
          ))
        ) : (
          <p>No orders found.</p>
        )}
      </Card>
    </div>
  );
}

export default App;

Try adding/deleting orders, passing different values to showActions, or customizing the layout further!


Composition: “OrderList” With Flexible Children

Imagine you want to display a list of orders, but sometimes show only a summary, sometimes all details, or even allow the parent to control rendering style. That’s the perfect use-case for composition.

Step 3: Building OrderList.jsx

// src/components/OrderList.jsx

function OrderList({ orders, renderItem }) {
  // Default renderer if none supplied
  const defaultRender = order => (
    <li key={order.id}>
      #{order.id} — {order.customer}: ₹{order.amount}, Status: {order.status}
    </li>
  );

  return (
    <ul style={{ listStyle: 'none', padding: 0 }}>
      {orders.map(order =>
        renderItem ? renderItem(order) : defaultRender(order)
      )}
    </ul>
  );
}

export default OrderList;

How to test this approach locally:

// src/App.js

import OrderList from './components/OrderList';
import Card from './components/Card';

const orders = [
  { id: 14321, customer: 'Sonali', amount: 550, status: 'Processing' },
  { id: 14322, customer: 'Prateek', amount: 1200, status: 'Delivered' },
];

function App() {
  return (
    <div style={{ maxWidth: 600, margin: '30px auto' }}>
      <Card title="Summary">
        <OrderList
          orders={orders}
          renderItem={order => (
            <li key={order.id} style={{ color: order.status === "Delivered" ? "green" : "black" }}>
              {order.customer} ({order.status})
            </li>
          )}
        />
      </Card>

      <Card title="Full Order Details">
        <OrderList orders={orders} />
      </Card>
    </div>
  );
}

export default App;

Advanced Composition: “OrderGrid” With Render Props and Custom Layouts

Take composition up a notch by making your “container” component accept custom renderers for not just items but also layouts—great for dashboards, print views, or mobile UIs.

Step 4: Create OrderGrid.jsx

// src/components/OrderGrid.jsx

function OrderGrid({ orders, renderHeader, renderItem }) {
  return (
    <div>
      {renderHeader && renderHeader(orders)}
      <div style={{
        display: 'grid',
        gridTemplateColumns: 'repeat(2, 1fr)',
        gap: '16px',
        marginTop: '12px'
      }}>
        {orders.map(order => renderItem(order))}
      </div>
    </div>
  );
}

export default OrderGrid;

How to use/test locally:

// src/App.js

import OrderGrid from './components/OrderGrid';

const orders = [
  { id: 14323, customer: 'Ritika', amount: 350, status: 'Processing' },
  { id: 14324, customer: 'Aniket', amount: 275, status: 'Pending' },
];

function App() {
  return (
    <div style={{ maxWidth: 600, margin: '30px auto' }}>
      <OrderGrid
        orders={orders}
        renderHeader={orders =>
          <div>
            <h4>Order Grid ({orders.length} orders)</h4>
          </div>
        }
        renderItem={order => (
          <div key={order.id} style={{ border: '1px solid #efefef', borderRadius: 6, padding: 12 }}>
            <strong>{order.customer}</strong>
            <div>Order #{order.id}</div>
            <div>Status: <span>{order.status}</span></div>
            <div>Amount: ₹{order.amount}</div>
          </div>
        )}
      />
    </div>
  );
}

export default App;

Handling Forms and Inputs: Controlled vs Uncontrolled

Reusable component systems often need to handle complex forms. As discussed previously (Controlled vs. Uncontrolled Inputs in React), the best practice is to use controlled inputs, keeping form state inside your React components for predictability and reusability.

Example: Reusable Order Input Form

// src/components/OrderForm.jsx

import { useState } from 'react';

function OrderForm({ onSave }) {
  const [customer, setCustomer] = useState('');
  const [amount, setAmount] = useState('');
  const [status, setStatus] = useState('Pending');

  const handleSubmit = e => {
    e.preventDefault();
    if (customer && amount) {
      onSave({
        id: Math.floor(Math.random() * 100000),
        customer,
        amount: parseInt(amount),
        status
      });
      setCustomer('');
      setAmount('');
      setStatus('Pending');
    }
  };

  return (
    <form onSubmit={handleSubmit} style={{ marginBottom: 20 }}>
      <input
        value={customer}
        onChange={e => setCustomer(e.target.value)}
        placeholder="Customer name"
        required
      />
      <input
        value={amount}
        onChange={e => setAmount(e.target.value.replace(/\D/g, ""))}
        placeholder="Amount (₹)"
        type="number"
        required
      />
      <select value={status} onChange={e => setStatus(e.target.value)}>
        <option value="Pending">Pending</option>
        <option value="Processing">Processing</option>
        <option value="Delivered">Delivered</option>
      </select>
      <button type="submit">Add Order</button>
    </form>
  );
}

export default OrderForm;

How to use/test this in your app:

// src/App.js

import { useState } from 'react';
import OrderForm from './components/OrderForm';
import OrderList from './components/OrderList';

function App() {
  const [orders, setOrders] = useState([]);

  const saveOrder = order => setOrders(prev => [order, ...prev]);

  return (
    <div style={{ maxWidth: 600, margin: '30px auto' }}>
      <OrderForm onSave={saveOrder} />
      <OrderList orders={orders} />
    </div>
  );
}

export default App;

Further Tips for Designing Reusable Components

  • Use Prop Types or TypeScript: Clearly define expected prop types for every reusable component for safety and editor hints.
  • Document Each Component Well: Leave jsdoc comments or simple code samples at the top of each file.
  • Decouple Styles: Pass styles via props or leverage CSS modules/styled-components for real-world usage.
  • Avoid Deep Nesting of Logic: Keep logic specific to usage in parent components, letting children focus on display and basic handling.
  • Test Extensively in Isolation: Try every edge case in App.js or a Storybook setup.

Recap & What’s Next

Reusable component systems help your React codebase scale, stay tidy, and adapt to changing demands. With the patterns above—composition, render props, and testing approaches—your order management apps (and any other complex UIs) will remain maintainable, fun, and robust.​


Up Next:
The following article will dive into “Parent-child communication and lifting state up,” giving you the skills to coordinate between components and manage shared data flows—essential for feature-rich, dynamic UIs.

Stay tuned, and happy coding!