Interested in our next book? Learn more about Building Large-scale JavaScript Web Apps with React

Design Pattern

Render Props Pattern

In the section on Higher Order Components, we saw that being able to reuse component logic can be very convenient if multiple components need access to the same data, or contain the same logic.

Another way of making components very reusable, is by using the render prop pattern. A render prop is a prop on a component, which value is a function that returns a JSX element. The component itself does not render anything besides the render prop. Instead, the component simply calls the render prop, instead of implementing its own rendering logic.

Imagine that we have a Title component. In this case, the Title component shouldn’t do anything besides rendering the value that we pass. We can use a render prop for this! Let’s pass the value that we want the Title component to render to the render prop.

<Title render={() => <h1>I am a render prop!</h1>} />

Within the Title component, we can render this data by returning the invoked render prop!

const Title = (props) => props.render();

To the Component element, we have to pass a prop called render, which is a function that returns a React element.

index.js
1import React from "react";
2import { render } from "react-dom";
3
4import "./styles.css";
5
6const Title = (props) => props.render();
7
8render(
9 <div className="App">
10 <Title
11 render={() => (
12 <h1>
13 <span role="img" aria-label="emoji">
14
15 </span>
16 I am a render prop!{" "}
17 <span role="img" aria-label="emoji">
18
19 </span>
20 </h1>
21 )}
22 />
23 </div>,
24 document.getElementById("root")
25);

Perfect, works smoothly! The cool thing about render props, is that the component that receives the prop is very reusable. We can use it multiple times, passing different values to the render prop each time.

index.js
1import React from "react";
2import { render } from "react-dom";
3import "./styles.css";
4
5const Title = (props) => props.render();
6
7render(
8 <div className="App">
9 <Title render={() => <h1>✨ First render prop! ✨</h1>} />
10 <Title render={() => <h2>🔥 Second render prop! 🔥</h2>} />
11 <Title render={() => <h3>🚀 Third render prop! 🚀</h3>} />
12 </div>,
13 document.getElementById("root")
14);

Although they’re called render props, a render prop doesn’t have to be called render. Any prop that renders JSX is considered a render prop! Let’s rename the render props that were used in the previous example, and give them specific names instead!

index.js
1import React from "react";
2import { render } from "react-dom";
3import "./styles.css";
4
5const Title = (props) => (
6 <>
7 {props.renderFirstComponent()}
8 {props.renderSecondComponent()}
9 {props.renderThirdComponent()}
10 </>
11);
12
13render(
14 <div className="App">
15 <Title
16 renderFirstComponent={() => <h1>✨ First render prop! ✨</h1>}
17 renderSecondComponent={() => <h2>🔥 Second render prop! 🔥</h2>}
18 renderThirdComponent={() => <h3>🚀 Third render prop! 🚀</h3>}
19 />
20 </div>,
21 document.getElementById("root")
22);

Great! We’ve just seen that we can use render props in order to make a component reusable, as we can pass different data to the render prop each time. But, why would you want to use this?

A component that takes a render prop usually does a lot more than simply invoking the render prop. Instead, we usually want to pass data from the component that takes the render prop, to the element that we pass as a render prop!

function Component(props) {
  const data = { ... }

  return props.render(data)
}

The render prop can now receive this value that we passed as its argument.

<Component render={data => <ChildComponent data={data} />}

Let’s look at an example! We have a simple app, where a user can type a temperature in Celsius. The app shows the value of this temperature in Fahrenheit and Kelvin.

App.js
1import React, { useState } from "react";
2import "./styles.css";
3
4function Input() {
5 const [value, setValue] = useState("");
6
7 return (
8 <input
9 type="text"
10 value={value}
11 onChange={e => setValue(e.target.value)}
12 placeholder="Temp in °C"
13 />
14 );
15}
16
17export default function App() {
18 return (
19 <div className="App">
20 <h1>☃️ Temperature Converter 🌞</h1>
21 <Input />
22 <Kelvin />
23 <Fahrenheit />
24 </div>
25 );
26}
27
28function Kelvin({ value = 0 }) {
29 return <div className="temp">{value + 273.15}K</div>;
30}
31
32function Fahrenheit({ value = 0 }) {
33 return <div className="temp">{(value * 9) / 5 + 32}°F</div>;
34}

Hmm.. Currently there’s a problem. The stateful Input component contains the value of the user’s input, meaning that the Fahrenheit and Kelvin component don’t have access to the user’s input!


Lifting state

One way to make the users input available to both the Fahrenheit and Kelvin component in the above example, we’d have to lift the state.

In this case, we have a stateful Input component. However, the sibling components Fahrenheit and Kelvin also need access to this data. Instead of having a stateful Input component, we can lift the state up to the first common ancestor component that has a connection to Input, Fahrenheit and Kelvin: the App component in this case!

function Input({ value, handleChange }) {
  return <input value={value} onChange={(e) => handleChange(e.target.value)} />;
}

export default function App() {
  const [value, setValue] = useState("");

  return (
    <div className="App">
      <h1>☃️ Temperature Converter 🌞</h1>
      <Input value={value} handleChange={setValue} />
      <Kelvin value={value} />
      <Fahrenheit value={value} />
    </div>
  );
}

Although this is a valid solution, it can be tricky to lift state in larger applications with components that handle many children. Each state change could cause a re-render of all the children, even the ones that don’t handle the data, which could negatively affect the performance of your app.


Render props

Instead, we can use render props! Let’s change the Input component in a way that it can receive render props.

function Input(props) {
  const [value, setValue] = useState("");

  return (
    <>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="Temp in °C"
      />
      {props.render(value)}
    </>
  );
}

export default function App() {
  return (
    <div className="App">
      <h1>☃️ Temperature Converter 🌞</h1>
      <Input
        render={(value) => (
          <>
            <Kelvin value={value} />
            <Fahrenheit value={value} />
          </>
        )}
      />
    </div>
  );
}

Perfect, the Kelvin and Fahrenheit components now have access to the value of the user’s input!

App.js
1import React, { useState } from "react";
2import "./styles.css";
3
4function Input(props) {
5 const [value, setValue] = useState("");
6
7 return (
8 <>
9 <input
10 type="text"
11 value={value}
12 onChange={e => setValue(e.target.value)}
13 placeholder="Temp in °C"
14 />
15 {props.render(value)}
16 </>
17 );
18}
19
20export default function App() {
21 return (
22 <div className="App">
23 <h1>☃️ Temperature Converter 🌞</h1>
24 <Input
25 render={value => (
26 <>
27 <Kelvin value={value} />
28 <Fahrenheit value={value} />
29 </>
30 )}
31 />
32 </div>
33 );
34}
35
36function Kelvin({ value }) {
37 return <div className="temp">{parseInt(value || 0) + 273.15}K</div>;
38}
39
40function Fahrenheit({ value }) {
41 return <div className="temp">{(parseInt(value || 0) * 9) / 5 + 32}°F</div>;
42}

Children as a function

Besides regular JSX components, we can pass functions as children to React components. This function is available to us through the children prop, which is technically also a render prop.

Let’s change the Input component. Instead of explicitly passing the render prop, we’ll just pass a function as a child for the Input component.

export default function App() {
  return (
    <div className="App">
      <h1>☃️ Temperature Converter 🌞</h1>
      <Input>
        {(value) => (
          <>
            <Kelvin value={value} />
            <Fahrenheit value={value} />
          </>
        )}
      </Input>
    </div>
  );
}

We have access to this function, through the props.children prop that’s available on the Input component. Instead of calling props.render with the value of the user input, we’ll call props.children with the value of the user input.

function Input(props) {
  const [value, setValue] = useState("");

  return (
    <>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="Temp in °C"
      />
      {props.children(value)}
    </>
  );
}

Great, this way the Kelvin and Fahrenheit component have access to the value, without having to worry about the name of the renderprop.

App.js
1import React, { useState } from "react";
2import "./styles.css";
3
4function Input(props) {
5 const [value, setValue] = useState(0);
6
7 return (
8 <>
9 <input
10 type="number"
11 value={value}
12 onChange={e => setValue(e.target.value)}
13 placeholder="Temp in °C"
14 />
15 {props.children(value)}
16 </>
17 );
18}
19
20export default function App() {
21 return (
22 <div className="App">
23 <h1>☃️ Temperature Converter 🌞</h1>
24 <Input>
25 {value => (
26 <>
27 <Kelvin value={value} />
28 <Fahrenheit value={value} />
29 </>
30 )}
31 </Input>
32 </div>
33 );
34}
35
36function Kelvin({ value }) {
37 return <div className="temp">{parseInt(value || 0) + 273.15}K</div>;
38}
39
40function Fahrenheit({ value }) {
41 return <div className="temp">{(parseInt(value || 0) * 9) / 5 + 32}°F</div>;
42}

Hooks

In some cases, we can replace render props with Hooks. A good example of this is Apollo Client.

No experience with Apollo Client is needed to understand this example.

One way to use Apollo Client is through the Mutation and Query components. Let’s look at the same Input example that was covered in the Higher Order Components section. Instead of using the graphql() higher order component, we’ll now use the Mutation component that receives a render prop.

InputRenderProp.js
1import React from "react";
2import "./styles.css";
3
4import { Mutation } from "react-apollo";
5import { ADD_MESSAGE } from "./resolvers";
6
7export default class Input extends React.Component {
8 constructor() {
9 super();
10 this.state = { message: "" };
11 }
12
13 handleChange = (e) => {
14 this.setState({ message: e.target.value });
15 };
16
17 render() {
18 return (
19 <Mutation
20 mutation={ADD_MESSAGE}
21 variables={{ message: this.state.message }}
22 onCompleted={() =>
23 console.log(`Added with render prop: ${this.state.message} `)
24 }
25 >
26 {(addMessage) => (
27 <div className="input-row">
28 <input
29 onChange={this.handleChange}
30 type="text"
31 placeholder="Type something..."
32 />
33 <button onClick={addMessage}>Add</button>
34 </div>
35 )}
36 </Mutation>
37 );
38 }
39}

In order to pass data down from the Mutation component to the elements that need the data, we pass a function as a child. The function receives the value of the data through its arguments.

<Mutation mutation={...} variables={...}>
  {addMessage => <div className="input-row">...</div>}
</Mutation>

Although we can still use the render prop pattern and is often preferred compared to the higher order component pattern, it has its downsides.

One of the downsides is deep component nesting. We can nest multiple Mutation or Query components, if a component needs access to multiple mutations or queries.

<Mutation mutation={FIRST_MUTATION}>
  {(firstMutation) => (
    <Mutation mutation={SECOND_MUTATION}>
      {(secondMutation) => (
        <Mutation mutation={THIRD_MUTATION}>
          {(thirdMutation) => (
            <Element
              firstMutation={firstMutation}
              secondMutation={secondMutation}
              thirdMutation={thirdMutation}
            />
          )}
        </Mutation>
      )}
    </Mutation>
  )}
</Mutation>

After the release of Hooks, Apollo added Hooks support to the Apollo Client library. Instead of using the Mutation and Query render props, developers can now directly access the data through the hooks that the library provides.

Let’s look at an example that uses the exact same data as we previously saw in the example with the Query render prop. This time, we’ll provide the data to the component by using the useQuery hook that Apollo Client provided for us.

InputHOC.js
InputHooks.js
1import React, { useState } from "react";
2import "./styles.css";
3
4import { useMutation } from "@apollo/react-hooks";
5import { ADD_MESSAGE } from "./resolvers";
6
7export default function Input() {
8 const [message, setMessage] = useState("");
9 const [addMessage] = useMutation(ADD_MESSAGE, {
10 variables: { message }
11 });
12
13 return (
14 <div className="input-row">
15 <input
16 onChange={(e) => setMessage(e.target.value)}
17 type="text"
18 placeholder="Type something..."
19 />
20 <button onClick={addMessage}>Add</button>
21 </div>
22 );
23}

By using the useQuery hook, we reduced the amount of code that was needed in order to provide the data to the component.


Pros

Sharing logic and data among several components is easy with the render props pattern. Components can be made very reusable, by using a render or children prop. Although the Higher Order Component pattern mainly solves the same issues, namely reusability and sharing data, the render props pattern solves some of the issues we could encounter by using the HOC pattern.

The issue of naming collisions that we can run into by using the HOC pattern no longer applies by using the render props pattern, since we don’t automatically merge props. We explicitly pass the props down to the child components, with the value provided by the parent component.

Since we explicitly pass props, we solve the HOC’s implicit props issue. The props that should get passed down to the element, are all visible in the render prop’s arguments list. This way, we know exactly where certain props come from.

We can separate our app’s logic from rendering components through render props. The stateful component that receives a render prop can pass the data onto stateless components, which merely render the data.


Cons

The issues that we tried to solve with render props, have largely been replaced by React Hooks. As Hooks changed the way we can add reusability and data sharing to components, they can replace the render props pattern in many cases.

Since we can’t add lifecycle methods to a render prop, we can only use it on components that don’t need to alter the data they receive.


References