Mixins Are Dead. Long Live Composition
When React 0.13 came out, everybody freaked out.
The introductory post made it clear that mixins are on their way out:
Unfortunately, we will not launch any mixin support for ES6 classes in React. That would defeat the purpose of only using idiomatic JavaScript concepts.
There is no standard and universal way to define mixins in JavaScript. In fact, several features to support mixins were dropped from ES6 today. There are a lot of libraries with different semantics. We think that there should be one way of defining mixins that you can use for any JavaScript class. React just making another doesn’t help that effort.
One can read this as “mixins will come later” but the truth is that Sebastian Markbåge, the great API terminator, does not favor them:
To be clear, mixins is an escape hatch to work around reusability limitations in the system. It’s not idiomatic React. Making composition easier is a higher priority than making arbitrary mixins work. I’ll focus on making composition easier so we can get rid of mixins.
Why use mixins anyway? What problems do they solve? Can we solve these problems differently, without inheritance, and super woes?
Utility Functions
This case is a no-brainer. If you use mixins to share utility functions, extract them to modules and import and use them directly.
Lifecycle Hooks and State Providers
This is the main use case for mixins. If you’re not very familiar with React’s mixin system, it tries to be smart and “merges” lifecycle hooks. If both the component and the several mixins it uses define the componentDidMount lifecycle hook, React will intelligently merge them so that each method will be called. Similarly, several mixins can contribute to the getInitialState result.
In practice, this behaviour is the single thing that makes mixins useful. They can subscribe the component’s state to a Flux Store or they can work with its DOM node after it is updated. It’s absolutely necessary that any component extension mechanism has the access to the component’s lifecycle.
However mixins are fragile for a number of reasons:
- The contract between a component and its mixins is implicit. The mixins often rely on certain methods being defined on the component, but there is no way to see that from the component’s definition.
- As you use more mixins in a single component, they begin to clash. For example, if you use something like StoreMixin(SomeStore) and you add another StoreMixin(OtherStore), React will throw an exception because your component now has two versions of methods with the same names. Different mixins will also clash if they define the same state fields.
- Mixins tend to add more state to your component whereas you should strive for less. You should read the excellent Why Flux Component is better than Flux Mixin essay by Andrew Clark on this topic.
- Mixins complicate performance optimizations. If you define the shouldComponentUpdate method in your components (manually or via PureRenderMixin), you might have issues if some of the mixins need their own shouldComponentUpdate implementations to be taken into account. This can be solved by adding even more “merging” magic, but is it really the way forward?
Enter Higher-Order Components
I first learned about this pattern from a gist by Sebastian Markbåge. The gist is a little bit cryptic if you’re not yet fully comfortable with ES6 syntax, so I’m going to use the “Flux Store mixin” example to explain it.
Note that this is just one possible way of replacing mixins with composition. See the notes at the end of the article for other approaches.
Suppose that you have a mixin that subscribes to the specified Flux Stores and triggers changes in component’s state. It might look like this:
function StoreMixin(...stores) {
var Mixin = {
getInitialState() {
return this.getStateFromStores(this.props);
},
componentDidMount() {
stores.forEach(store =>
store.addChangeListener(this.handleStoresChanged)
);
this.setState(this.getStateFromStores(this.props));
},
componentWillUnmount() {
stores.forEach(store =>
store.removeChangeListener(this.handleStoresChanged)
);
},
handleStoresChanged() {
if (this.isMounted()) {
this.setState(this.getStateFromStores(this.props));
}
}
};
return Mixin;
}
To use it, the component adds StoreMixin to the mixins list and defines the getStateFromStores(props) function:
var UserProfilePage = React.createClass({
mixins: [StoreMixin(UserStore)],
propTypes: {
userId: PropTypes.number.isRequired
},
getStateFromStores(props) {
return {
user: UserStore.get(props.userId);
}
}
render() {
var { user } = this.state;
return <div>{user ? user.name : 'Loading'}</div>;
}
How do we solve this without a mixin?
A higher-order component is just a function that takes an existing component and returns another component that wraps it.
Consider this implementation of connectToStores:
function connectToStores(Component, stores, getStateFromStores) {
const StoreConnection = React.createClass({
getInitialState() {
return getStateFromStores(this.props);
},
componentDidMount() {
stores.forEach(store =>
store.addChangeListener(this.handleStoresChanged)
);
this.setState(getStateFromStores(this.props));
},
componentWillUnmount() {
stores.forEach(store =>
store.removeChangeListener(this.handleStoresChanged)
);
},
handleStoresChanged() {
if (this.isMounted()) {
this.setState(getStateFromStores(this.props));
}
},
render() {
return <Component {...this.props} {...this.state} />;
}
});
return StoreConnection;
};
It looks a lot like the mixin, but instead of managing the component’s internal state, it wraps the component and passes some additional props to it. This way wrapper’s lifecycle hooks work without any special merging behavior, by the virtue of simple component nesting!
It is then used like this:
var ProfilePage = React.createClass({
propTypes: {
userId: PropTypes.number.isRequired,
user: PropTypes.object // note that user is now a prop
},
render() {
var { user } = this.props; // get user from props
return <div>{user ? user.name : 'Loading'}</div>;
}
});
// Now wrap ProfilePage using a higher-order component:
ProfilePage = connectToStores(ProfilePage, [UserStore], props => ({
user: UserStore.get(props.userId)
});
That’s it!
The last missing piece is the handling of componentWillReceiveProps. You can find it in the connectToStores source code in the updated Flux React Router Example.
What’s Next
I plan to use higher-order components in the next version of React DnD.
They don’t solve all the use cases for mixins, but come close. Don’t forget that the wrapper can pass arbitrary props to the wrapped component, even the callbacks. It’s possible that the higher-order components can be abused too, but unlike mixins they only rely on simple component composition instead of a bag of tricks and special cases.
There are things you can’t implement with higher-order components. For example, PureRenderMixin would be impossible to implement because the wrapper has no way to look into the wrapper component’s state and define its shouldComponentUpdate. However this is precisely the case where, in React 0.13, you might want to use a different base class, for example PureComponent that descends from Component and implements shouldComponentUpdate. Now that’s a valid use case for inheritance!
Operating on the DOM nodes may also be tricky because the wrapper component has no way to know when the child’s state updates. I hope that this will be addressed in the future versions of React.
Other Approaches
There are other perfectly valid patterns for composition, such as composition right inside render() as used in Flummox. It is also based on nesting, but is less verbose than the higher-order components.
You can always make your own mixin system, if you prefer to. By all means, you’re not limited to the higher order components! I wrote this article to shed more light on this approach. We’ll see what works best over the next months. I’m sure that the winning approaches will rely on composition instead of multiple inheritance (which is what mixins really are).
React is also tackling the problem of sideways data loading through a new API based on Observables. We’ll see what 0.14 brings!
Follow Dan Abramov on Twitter
⚛