The many ways to do component composition in React
Bài đăng này đã không được cập nhật trong 7 năm
These are some techniques you can use to compose your component with React.
Mixins
This is the original and legacy way of doing component composition since the very first days of React. It is not recommended anymore and also is not possible with ES6 class syntax. Here's how you would use it, back in the React.createClass
days:
import React from 'react'
const ClockMixin = {
getInitialState() {
return {
time: Date.parse(new Date),
};
}
time() {
return (new Date(this.state.time)).toLocaleTimeString();
}
componentDidMount() {
this.setState({
time: this.state.time + 1000
});
}
}
const App = React.createClass({
mixins: [ClockMixin],
render() {
return <div>{this.time()}</div>
}
})
The reason that mixin faded out as the solution of choice for component composition is that the React team decided to go for ES6 instead of having to maintain their own class model. But the fact is mixin itself does have its own problems. To summarize they are:
- Components are tightly coupled. Changing a mixin affects multiple components.
- Implicit dependencies. They make it harder to tell where a particular state/method come from, especially when there are multiple mixins applied.
- Mixins can cause name clashes. Components cannot use mixins that work with same state or method. Modifying names of a state/method is not an easy option because of components being tightly coupled.
Anyway, let's move on to the new way to do component composition after mixins have faded away due to technical reason.
Higher-Order Component
When mixins is no longer an available option, higher-order component arises as the new standard way to do component composition in React.
Similar to how a higher-order function
- Take function(s) as its argument(s)
- Return a function as a result
A higher-order component is also a function that
- Take component(s) as its argument(s)
- Return a component as a result
So the previous component would be written as follow
import React from 'react'
import ReactDOM from 'react-dom'
class withClock = (Component) => {
return class extends React.Component {
state = {
time: Date.parse(new Date),
}
time() {
return (new Date(this.state.time)).toLocaleTimeString();
}
render() {
return <Component {...this.props} time={this.time()}/>;
}
}
}
class App extends React.Component {
render() {
return <div>{this.time}</div>;
}
}
ReactDOM.render(withClock(<App/>), document.getElementById('app'));
Here the time
prop is injected in the the App
component. The App
component won't get bloated with all the states and methods of withClock
like the one composed with mixin. It only receives an extra prop. And it's a little bit easier to acknowledge this extra prop than all the states and methods that got implicitly injected into the component.
That's a good substitution for mixin as the method to reuse code with ES6 class. However, the React team does have a point when they called higher order components "The new mixins". All the problems that come with old mixins are pretty much still there. Since props are injected into the rendered component, components are still tightly coupled and name collisions still happen. The component also doesn't get any clearer than when it is composed with multiple mixins. It even gets more ugly when you want to combine multiple higher-order components.
Component Injection
This one is a little bit different from the previous two since instead of enhancing a component by giving it extra state/prop/method, it tries to enhance the render function of that component. This has an advantages over the other twos on that it doesn't need to actually touch any of the inner working of a component, thus effectively negates all the problems with injected states/props stated above. In more fancy words, it is a dynamic composition rather than static composition.
In the previous two techniques, you inject state/props into another component so it can use them to render its content. What is rendered is of no concern to the mixin or higher order component and they have no effect on what is rendered, only on what data is used. Hence the name static composition. With this technique, the component itself is injected and we can decide what is rendered in the render time, hence the name dynamic composition.
There are usually two types of component injection. Inject as children and inject as props.
One common use case of component injection as children is the Tabs
component. Here is a simplified example.
class Panel extends React.Component {
render() {
return (
<Tabs>
<Tab label="Tab 1">
{...tabContents1}
</Tab>
<Tab label="Tab 2">
{...tabContents2}
</Tab>
</Tabs>
);
}
}
class Tab extends React.Component {
render() {
return (
<div className='tab-contents'>
{this.props.children}
</div>
);
}
}
Component injection as props is particularly useful to make component with multiple slots, like in Angular or Vue.
Content distribution with slots
If you are familiar with component slot in Vue, you may realize that it is an equivalent of the default slot. All children are rendered as one fragment of the wrapper component. But what if we need to render it as multiple fragments scattered in many places in the wrapper component. This is where component injection as props become helpful. It's essentially the same concept with the previous technique, except we pass the component as component props. These two techniques can also be used together.
class PageHeader extends React.Component {
render() {
return <header>This is header</header>
}
}
class PageFooter extends React.Component {
render() {
return <footer>This is footer</footer>
}
}
class App extends React.Component {
render() {
return (
<div className='app'>
{this.props.header}
<div className='contents'>
{this.props.children}
</div>
{this.props.footer}
</div>
);
}
}
ReactDOM.render(
<App header={<Header/>} footer={<Footer/>}>
<p>This is app contents</p>
</App>,
document.getElementById('app')
);
Now, what if we want to use the wrapper component's context (state or props) in the injected component so we can make them reusable templates. This is also known as Scoped slots in Vue.
Render Callback
Render callback arised recently as an alternative to higher-order component. It quickly becomes the preferred choice of many among the React community. This approach is very similar to component injection. It is essentially the same concept. The only different is instead of injecting a component, we inject a function that will return a component. This function can receive data from the wrapper component's context and pass it as props for the injected component. So I wonder should we call this Render Function Injection. Anyway, the reason it became so popular is because it is supposed to solve all the problems accompanied with mixins and higher-order component, which we can see explanation above.
Similar to component injection, it can be done in two ways, inject as children (a.k.a Function as Child Component) and inject as props (a.k.a Function as Prop Component or Render Props)
Function as Child Component
So this technique is possible because React allows us to use the children
as a function. If you have read through the last part, you may have already knew what we are gonna do here. Here's your example.
class App extends React.Component {
render() {
return (
<div className='app'>
{this.props.children(this.props.userName)}
</div>
)
}
}
ReactDOM.render(
<App userName='Eirika'>
{(name) => <div>Hello {name}</div>}
</App>,
document.getElementById('app')
)
There you can see how it makes the wrapper component props available to the injected component via a function parameter.
There's only one problem here that some might find annoyed. Using children
to pass the render callback might be confusing and not very self-documenting.
You would never name your add function above badger (although that would be cool) or tapioca or even children, would you? Of course you wouldn’t. That would be silly, confusing, and not very self-documenting. Then why in the world would you ever use a prop named children to pass a render callback function?
But some might find cases where it is actually an okay solution.
Function as Prop Component
There's not many things to say here as we may all know what it should look like now.
class App extends React.Component {
render() {
return (
<div className='app'>
{this.props.header(this.props.appName)}
<div className='contents'>
{this.props.children}
</div>
{this.props.footer(this.props.copyright)}
</div>
);
}
}
const header = (appName) => (<header>Welcome to {appName}</header>)
const footer = (copyright) => (<footer>©2017 {copyright}</footer>)
ReactDOM.render(
<App header={header('My App')} footer={footer('Me')}>
<p>This is app contents</p>
</App>,
document.getElementById('app')
);
The rendered HTML would be
<div className='app'>
<header>Welcome to My App</header>
<div className='contents'>
<p>This is app contents</p>
</div>
<footer>©2017 Me</footer>
</div>
Although the injection techniques has obvious advantages over higher-order components, they do have drawbacks. Component injection doesn't have access to wrapper component's context. Render callback has access to wrapper component's context but components are created at render time and cannot be optimized with shouldComponentUpdate
. However, it might not be a real problem in many cases though. One example that you might already be familiar with is Route
component from react-router. It is actually implemented with all of the injection techniques above.
All rights reserved