1,124 words, 6 minutes read time.

As you build more complex web applications with React, one of the most frequent tasks you’ll encounter is rendering lists of items like users, products, or articles. Often, you’ll find that different parts of your application need to display the same data, but with varying levels of detail. For example, a user list might show just the names in one part of the app, while elsewhere, you may want to include profile pictures and contact details.
In these scenarios, creating a new list component for each use case might seem like the solution, but it leads to code duplication and a harder-to-maintain codebase. Fortunately, there’s a better approach: building reusable list components with custom render functions. This strategy provides the flexibility to handle different render requirements without creating multiple components.
In this guide, we’ll explore how to create a reusable list component in React using custom render functions and how this pattern can save you both time and headaches in the long run. Let’s dive in.
The Problem: List Components with Multiple Rendering Needs
Suppose you are working on an e-commerce application, where the same product data needs to be displayed in several formats:
- A basic product list for quick browsing (just product names).
- A detailed list that includes prices, descriptions, and images for deeper dives into the product catalog.
Traditionally, you might create separate components for each scenario, but this method quickly leads to duplicated code, increasing the risk of bugs and making future maintenance difficult.
By using a render function, we can centralize the list rendering logic into one component and delegate the rendering of each individual item to a customizable function passed in as a prop.
The Solution: Building a Flexible List Component
The solution to this problem is to build a reusable list component that takes a render function as a prop. This function allows the parent component to control how each item in the list is rendered without modifying the list component itself.
Let’s walk through an example of how to do this from scratch.
Creating the Base List Component
Here’s how you can create a simple, reusable list component using a render prop pattern. This list component will accept data (in the form of an array) and a renderItem function, which defines how each list item should appear.
import React from 'react';
const List = ({ items, renderItem }) => {
return (
<div>
{items.map((item, index) => (
<div key={index} className="list-item">
{renderItem(item)}
</div>
))}
</div>
);
};
export default List;
In this component:
itemsis an array of data that we want to display.renderItemis a function passed by the parent component that defines how each item in the list should be rendered.
Implementing the List in Different Use Cases
Now, let’s see how this reusable List component can be used in different contexts.
1. Basic Product List
For a basic product list, we might only want to show the names of the products. Here’s how you can achieve that using our List component:
const products = [
{ id: 1, name: 'Wireless Mouse' },
{ id: 2, name: 'Mechanical Keyboard' },
{ id: 3, name: 'Noise-Canceling Headphones' },
];
const BasicProductList = () => {
return (
<List
items={products}
renderItem={(product) => <p>{product.name}</p>}
/>
);
};
export default BasicProductList;
In this example, we pass a simple renderItem function that returns a <p> element containing the product’s name. Notice how we don’t have to alter the List component itself to change how the items are rendered.
2. Detailed Product List
Now, let’s say we want a more detailed product list that includes not only the name but also the price and a short description of each product:
const products = [
{ id: 1, name: 'Wireless Mouse', price: '$25', description: 'A smooth and responsive mouse for everyday use.' },
{ id: 2, name: 'Mechanical Keyboard', price: '$90', description: 'A high-quality keyboard for typing enthusiasts.' },
{ id: 3, name: 'Noise-Canceling Headphones', price: '$150', description: 'Block out the noise and enjoy your music.' },
];
const DetailedProductList = () => {
return (
<List
items={products}
renderItem={(product) => (
<div>
<h3>{product.name}</h3>
<p>{product.price}</p>
<p>{product.description}</p>
</div>
)}
/>
);
};
export default DetailedProductList;
In this case, the renderItem function renders a div for each product, containing its name, price, and description. Once again, we can reuse the same List component, while only modifying how individual items are rendered.
The Benefits of Using Custom Render Functions
1. Flexibility
The most significant advantage of this pattern is flexibility. By passing a render function, the list component becomes highly adaptable. You can easily switch between different rendering strategies without rewriting your list logic or duplicating code.
2. Separation of Concerns
This approach neatly separates concerns. The List component is responsible for iterating over the data, while the renderItem function handles the rendering of individual items. This clear separation makes the code easier to understand and maintain.
3. Reduced Code Duplication
Without this pattern, you might be forced to create different components for each use case (e.g., BasicProductList, DetailedProductList). But with custom render functions, you avoid duplicating list logic and keep your code DRY (Don’t Repeat Yourself).
4. Improved Scalability
As your application grows, you might need to render the same data in various formats. Using this reusable list component, you can scale easily without needing to rewrite core logic. New use cases become a matter of passing a new renderItem function rather than creating a new component from scratch.
Expanding the Concept
Now that we’ve seen the basics, let’s explore how this approach can be further expanded. In larger applications, you may need additional features like:
- Loading States: Display a loading spinner while the data is being fetched.
- Error Handling: Show an error message if the data fails to load.
- Empty States: Render a fallback message or image when there’s no data to display.
Here’s an example of adding an empty state to our reusable list component:
const List = ({ items, renderItem, emptyMessage = 'No items available.' }) => {
if (items.length === 0) {
return <p>{emptyMessage}</p>;
}
return (
<div>
{items.map((item, index) => (
<div key={index} className="list-item">
{renderItem(item)}
</div>
))}
</div>
);
};
This version of List displays a fallback message if the items array is empty. The message can also be customized via the emptyMessage prop.
Final Thoughts
By building reusable list components with custom render functions, you can create a highly flexible architecture that’s easy to scale and maintain. This pattern keeps your code DRY, reduces duplication, and gives you the flexibility to adapt list rendering to various parts of your application.
The render prop pattern is a widely accepted best practice in React, and it’s particularly useful for list components. Whether you’re rendering simple lists or complex data structures, this approach will help you create clean, reusable, and maintainable components.
If you’re looking to improve the flexibility and scalability of your React application, start using custom render functions in your lists today!
Hat tip: A big thank you to Vishnu Satheesh for the original article titled “How to Build Reusable List Components in React with Custom Render Functions?” which inspired this blog.
