Introducing: Composition Provider
Posted on January 06, 2019 -  đ©đżâđ»Â 6 min readBefore 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:
-
youâre in control of both ends, the component itself and the provider(s).
- you have to pass several props down the component tree.
- you have a very deep component tree and donât want to cause re-rendering for all the components in the middle.
-
as a library author, exposing some internal component API - this may make the library API less predictable.
- when you define props like
xxxProps
- you already want to expose the props forxxx
as part of your API. - itâs important to also define a scope for your components so things donât leak outside your API boundary.
- when you define props like
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 :)