Implicitly passing props with React cloneElement
- Published on
- Related tags
Leverage the React.cloneElement API to pass certain props implicitly.
This article was originally posted on Medium back in 2019, and I've ported it here for historical purposes. Read on medium.com.
While working on a fun side project recently, I ran into an interesting situation where I needed to pass a prop down to multiple children that would help determine the styling of those children. There was a slight caveat — I did not want to do this explicitly, but rather implicitly in a specific context.
Let's explore this!
Note: there are a couple of references to styled-components throughout this article, and it's assumed you have a basic understanding of the library.
The component that we're going to dissect is FloatingLabel, which is essentially a stylistic wrapper that renders an input and a label in a “floating” style.
If you're not familiar with what this is — the input and label are styled in such a way that if the input is focused, the label shrinks into the top left corner to make room for any text (such as placeholder text or text typed by the user). The effect is pretty nice — you can have labels (something you should be using anyway for accessibility) but they don't have to clutter the UI, and can sit inside their respective inputs.
I went back and forth on structuring the FloatingLabel
component, and initially was leaning towards a more traditional API with standard passing of props. The component was more generalized and was simply called Input, which rendered what it needed (an input and a label, wrapped in a div).
_10<Input_10 name="blah"_10 id="blah"_10 labelName="Cool Label"_10 value={blah}_10 onChange={this.handleChange}_10 placeholder="Lol"_10/>
However, after reading this awesome post that advocates for using children as props, I decided to restructure my API to be more explicit.
In the above code we are passing some props to Input and it's unclear what's going on. For example, what does Input even render? At first glance you might think it only renders an HTML input, however it's also rendering a label using the labelName
prop. And when looking at props, questions arise such as... is the id
prop for the input or the label? Is the placeholder
prop for the label text, or the input placeholder?
So after playing around with multiple configurations, I ended up refactoring my component API to resemble this:
_10<FloatingLabel>_10 <Input_10 id="blah"_10 name="blah"_10 value={firstInput}_10 onChange={this.handleChange}_10 placeholder="Hi"_10 />_10 <Label htmlFor="blah">Float On</Label>_10</FloatingLabel>
FloatingLabel
is a stylistic wrapper of sorts (rendering a div that is positioned relative, something that is necessary for the effect) which wraps an input and a label using this.props.children. Notice how everything is now very explicit - you know exactly which HTML elements are used and which props are for which components.
The interesting problem I ran into next was how to style Input and Label to “float” in this particular context only, since we want to be able to reuse them without having to always float.
Technically, we could pass a prop signaling for it to float, something like:
_13<FloatingLabel>_13 <Input_13 id="blah"_13 name="blah"_13 value={firstInput}_13 onChange={this.handleChange}_13 placeholder="Hi"_13 float="true"_13 />_13 <Label htmlFor="blah" float="true">_13 Float On_13 </Label>_13</FloatingLabel>
However, declaring it twice felt really cumbersome to me, and it seemed a little unnecessary because we know that if we're looking to render a FloatingLabel
then we definitely want the Input and Label to “float”... we shouldn't have to explicitly define this, it should just happen.
Luckily, React provides some handy utilities to deal with situations like this, where the children props data structure is unknown and we need to dynamically pass props around.
Using the React.Children.map utility, we can map over each child and invoke a function on each. We can then use React.cloneElement to assign the float prop to each child, which we can then leverage for styling. The API for FloatingLabel
ultimately ended up looking like this, where children is getting wrapped with the new props:
_15const FloatingLabel = props => {_15 const { children } = props;_15 const childrenWithProps = React.Children.map(children, child =>_15 // this is the magic!_15 React.cloneElement(child, { float: true })_15 );_15 return <StyledFloatingLabel>{childrenWithProps}</StyledFloatingLabel>;_15};_15export default FloatingLabel;_15_15// styled-components wrapper to make sure it's positioned relative_15const StyledFloatingLabel = styled.div`_15 position: relative;_15 ...rest of the styles_15`;
So basically, take each child (in our case, Input and Label) and clone them, then add on the float
prop (an optional prop) and set it to true, which only gets added if Input and Label are rendered within the context of FloatingLabel
.
Inside Input and Label, we can access the float prop and if it's true, set the float styles (aka shrink the label to top left on input focus, show the placeholder on input focus, etc).
To give you an idea of what that looks like, let's look at the Label component and how we could style that:
_22const Label = props => (_22 // Access the props with spread attributes_22 // StyledLabel is a styled-components wrapper which then has_22 // access to the props (in particular, float)_22 <StyledLabel {...props}>{props.children}</StyledLabel>_22);_22export default Label;_22_22const StyledLabel = styled.label`_22 font-size: 1.2rem;_22 padding: 0 1rem;_22 transition: all 0.2s;_22 color: #8e2de2;_22 /* Here we can access the props, if float: true, then add some float-specific styles */_22 ${props =>_22 props.float &&_22 `_22 position: absolute;_22 top: 0;_22 left: 0;_22 `}_22`;
This saves us from having to pass the float prop explicitly to each child when called within FloatingLabel
, and promotes reusability outside of that context, in case we need to render a label or input without floating, as you can see here:
Conclusion
I think using React.cloneElement
for context-dependent props is a pretty neat way to cut down on component clutter. Passing those implicitly is one less thing a user has to reason about when navigating your API.
I'm interested in hearing your thoughts about this method! Do you prefer to pass all props explicitly?