Fixing the callback hell of over-parenting with React

Kasper Pihl Tornøe
4 min readAug 13, 2017

--

Today I want to share with you a small util that has helped us solve the callback hell that it can sometimes be with React components. It’s a really handy little util we call react-delegate and we pretty quickly ended up using it everywhere in our app.

You see when we started using react at Swipes, I pretty quickly grew tired of the callback hell you end up with when you build components. Things like this…

<Task
task={task}
onTaskComplete={this.onTaskComplete}
onTaskDelete={this.onTaskDelete}
onTaskSchedule={this.onTaskSchedule}
/>

Where a component can do a lot of things that it needs to tell its parent through callbacks. I call this over-parenting :) you need to leave a little room for the children to manage on their own.

Things gets especially complicated when you need to tell your grand-parents about what you are doing.

<Subtask
subtask={task.subtask}
onSubtaskComplete={this.props.onTaskComplete}
onSubtaskDelete={this.props.onTaskDelete}
onSubtaskSchedule={this.props.onTaskSchedule}
/>

I was including all these callbacks down the tree and I would deliberately try making as few callbacks as possible just to keep the code clean. Which I don’t think is a good idea, as component should be as descriptive as possible.

So how can you let a component be as descriptive as they want, without having to pass all methods around all the time.

So I came to React from native iOS development and has always been fascinated by Apples concept of delegation.

In short you can set a delegate, that will respond to certain methods. So instead of passing all the methods, you pass a reference to an object instead that will respond to some defined methods.

Say what? Okay let me show you. Say you have a TaskList and you want to render some Tasks, and that those tasks can tell your list when someone clicks complete, delete or schedule. In React this would normally look something like this:

/* BEFORE */
class TaskList extends React.Component {
constructor(props) {
super(props);
this.onTaskComplete = this.onTaskComplete.bind(this);
this.onTaskDelete = this.onTaskDelete.bind(this);
this.onTaskSchedule = this.onTaskSchedule.bind(this);
}
onTaskComplete() { /* completing stuff */ }
onTaskDelete() { /* deleting stuff */ }
onTaskSchedule() { /* scheduling stuff */ }

render() {
const { tasks } = this.props;

return tasks.map(task => (
<Task
task={task}
onTaskComplete={this.onTaskComplete}
onTaskDelete={this.onTaskDelete}
onTaskSchedule={this.onTaskSchedule}
/>
));
}
}

We tried different approaches but always ended up with a lot of methods passed down the tree as you see above. Today I’m proud to introduce a better way with react-delegate. Taking the best from both the iOS and React world. With react-delegate your task list would look like this:

/* AFTER */
class TaskList extends React.Component {
constructor(props) {
super(props);
}
onTaskComplete() { /* completing stuff */ }
onTaskDelete() { /* deleting stuff */ }
onTaskSchedule() { /* scheduling stuff */ }
render() {
const { tasks } = this.props;
return tasks.map(task => (
<Task
task={task}
delegate={this}
/>
));
}
}

Now that’s simple. But how would you set it up? The idea is that in your Task (child) component, you would define all the delegate methods that you want, and then your Task (child) component would call them as they happen. Now our little util makes that really easy. So we could set up the task like this:

import { setupDelegate } from 'react-delegate';
class Task extends React.Component {
constructor(props) {
super(props);
setupDelegate(this, 'onTaskComplete', 'onTaskDelete', 'onTaskSchedule');
}
render() {
const { task } = this.props;
return (
<div onClick={this.onTaskComplete}>
{task.title}
</div>
)
}
}

Now we get onTaskComplete, onTaskDelete and onTaskSchedule as functions on this and we can call them like on fx onClick events. Under the hood we then check if the delegate provided is complying with the methods onTaskComplete and so on. Luckily as you can see in the example of the list we do comply :)

What about getting data back through the delegate like a task id and so on?
Well you could of course make a middle handler like this

/* BEFORE */ #boring
import { setupDelegate } from 'react-delegate';
class Task extends React.Component {
constructor(props) {
super(props);
setupDelegate(this, 'onTaskComplete');
}
onClick(){
this.onTaskComplete(this.props.task.id);
}
render() {
const { task } = this.props;
return (
<div onClick={this.onClick}>
{task.title}
</div>
)
}
}

But we ended up doing that so many times we simplified that a lot for you as well. So the above thing we can achieve like this instead:

/* AFTER */ #awesome
import { setupDelegate } from 'react-delegate';
class Task extends React.Component {
constructor(props) {
super(props);
setupDelegate(this, 'onTaskComplete');
}
render() {
const { task } = this.props;
return (
<div onClick={this.onTaskCompleteCached(task.id)}>
{task.title}
</div>
)
}
}

Each delegate method gets a …Cached function that you can use to bind arguments to. Dope.

This little setup has been such a game-changer for us and how we use React and I hope you will try it out and let me know what you think.

You can find it and learn more about it on github: https://github.com/swipesapp/react-delegate

Let the callback hell be over, delegate!

--

--