Introducing react-lift-props
Create systems of components using deeply nested children.
The problem
There’s a cool pattern in React that people use when they want to create some components that work together to make up a larger system. I haven’t really found a name for it, but it looks something like this:
<Stepper>
<Step name="Select campaign settings">
For each ad campaign that you create, you can...
</Step>
<Step name="Create an ad group">
...
</Step>
<Step name="Create an ad">
...
</Step>
</Stepper>
It looks pretty simple on the surface, but if you think about it you can see the Stepper is doing some magic here. The Stepper is looping over it’s children, and looking at the “name” prop. It then takes that name prop and creates a header with a circle with the step number in it. It looks something like this:
const Stepper = ({ children }) => (
<div>
{children.map((child, idx) => (
<div>
<StepTitle
number={idx}
title={child.props.name}
/>
<Collapsible>
{child.props.children}
</Collapsible>
</div>
)}
</div>
);
This is pretty cool, but there’s a limitation to it. What if I wanted to wrap a Step
inside a different component so I could encapsulate the step name and/or other properties of the Step
. For example, let’s say I wanted to do this:
const SelectSettingsStep = () => {
return (
<Step name="Select campaign settings">
For each ad campaign that you create, you can...
</Step>
)
}
There’s many reasons we might want to do this:
- Code re-use: Now we can re-use the
SelectSettingsStep
in multiple differentSteppers
. - Adaptable: It’s much easier to change the
SelectSettingsStep
now without worrying about affecting the rest of the app. For example, maybe we decide theSelectSettingsStep
should actually be two steps. Now we can easily just add anotherStep
as a child ofSelectSettingsStep
. - Encapsulation: What if we wanted to slightly change the step name based on some criteria? That logic doesn’t belong in the component we used the
Stepper
in, it should go in thisSelectSettingsStep
, which we can now do. - Information hiding: This approach also let’s us hide away the information and complexity. This helps make it more adaptable, but also makes the place we use the
Stepper
easier to read and understand.
<Stepper>
<SelectSettingsStep />
<CreateAdGroupStep />
<CreateAdStep />
</Stepper>
- Testable: It’s much easier to write unit tests for the
SelectSettingsStep
since we’ve separated it out into a smaller, more manageable chunk. - Cleaner: We’ve also just generally reduced clutter and made it easier on the eye to read.
Ok, so now we’re really excited and we want to split this up. But we can’t. The Stepper isn’t written in a way that allows this. That’s because of this here:
<StepTitle
number={idx}
title={child.props.name}
/>
You can see we do child.props.name
, but there is no name on the child anymore. This really sucks. Your next thought might be to try child.props.children[0].props.name
, but that doesn’t work because there’s no children either.
<Stepper>
<SelectSettingsStep />
^^^ no name or children here
The solution
That’s where react-lift-props comes in. It’s a small library that makes use of the React context API to solve this problem. It lets you create components called Lifters
that will lift the props to the nearest component wrapped with withLiftedProps
. All we need to do is change our Step
to this:
import { createLifter } from 'react-lift-props';export default Step = createLifter({ displayName: 'Step' });
And then change our Stepper
code to use withLiftedProps
and loop over this.props.liftedProps
instead of this.props.children
like so:
const Stepper = withLiftedProps(({ liftedProps }) => (
<div>
{liftedProps.map((stepProps, idx) => (
<div>
<StepTitle
number={idx}
title={stepProps.name}
/>
<Collapsible>
{stepProps.children}
</Collapsible>
</div>
)}
</div>
));
🎉 And that’s it. Now you can create systems of components using deeply nested children. Check it out on GitHub and npm .
Notes
- Under the hood
react-lift-props
sneakily uses the React context API. It works well, but since context wasn’t designed for that it would be good to re-implement usingreact-call-return
once it’s more stable, or a different solution React might introduce. Should be able to do this in a non-breaking way. - Unfortunately, the current architecture doesn’t support React Native super well. This is a limitation that we have to deal with until React releases a different solution.