Mixins Are Dead. Long Live Composition

Utility Functions

Lifecycle Hooks and State Providers

  • 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

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;
}
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>;
}
function connectToStores(Component, stores, getStateFromStores) {
const StoreConnection = React.createClass({
getInitialState() {
return getStateFromStores(this.props);
},
componentDidMount() {
stores.forEach(store =>
store.addChangeListener(this.handleStoresChanged)
);
},
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;
};
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)
});

What’s Next

Other Approaches

--

--

--

Working on @reactjs. Co-author of Redux and Create React App. Building tools for humans.

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Dan Abramov

Dan Abramov

Working on @reactjs. Co-author of Redux and Create React App. Building tools for humans.

More from Medium

HOW TO BUY $ELITE TOKEN

The Juno network’s governance proposal that could change everything

Sweetgum Labs Partners With Solid World

Dust Ventures and CRODO — a mutually beneficial partnership?