Learn about Render Props callback hell solution through examples

  • 2021-09-12 00:04:50
  • OfStack

In short, as long as the value of an attribute in a component is a function, it can be said that the component uses the technology of Render Props. It sounds like that. What are the application scenarios of Render Props? Let's start with a simple example. If we want to implement a component that displays personal information, 1 may be implemented as follows:


const PersonInfo = props => (
 <div>
  <h1> Name: {props.name}</h1>
 </div>
);
//  Call 
<PersonInfo name='web Front end '/>

If you want to need another age on the PersonInfo component, we will do this:


const PersonInfo = props => (
 <div>
  <h1> Name: {props.name}</h1>
  <p> Age: {props.age}</[>
 </div>
);

//  Call 
<PersonInfo name='web Front end ' age='18'/>

Then, if you want to add links, you have to realize the logic of sending links inside PersonInfo components. Obviously, this way violates the opening and closing principle of one of the six principles of software development, that is, every modification needs to be modified inside the components.

Opening and closing principle: closing for modification and opening for expansion.

Is there any way to avoid modifying this way?

In the native js, if we have to do something after calling a function, we will use a callback function to handle this situation.

In react, we can use Render Props, which is actually like callback 1:


const PersonInfo = props => {
return props.render(props);
}
//  Use 

<PersonInfo 
 name='web Front end ' age = '18' link = 'link'
 render = {(props) => {
  <div>
   <h1>{props.name}</h1>
   <p>{props.age}</p>
   <a href="props.link" rel="external nofollow" ></a>
  </div>
 }}
/>

It is worth mentioning that it is not only the function passed in the render attribute that can be called Render Props. In fact, any attribute can be called Render Props as long as its value is a function. For example, in the above example, if the attribute name of render is changed to children, it is actually simpler to use:


const PersonInfo = props => {
  return props.children(props);
};

<PersonInfo name='web Front end ' age = '18' link = 'link'>
{(props) => (
  <div>
    <h1>{props.name}</h1>
    <p>{props.age}</p>
    <a href={props.link}></a>
  </div>
)}
</PersonInfo

In this way, you can write functions directly in PersonInfo tags, which is more intuitive than before in render.

So, Render Props in React you can understand it as the callback function in js.

Good design of React components is the key to maintainability and easy code change.

In this sense, React provides many design techniques, such as composition, Hooks, high-order components, Render, Props, and so on.

Render props can effectively design components in a loosely coupled way. Its essence is to delegate rendering logic to the parent component using a special prop (commonly known as render).


import Mouse from 'Mouse';
function ShowMousePosition() {
 return (
  <Mouse 
   render = {
    ({ x, y }) => <div>Position: {x}px, {y}px</div> 
   }
  />
 )
}

Sooner or later, when using this pattern, you will encounter the problem of nesting components in multiple render prop callbacks: render props callback hell.

1. Render Props callback hell

Suppose you need to detect and display the city where the website visitors are located.

First, you need to determine the components of the user's geographic coordinates, such as < AsyncCoords render={coords = > ...} Such components operate asynchronously, using Geolocation API, and then calling Render prop for a callback.

Then the obtained coordinates are used to approximate the user's city: < AsyncCity lat={lat} long={long} render={city = > ...} / > This component is also called Render prop.

Then let's merge these asynchronous components into < DetectCity > Component


function DetectCity() {
 return (
  <AsyncCoords 
   render={({ lat, long }) => {
    return (
     <AsyncCity 
      lat={lat} 
      long={long} 
      render={city => {
       if (city == null) {
        return <div>Unable to detect city.</div>;
       }
       return <div>You might be in {city}.</div>;
      }}
     />
    );
   }}
  />
 );
}
//  Use somewhere 
<DetectCity />

You may have found this problem: nesting of Render Prop callback functions. The more nested callback functions, the harder it is to understand the code. This is the Render Prop callback hell problem.

Let's change to a better component design to eliminate the nesting problem of callbacks.

2. Class method

To convert the nesting of callbacks into more readable code, we refactor callbacks into methods of classes.


class DetectCity extends React.Component {
 render() {
  return <AsyncCoords render={this.renderCoords} />;
 }

 renderCoords = ({ lat, long }) => {
  return <AsyncCity lat={lat} long={long} render={this.renderCity}/>;
 }

 renderCity = city => {
  if (city == null) {
   return <div>Unable to detect city.</div>;
  }
  return <div>You might be in {city}.</div>;
 }
}

//  Use somewhere 
<DetectCity />

Callbacks are extracted into separate methods renderCoords () and renderCity (). This component design is easier to understand because the rendering logic is encapsulated in a separate method.

If more nesting is needed, classes are added vertically (by adding new methods) rather than horizontally (by nesting functions with each other), and the callback hell problem disappears.

2.1 Access the component props inside the rendering method

The methods renderCoors () and renderCity () are defined using the arrow function, so that this can be bound to the component instance, so you can use the < AsyncCoords > And < AsyncCity > These methods are called in the.

With this as a component instance, you can get what you need through prop:


class DetectCityMessage extends React.Component {
 render() {
  return <AsyncCoords render={this.renderCoords} />;
 }

 renderCoords = ({ lat, long }) => {
  return <AsyncCity lat={lat} long={long} render={this.renderCity}/>;
 }

 renderCity = city => {
  //  Look at this 
  const { noCityMessage } = this.props;
  if (city == null) {
   return <div>{noCityMessage}</div>;
  }
  return <div>You might be in {city}.</div>;
 }
}
<DetectCityMessage noCityMessage="Unable to detect city." />

The this value in renderCity () points to < DetectCityMessage > Component instance. It is now easy to get the value of noCityMessage from this. props.

3. Function combination method

If we want an easier method that doesn't involve creating classes, we can simply use function combinations.

Refactoring DetectCity components using function combinations:


function DetectCity() {
 return <AsyncCoords render={renderCoords} />;
}

function renderCoords({ lat, long }) {
 return <AsyncCity lat={lat} long={long} render={renderCity}/>;
}

function renderCity(city) {
 if (city == null) {
  return <div>Unable to detect city.</div>;
 }
 return <div>You might be in {city}.</div>;
}

// Somewhere
<DetectCity />

Rather than creating classes with methods, the regular functions renderCoors () and renderCity () now encapsulate the rendering logic.

If you need more nesting, just add a new function again. Code grows vertically (by adding new functions) rather than horizontally (by nesting), thus solving the callback hell problem.

Another benefit of this approach is that you can test the rendering functions separately: renderCoords () and renderCity ().

3.1 prop for accessing the internal components of the rendering function

If you need to access prop in the rendering function, you can insert the rendering function directly into the component


function DetectCityMessage(props) {
 return (
  <AsyncCoords 
   render={renderCoords} 
  />
 );

 function renderCoords({ lat, long }) {
  return (
   <AsyncCity 
    lat={lat} 
    long={long} 
    render={renderCity}
   />
  );
 }

 function renderCity(city) {
  const { noCityMessage } = props;
  if (city == null) {
   return <div>{noCityMessage}</div>;
  }
  return <div>You might be in {city}.</div>;
 }
}

// Somewhere
<DetectCityMessage noCityMessage="Unknown city." />

Although this structure works, I don't like it very much, because every time < DetectCityMessage > On re-rendering, new function instances of renderCoords () and renderCity () are created.

The class methods mentioned earlier may be more suitable for use. At the same time, these methods are not recreated every time they are re-rendered.

4. Practical methods

If you want more flexibility in how you handle render props callbacks, using React-adopt is a good choice.

Use react-adopt to reconstruct < DetectCity > Components:


const PersonInfo = props => (
 <div>
  <h1> Name: {props.name}</h1>
  <p> Age: {props.age}</[>
 </div>
);

//  Call 
<PersonInfo name='web Front end ' age='18'/>
0

react-adopt requires a special mapper to describe the sequence of asynchronous operations. At the same time, the library is responsible for creating custom rendering callbacks to ensure the correct asynchronous execution sequence.

You may notice that the above example using react-adopt requires more code than the method using a combination of class components or functions. So why use "react-adopt"?

Unfortunately, if you need to aggregate the results of multiple render props, then the class component and function combination method is not appropriate.

4.1 Aggregate the results of multiple rendering props

Imagine 1 when we render the results of three render prop callbacks (AsyncFetch1, AsyncFetch2, AsyncFetch3)


const PersonInfo = props => (
 <div>
  <h1> Name: {props.name}</h1>
  <p> Age: {props.age}</[>
 </div>
);

//  Call 
<PersonInfo name='web Front end ' age='18'/>
1

< MultipleFetchResult > Component is immersed in the results of all 3 asynchronous fetch operations, which is a case of being afraid of callback hell.

If you try to use the combination method of class components or functions, it will be troublesome. Callback hell to parameter binding hell:


const PersonInfo = props => (
 <div>
  <h1> Name: {props.name}</h1>
  <p> Age: {props.age}</[>
 </div>
);

//  Call 
<PersonInfo name='web Front end ' age='18'/>
2

We must manually bind the results of the render prop callbacks until they finally reach the renderResult3 () method.

If you don't like manual binding, it may be better to adopt react-adopt:


mport { adopt } from 'react-adopt';
const Composed = adopt({
 result1: ({ render }) => <AsyncFetch1 render={render} />,
 result2: ({ render }) => <AsyncFetch2 render={render} />,
 result3: ({ render }) => <AsyncFetch3 render={render} />
});
function MultipleFetchResult() {
 return (
  <Composed>
   {({ result1, result2, result3 }) => (
    <span>
     Fetch result 1: {result1}
     Fetch result 2: {result2}
     Fetch result 3: {result3}
    </span>
   )}
  </Composed>
 );
}

// Somewhere
<MultipleFetchResult />

In the function ({result1, result2, result3}) = > {…} Provided to < Composed > . Therefore, we don't have to bind parameters or nest callbacks manually.

Of course, the cost of react-adopt is to learn additional abstractions and slightly increase the size of the application.


Related articles: