For my commercial projects I mainly use Vue.js. But sometimes when it’s needed I also develop web apps in React. I am no React expert at all, but I want to share my thoughts about similar patterns. Today I’ll focus on how to compensate <slot>
in React.
If you need to catch up with Vue slots first - check out my other post about it
We want to compose our component like this
const App = () => {
return(
<BaseLink to={"/home"}>
Some Text
</BaseLink>
);
};
This is pretty simple in React, because by default everything enclosed by components tag is assigned to props.children
. So in such case our <BaseLink>
component might look like this:
const BaseLink = props => {
return(
<a href={props.to} class="base-link">{props.children}</a>
);
};
We can clean up this a bit, using ES6 destructuring
const BaseLink = ({to, children}) => (
<a href={props.to} class="base-link">{props.children}</a>
);
See example below:
See the Pen Basic Vue slot made in React by Karol Świeca (@khazarr) on CodePen.
Ok now things are getting a little bit tricky. Let’s start with defining what we would like to achieve. In Vue our syntax might look like this:
<article class="message">
<div class="message-header">
<slot name="header"></slot>
</div>
<div class="message-body">
<slot name="body"></slot>
</div>
</article>
I have taken Bulma message component, and I would like to somehow pass inject any html in those two places.
Let’s start with React <Message>
component looking like this
const Message = props => {
console.log(props)
return (
<article className="message">
<div className="message-header">
// Tittle should go here
</div>
<div className="message-body">
// body should go here
</div>
</article>
);
};
And the usage in <App>
const App = () => {
return (
<main class="container">
<Message>
<div id="title">
<p>Title</p>
<div>
<button>CTA 1 </button>
<button> Close</button>
</div>
</div>
<div id="body">Body</div>
</Message>
</main>
);
};
When we console.log()
our props.children
we’ll see an array containing two React elements. We start with the most naive implementation of named slots.
const Message = props => {
return (
<article className="message">
<div className="message-header">
{props.children[0]}
</div>
<div className="message-body">
{props.children[1]}
</div>
</article>
);
};
It will work. But there are some problems with this approach:
<App>
and <Message>
componentTo fix first one we will need something simmilar to Vue’s <template>
tag. What it could be in React’s world? It’s <React.Fragment>
.
Now we will switch message component to fix all of those problems. If we have access to a children
element. We have an access to a React element. That means we can assign props
and use them to find what we need to fit in our slots!
Let’s write a helper function for that
const findNamedSlot = (name, children) =>
React.Children.toArray(children).find(
child => child.props.name === name
);
We will now fix two problems - because every React component have props, our app won’t break if we won’t find what we are looking for. Check out whole App here
See the Pen Named Vue slot made in React - take one by Karol Świeca (@khazarr) on CodePen.
But there is a problem. As stated in docs - key
is the only attribute that should be passed to React.Fragment
. Therefore it’s not an ideal solution. There must be something better right?
This is exactly what we need, to have functionality similar to Vue’s named slots. We need to refactor Message component to a class
and use static
fields for our named slots. This is better way to solve this problem.
class Message extends React.Component {
static Title = props => (
<div className="message-header">{props.children}</div>
);
static Body = props => <div className="message-body">{props.children}</div>;
render() {
return <article className="message">{this.props.children}</article>;
}
}
Then we can use it in our <App>
component.
const App = () => {
return (
<main className="container">
<Message>
<Message.Title>Title</Message.Title>
<Message.Body>Message</Message.Body>
</Message>
</main>
);
};
Thanks to this approach we can see very descriptive connection between our new “slots”. <Message.Title>
is a part of <Message>
, and we can provide some content to it. What is important to remember is fact that if we swap the order and component will behave differently. Let’s see that with codepen example:
See the Pen Named Vue slot made in React - take two by Karol Świeca (@khazarr) on CodePen.
So in fact, compound components gives us even more reusability/flexibility potential than named slots. But there is a little catch here. Let’s extend our <Message>
component that it will have some state
class Message extends React.Component {
state = {hasUserInteracted: false}
handleInteraction = () => !this.state.hasUserInteracted && this.setState({hasUserInteracted: true})
//...
}
What do you think will happen if we try to access state inside our React “named slot” ?
static Title = props => {
return (
<div className="message-header">
{this.state.hasUserInteracted ? "Thanks for reading!" : props.children}
</div>
);
};
Uncaught TypeError: Cannot read property 'hasUserInteracted' of undefined
Do you have an idea why we cannot access state in our static property? Yup, because they are static.
If you are lost, please stay with me. static
keyword comes from object oriented world. It’s a class
property, not an instance property. This means that they do not have access to this
, but you can call them without creating new instance of a class. For example you can call Object.assign()
without creating an Object instance first.
Using props
! But how to pass state
as a props? We need to help ourselves with below function:
React.cloneElement(
element,
[props],
[...children]
)
This is the way of passing additional props to an element. And since we know, our elements are children
of <Message>
we need to iterate over them. But there is a catch. We cannot simply use Array.map()
because it happens that if we have only only child React do not wrap in in array. But they provided handy method for this exact problem - React.Children.map
.
I will create helper method for that:
renderChildren() {
const {hasUserInteracted} = this.state
return React.Children.map(this.props.children, child => {
return React.cloneElement(child, {
hasUserInteracted
});
});
}
And you can see full working example below (hover over messagebox to toggle hasUserInteracted
flag)
See the Pen Named Vue slot made in React - take three by Karol Świeca (@khazarr) on CodePen.
In my next post I will focus on how to create something analogous to Vue scoped slots in React - so stay tuned ;)
props.children
are pretty similar to Vue’s default slot.this
of a component. If you’d like to access something - use React.cloneElement()