Introducing: Composition Provider

Posted on January 06, 2019  -  👩🏿‍💻 6 min read

Before we can dive deep into a pattern and what it means, we’ll need to first understand what it is we’re trying to solve. The main inconvenience is what’s called “prop drilling”.

Prop drilling ⛏

If familiar with the term “prop drilling” you can go on ahead and jump to the next section. If not, here’s a crash course - let’s say you have these components (for simplicity there are no props):

// CardHeader.js
<Header>
  <Title />
  <Menu />
</Header>

// Card.js
<Card>
  <CardHeader />
  <Content />
</Card>

// CardList.js
<div>
  {cards.map(card => (
    <Card {...card} key={card.id} />
  ))}
</div>

Let’s focus on the Menu component, the one in CardHeader. Usually, in this kind of case you’d want to have the same actions for all cards so you’d just render it like this:

// Inside CardHeader.js
<Menu actions={[{ text: 'Rename' }, { text: 'Move' }]}>

How do you handle the action itself? You have to pass onActionClick as well, right? But, you don’t know how to handle it in CardHeader or Card even. You probably know how to handle it in CardList where you have access to all the cards, to check for naming conflicts for example.

At this point what you do is send the onActionClick handler down the component tree all the way from CardList to Menu. This is what’s known as “prop drilling”.

What’s the problem with prop drilling? 🚫⛏

This is a simple case, what happens when you have multiple props you want to “drill” to a certain component? What happens when there are multiple components that you want to “drill” multiple props to? That’s when you see props like menuProps that represent a high-level prop for all things related to the Menu.

What if somewhere along the component tree forgets to pass down these props? What if rendering component and a top level one both need to pass props to the inner component? That’s when you’ll start seeing compositioning logic in each parent component to ensure that all parts of the logic are being composed correctly.

So you see, that’s a lot of “What if”s and “What happens when”s, which unfortunately, aren’t that rare.

Dreaming of different approach 💭

Wouldn’t it be great if you could “magically” supply props to a component deep down in the tree? Being able to “render” something and whatever props you sent it would “propagate” to the actual component they belong to?

This is where the pattern of Composition Provider comes in.

Let’s start by thinking about the API.

// CardList.js
<ComponentRelatingToMenu onActionClick={action => console.log(action)}>
  <div>
    {cards.map(card => (
      <Card {...card} key={card.id} />
    ))}
  </div>
</ComponentRelatingToMenu>

It would pass along the onActionClick to all the Menu components that are somewhere in that tree. This gives us the benefit of decoupling the Menu API from all the parent components (no need to do “prop drilling”).

When dreams come true ✨

How would we achieve that? how do we “skip” components and pass something to nested components. The answer is Context!

Context allows us to create a Provider and a Consumer. The Provider supplies some value that the corresponding Consumer can then use.

Given that, we can turn ComponentRelatingToMenu into a Provider and Menu into a Consumer. Let’s also rename it now, instead of ComponentRelatingToMenu how about Menu.Provider?
And now, we’ll have a “normal” Menu component and a Menu.Provider that can optionally supply props to a Menu component down the tree.

A super super simple implementation of this pattern would be something like this:

// Menu.js
const { Consumer, Provider } = React.createContext();

const InnerMenu = props => {
  // This is where the actual menu code would be.
};

const Menu = props => {
  <Consumer>
    {contextProps => {
      <InnerMenu {...props} {...contextProps} />;
    }}
  </Consumer>;
};

Menu.Provider = ({ children, ...props }) => {
  return <Provider value={props}>{children}</Provider>;
};

export default Menu;

This uses the current Context API, we can achieve the same thing with the new useContext hook instead of Context.Consumer to simplify this even further.

Also, it’s just meant to give you an idea of how to implement this kind of pattern. It doesn’t contain everything! For instance, props and contextProps aren’t composed at all - what if you get className from both of them? In this case you’ll need to concat both strings to apply both of them. But strings are relatively a simple case, what happens when you’ll try to handle onClick for example? you’d need to make sure both props.onClick and contextProps.onClick are called, you can imagine this would be hard to maintain by doing things specifically each time for each prop.

Make sure you compose all the props together correctly, otherwise you’re gonna have a bad time.

Back to reality 📦

Because we live in a world where we want to do as little work as possible - I created the package react-composition-provider. Instead of writing all of this boilerplate code yourself everytime you want to implement this pattern you can just use this package instead.

The same Menu with react-composition-provider would look like this

import { withCompositionProvider } from 'react-composition-provider';

const InnerMenu = props => {
  // This is where the actual menu code would be.
};

export default withCompositionProvider(InnerMenu);

Other than saving you all the boilerplate code, it also gives you a lot more features than the basic pattern.

For more information about this package you can view the Github repo here

Some ground rules 📚

Although this pattern can be very very helpful and you’re maybe thinking to yourself “I’m going to define all of my components like this now!“.

JUST DON’T

General rules of thumb (there are probably more since this is new) of dos and don’ts with this pattern:

  1. you’re in control of both ends, the component itself and the provider(s).

    1. you have to pass several props down the component tree.
    2. you have a very deep component tree and don’t want to cause re-rendering for all the components in the middle.
  2. as a library author, exposing some internal component API - this may make the library API less predictable.

    1. when you define props like xxxProps - you already want to expose the props for xxx as part of your API.
    2. it’s important to also define a scope for your components so things don’t leak outside your API boundary.

So, as with all patterns out there - make sure you understand what you’re doing and don’t abuse it, but most importantly -

Have Fun :)