惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Spread Privacy
Spread Privacy
V
Visual Studio Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Recorded Future
Recorded Future
云风的 BLOG
云风的 BLOG
Microsoft Azure Blog
Microsoft Azure Blog
I
InfoQ
Apple Machine Learning Research
Apple Machine Learning Research
MyScale Blog
MyScale Blog
M
MIT News - Artificial intelligence
WordPress大学
WordPress大学
Recent Announcements
Recent Announcements
V
V2EX
The GitHub Blog
The GitHub Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Jina AI
Jina AI
小众软件
小众软件
aimingoo的专栏
aimingoo的专栏
V
Vulnerabilities – Threatpost
C
Check Point Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
AI
AI
宝玉的分享
宝玉的分享
P
Proofpoint News Feed
量子位
Attack and Defense Labs
Attack and Defense Labs
H
Hackread – Cybersecurity News, Data Breaches, AI and More
P
Privacy International News Feed
Google DeepMind News
Google DeepMind News
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
C
CERT Recently Published Vulnerability Notes
腾讯CDC
Latest news
Latest news
Google DeepMind News
Google DeepMind News
The Register - Security
The Register - Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
G
GRAHAM CLULEY
Blog — PlanetScale
Blog — PlanetScale
博客园_首页
美团技术团队
The Cloudflare Blog
T
Tenable Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
J
Java Code Geeks
SecWiki News
SecWiki News
Webroot Blog
Webroot Blog
N
News | PayPal Newsroom
博客园 - 叶小钗
博客园 - Franky

Dmitri Pavlutin Blog

Pure Functions in JavaScript: A Beginner's Guide Record Type in TypeScript: A Quick Intro How to Write Comments in React: The Good, the Bad and the Ugly 4 Ways to Create an Enum in JavaScript React forwardRef(): How to Pass Refs to Child Components TypeScript Function Types: A Beginner's Guide How to Use v-model to Access Input Values in Vue Mastering Vue refs: From Zero to Hero Environment Variables in JavaScript: process.env 5 Must-Know Differences Between ref() and reactive() in Vue How to Destructure Props in Vue (Composition API) Triangulation in Test-Driven Development How to Use nextTick() in Vue Programming to Interface Vs to Implementation A Smarter JavaScript Mapper: array.flatMap() Array Grouping in JavaScript: Object.groupBy() How to Access ES Module Metadata using import.meta JSON Modules in JavaScript How to Trim Strings in JavaScript TypeScript Function Overloading How to Debounce and Throttle Callbacks in Vue How to Show/Hide Elements in Vue Sparse vs Dense Arrays in JavaScript How to Fill an Array with Initial Values in JavaScript Covariance and Contravariance in TypeScript What are Higher-Order Functions in JavaScript? How to Use TypeScript with React Components Index Signatures in TypeScript How to Use React useReducer() Hook unknown vs any in TypeScript A Guide to React Context and useContext() Hook How to Use Promise.any() 2 Ways to Remove a Property from an Object in JavaScript 'return await promise' vs 'return promise' in JavaScript How to Use Promise.allSettled() How to Use fetch() with JSON JavaScript Promises: then(f,f) vs then(f).catch(f) What is a Promise in JavaScript? How to Use Promise.all() A Simple Guide to Component Props in React Don't Stop Me Now: How to Use React useTransition() hook A Simple Explanation of JavaScript Variables: const, let, var ES Modules Dynamic Import How to Memoize with React.useMemo() How to Cleanup Async Effects in React Why Math.max() Without Arguments Returns -Infinity How to Debounce and Throttle Callbacks in React Don't Confuse Function Expressions and Function Declarations in JavaScript How to Use ES Modules in Node.js Solving a Mystery Behavior of parseInt() in JavaScript How to Use Array Reduce Method in JavaScript 3 Ways to Merge Arrays in JavaScript A Guide to Jotai: the Minimalist React State Management Library The Difference Between Values and References in JavaScript How to Implement a Queue in JavaScript A Helpful Algorithm to Determine "this" value in JavaScript React useRef() Hook Explained in 3 Steps 7 Interview Questions on "this" keyword in JavaScript. Can You Answer Them? How to Greatly Enhance fetch() with the Decorator Pattern 7 Interview Questions on JavaScript Closures. Can You Answer Them? What's a Method in JavaScript? array.sort() Does Not Simply Sort Numbers in JavaScript How to Solve the Infinite Loop of React.useEffect() The New Array Method You'll Enjoy: array.at(index) What's the Difference between DOM Node and Element? Why Promises Are Faster Than setTimeout()? Everything About Callback Functions in JavaScript How React Updates State 5 Mistakes to Avoid When Using React Hooks 5 Best Practices to Write Quality JavaScript Variables Type checking in JavaScript: typeof and instanceof operators 3 Ways to Check if a Variable is Defined in JavaScript React Forms Tutorial: Access Input Values, Validate, Submit Forms Prototypal Inheritance in JavaScript How to Timeout a fetch() Request How to Learn JavaScript If You're a Beginner A Simple Explanation of React.useEffect() A Simple Explanation of JavaScript Iterators How to Use React Controlled Inputs Everything about null in JavaScript How to Use Fetch with async/await Getting Started with Arrow Functions in JavaScript An Interesting Explanation of async/await in JavaScript Front-end Architecture: Stable and Volatile Dependencies Is it Safe to Compare JavaScript Strings? How to Access Object's Keys, Values, and Entries in JavaScript What Actually is a String in JavaScript? 3 Ways to Shallow Clone Objects in JavaScript (w/ bonuses) Checking if an Array Contains a Value in JavaScript JavaScript Event Delegation: A Beginner's Guide How to Parse URL in JavaScript: hostname, pathname, query, hash 3 Ways to Detect an Array in JavaScript How to Get the Screen, Window, and Web Page Sizes in JavaScript 3 Ways to Check If an Object Has a Property/Key in JavaScript How to Compare Objects in JavaScript Object.is() vs Strict Equality Operator in JavaScript Own and Inherited Properties in JavaScript 5 Differences Between Arrow and Regular Functions How to Use Object Destructuring in JavaScript Your Guide to React.useCallback()
How to Solve Render Props Callback Hell
Dmitri Pavlutin · 2019-09-24 · via Dmitri Pavlutin Blog

A good design of React components is the key to a maintainable and easy to change codebase.

In this sense, React offers a lot of design techniques like composition, hooks, higher-order components, render props, and more.

Render props is efficient to design components in a loosely coupled manner. Its essence consists in using a special prop (usually named render) that delegates the rendering logic to the parent component:


import Mouse from 'Mouse';

function ShowMousePosition() {

return (

<Mouse

render={({ x, y }) => <div>Position: {x}px, {y}px</div> }

/>

);

}


When using this pattern, sooner or later you'll face a problem of nesting components within multiple render prop callbacks: render props callback hell.

In this post, I will describe 3 simple and efficient approaches on how to solve this problem: using class component, function composition or react-adopt tool.

1. Render props callback hell

Let's say you need to detect and display the city of the website visitor.

First, you'll need the component that determines the user's geographical coordinates. A component like <AsyncCoords render={coords => ... } /> makes an async operation, for example using Geolocation API, then calls render prop callback with the coordinates.

Then these coordinates are used to determine approximately the user's city: <AsyncCity lat={lat} long={long} render={city => ...} />. This component also calls render prop as soon as the city is known.

The implementation details of <AsyncCoords> and <AsynCity> are unimportant for now. What's important is that the component calls render prop callback as soon as the result is ready.

Let's combine these async components into <DetectCity>:


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>;

}}

/>

);

}}

/>

);

}

// Somewhere

<DetectCity />


You might already spot the issue: the nesting of the render prop callback functions. The more callbacks are nested, the harder it is going to understand the code. This is the render props callback hell problem.

Let's find better component designs to exclude the nesting of the callbacks.

How to solve React render props callback hell

2. Class approach

To transform the nesting of callbacks into something more readable, let's refactor the callbacks into methods of a class.

The refactored <DetectCity> to a class component might look like this:


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>;

}

}

// Somewhere

<DetectCity />


The callbacks were extracted into separated methods renderCoords() and renderCity(). Such component design is easier to understand because the rendering logic is encapsulated into a separated method.

If you need even more nesting, the class will grow vertically (by adding new methods) rather than horizontally (by nesting functions into each other). The callback hell problem vanishes.

2.1 Access component props inside render methods

The new render methods renderCoors() and renderCity() are defined using a fat arrow method syntax. It allows to bind this value to the component instance, even if these methods are invoked somewhere inside <AsyncCoords> and <AsyncCity>.

Having this as the component instance is useful if you want to access the props of the main component inside the render methods:


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 => {

const { noCityMessage } = this.props;

if (city == null) {

return <div>{noCityMessage}</div>;

}

return <div>You might be in {city}.</div>;

}

}

// Somewhere

<DetectCityMessage noCityMessage="Unable to detect city." />


Inside renderCity() the this value points to the <DetectCityMessage> component instance. Now it's easy to access noCityMessage prop from this.props.

3. Function composition approach

If you want a lighter approach that doesn't involve creating classes, you can simply use function composition.

In such case the refactored <DetectCity> might look like this:


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 />


Now, instead of creating a class with methods, regular functions renderCoors() and renderCity() encapsulate the rendering logic.

If you need more nesting, you can simply add new functions. The code will grow vertically (by adding new functions) rather than horizontally (by nesting), solving this way the callback hell problem.

One more benefit of this approach is that you can test in isolation the render functions: renderCoords() and renderCity(). It would help achieve easier higher code coverage. Plus, you can always create a module for each render function and reuse it.

3.1 Access component props inside render functions

If you need to access the props inside the render functions, you could insert the render functions directly inside 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." />


While this structuring works, I don't like it much because every time <DetectCityMessage> re-renders, new function instances of renderCoords() and renderCity() are created.

The already mentioned class approach might be better to use. Render methods are always bound to the component instance, so you can access this.props without hassle. At the same time the methods are not recreated on every re-render.

4. Utility approach

React-adopt utility is a decent alternative if you want more flexibility on how to handle the render props callbacks.

The refactored <DetectCity> component by using react-adopt is as follows:


import { adopt } from 'react-adopt';

const Composed = adopt({

coords: ({ render }) => <AsyncCoords render={render} />,

city: ({ coords: { lat, long }, render }) => (

<AsyncCity lat={lat} long={long} render={render} />

)

});

function DetectCity() {

return (

<Composed>

{ city => {

if (city == null) {

return <div>Unable to detect city.</div>;

}

return <div>You might be in {city}.</div>;

}}

</Composed>

);

}

// Somewhere

<DetectCity />


react-adopt requires a special mapper Composed that describes the order of async operations. At the same time, the library takes care of creating the customized render callbacks to ensure the correct async execution order.

As you might notice, the above example using react-adopt requires more code than the approaches using a class component or function composition. So why bother using react-adopt?

Unfortunately, the class component and function composition approaches are not suitable if you need to aggregate the results of multiple render props.

Let's detail this limitation.

4.1 Aggregate multiple render props results

Imagine a situation when you want to render the result of 3 render prop callbacks (AsyncFetch1, AsyncFetch2, AsyncFetch3):


function MultipleFetchResult() {

return (

<AsyncFetch1 render={result1 => (

<AsyncFetch2 render={result2 => (

<AsyncFetch3 render={result3 => (

<span>

Fetch result 1: {result1}

Fetch result 2: {result2}

Fetch result 3: {result3}

</span>

)} />

)} />

)} />

);

}

// Somewhere

<MultipleFetchResult />


<MultipleFetchResult> component renders the result of all 3 async fetch actions. That's a nasty callback hell situation.

If you try to use the class component or function composition approach, it's going to be troublesome. The callback hell transforms into arguments binding hell:


class MultipleFetchResult extends React.Component {

render() {

return <AsyncFetch1 render={this.renderResult1} />;

}

renderResult1(result1) {

return (

<AsyncFetch2

render={this.renderResult2.bind(this, result1)}

/>

);

}

renderResult2(result1, result2) {

return (

<AsyncFetch2

render={this.renderResult3.bind(this, result1, result2)}

/>

);

}

renderResult3(result1, result2, result3) {

return (

<span>

Fetch result 1: {result1}

Fetch result 2: {result2}

Fetch result 3: {result3}

</span>

);

}

}

// Somewhere

<MultipleFetchResult />


You have to manually bind the result of render prop callbacks until they finally reach renderResult3() method.

If you don't like manual binding, react-adopt could work better. Let's see a refactored version using this utility:


import { 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 />


The render props results are ready inside the function ({ result1, result2, result3 }) => {...} supplied to <Composed>. Thus you don't have to manually bind arguments or nest callbacks.

Of course react-adopt comes with the price of additional abstractions to learn and a slight app size increase.

5. Conclusion

Render prop is an efficient technique to design React components. However, one problem that affects its usability is the render props callback hell.

For simple situations when the render props results are used in a chain, the function composition or class component approaches work well.

But if you have a more complex case, with multiple render props callbacks using the result of each other, react-adopt will make your code lighter and easier to understand.

Do you know other effective ways to solve render props callback hell? Please write a comment below!