React Render Prop Pattern

6 min read

React Render Prop Pattern

Introduction

Sometimes when creating React components, we want to make it as reusable as possible to support different use-cases. One technique to achieve this is called the Render Props technique. According to the ReactJS docs,

The term render prop refers to a technique for sharing code between React components using a prop whose value is a function. -ReactJS docs

Furthermore,

A component with a render prop takes a function that returns a React element and calls it instead of implementing its own render logic. -ReactJS docs

Applying the Render Prop Technique

Let's say we need an AppList component and one of the requirements is to have its list items to be configurable, that is, it can render any component/ui.

We can start with something very simple like the one below:

AppList.tsx
import { type ReactNode } from 'react';

interface AppListProps {
  data: any[];
  renderItem: (item: any) => ReactNode;
}

function AppList(props: AppListProps) {
  const { data, renderItem } = props;

  return (
    <ul>
      {data.map((d) => {
        return <li key={d}>{renderItem(d)}</li>;
      })}
    </ul>
  );
}

export default AppList;

Our AppList component can take a data prop which is any array and a renderItem prop which is a function which has a data item as its argument. The renderItem prop here is our render prop. We can use the current implementation as follows:

App.tsx
const listData = ['Matthew', 'Mark', 'Luke', 'John', 'Paul'];

function App() {
  return <AppList data={listData} renderItem={(item) => item} />;
}

export default App;

That works. We can also return JSX to render an item like this:

App.tsx
<AppList
  data={listData}
  renderItem={(item) => (
    <button>
      <span>{item} 👋</span>
    </button>
  )}
/>

This renders like this:

A sample output of the AppList component

The render prop just works because by definition it is a function prop that any component can use to know what to render.

Supporting Array of Objects

That works indeed. But so far we're assuming the data is just a plain array of, say, strings or numbers. How about when it is an array of objects? This should be supported by our AppList component because most of the times application data are objects.

Well, it is quite simple. Let's change our data first:

interface Apostle {
  name: string;
  age: number;
}

const listData: Apostle[] = [
  {
    name: 'Matthew',
    age: 45,
  },
  {
    name: 'Mark',
    age: 38,
  },
  {
    name: 'Luke',
    age: 52,
  },
  {
    name: 'John',
    age: 39,
  },
  {
    name: 'Paul',
    age: 43,
  },
];

We can just pass that as our new list data:

App.tsx
function App() {
  return (
    <AppList
      data={listData}
      renderItem={(item) => (
        <button>
          <span>
            Hi! I'm {item.name} 👋. I'm {item.age} years old.
          </span>
        </button>
      )}
    />
  );
}

Output:

A sample output of the AppList component

Solving the key Problem

That works again but now we have a problem. Can you guess it? Open your devtools and face this error:

An error on keys thrown by ReactJs

What is the culprit? Let us revisit the AppList component. Note that in line 14 we're passing an item called d but given our new data structure, it is now an Apostle object.

AppList.tsx
import { type ReactNode } from 'react';

interface AppListProps {
  data: any[];
  renderItem: (item: any) => ReactNode;
}

function AppList(props: AppListProps) {
  const { data, renderItem } = props;

  return (
    <ul>
      {data.map((d) => {
        return <li key={d}>{renderItem(d)}</li>;
      })}
    </ul>
  );
}

export default AppList;

What can we do to fix this? First we need to make the AppList component generic.

AppList.tsx
import { type ReactNode } from 'react';

interface AppListProps<T> {
  data: T[];
  renderItem: (item: T) => ReactNode;
}

function AppList<T>(props: AppListProps<T>) {
  const { data, renderItem } = props;

  return (
    <ul>
      {data.map((d) => {
        return <li key={d}>{renderItem(d)}</li>;
      })}
    </ul>
  );
}

Then let's add a new prop called keyExtractor in order for the consumer of our component to decide how a key will be assigned to each list item:

AppList.tsx
import { type ReactNode } from 'react';

interface AppListProps<T> {
  data: T[];
  renderItem: (item: T) => ReactNode;
  keyExtractor: (item: T) => string;
}

function AppList<T>(props: AppListProps<T>) {
  const { data, renderItem, keyExtractor } = props;

  return (
    <ul>
      {data.map((d) => {
        return <li key={keyExtractor(d)}>{renderItem(d)}</li>;
      })}
    </ul>
  );
}

We use that in App.tsx as follows:

App.tsx
function App() {
  return (
    <AppList
      data={listData}
      keyExtractor={(item) => item.name}
      renderItem={(item) => (
        <button>
          <span>
            Hi! I'm {item.name} 👋. I'm {item.age} years old.
          </span>
        </button>
      )}
    />
  );
}

One advantage of this approach is that the user does not need to pass something like an onClick prop to the AppList itself. The user of our component can pass that directly to the very list item component being rendered by the AppList.

App.tsx
function App() {
  return (
    <AppList
      data={listData}
      keyExtractor={(item) => item.name}
      renderItem={(item) => (
        <button onClick={() => alert(`Hi! I am ${item.name}`)}>
          <span>
            Hi! I'm {item.name} 👋. I'm {item.age} years old.
          </span>
        </button>
      )}
    />
  );
}

Code clean up

Let us clean our App.tsx a little bit by replacing item with what entity it really is, in this case an apostle entity.

App.tsx
function App() {
  return (
    <AppList
      data={listData}
      keyExtractor={(apostle) => apostle.name}
      renderItem={(apostle) => (
        <button onClick={() => alert(`Hi! I am ${apostle.name}`)}>
          <span>
            Hi! I'm {apostle.name} 👋. I'm {apostle.age} years old.
          </span>
        </button>
      )}
    />
  );
}

Where to go from here?

What you have learned is not all that you can do with render props. You can also use this pattern to pass state values from one component to another. This use-case is well utilized in many React component libraries.

Conclusion

A time will come where we'll face the need to create components that are flexible enough to support most of our use cases. For such need, design patterns can be a great help. In this post we're able to utilize one pattern called the render prop technique to create a flexible, reusable component.

Happy coding!

-jep