Skip to content

Instantly share code, notes, and snippets.

@aldendaniels
Last active October 6, 2018 09:50
Show Gist options
  • Save aldendaniels/5d94ecdbff89295f4cd6 to your computer and use it in GitHub Desktop.
Save aldendaniels/5d94ecdbff89295f4cd6 to your computer and use it in GitHub Desktop.
Alternative to Higher-order Components

React now supports the use of ES6 classes as an alternative to React.createClass().

React's concept of Mixins, however, doesn't have a corollary when using ES6 classes. This left the community without an established pattern for code that both handles cross-cutting concerns and requires access to Component Life Cycle Methods.

In this gist, @sebmarkbage proposed an alternative pattern to React mixins: decorate components with a wrapping "higher order" component that handles whatever lifecycle methods it needs to and then invokes the wrapped component in its render() method, passing through props.

While a viable solution, this has a few drawbacks:

  1. There's no way for the child component to override functionality defined on the higher order component.

  2. The higher order component will obscure public methods on the wrapped component. This normally isn't an issue, but sometimes components expose static methods to good purpose. For example, React Router uses this approach for its [lifecycle hooks](http://rackt.github.io/react-router/#Route Handler).

  3. This is a React-specific solution to a general (language level) problem.

To address these concerns, here's an alternative approach that I've begun adopting. It's quite generic (not specific to React or even ES6 classes) and avoids the issues above.

The solution? Simply use classical inheritence, but construct the inheritence chain dynamically to improve composability.

Here's @sebmarkbage orginal example, rewritten using this approach:

#####Higher Order Component

function enhance(ParentClass) {
  return class Enhance extends ParentClass {
    constructor() {
      super(...arguments);
      this.state = { data: null };
    }
    componentDidMount() {
      if (super.componentDidMount) {
        super.componentDidMount(...arguments);
      }
      this.setState({ data: 'Hello' });
    }
  };
}

#####Enhanced Component

class MyComponent extends mixin(enhance, React.Component) {
  render() {
    if (!this.data) {
      return <div>Waiting...</div>;
    }
    return <div>{this.data}</div>;
  }
}

Note: Methods on mixin classes should always check to see if super.methodName is defined and call it as appropriate. This way, mixins can be dynamically composed in the inheritence chain without conflicts.

#####Mixin Utility

function mixin(...args) {
  var ParentClass = args.pop();
  var classGenerators = args;
  return classGenerators.reverse().reduce((ParentClass, classGenerator) => {
    return classGenerator(ParentClass);
  }, ParentClass);
}

#####Summary I've started using this pattern to good purpose. You can chain as many mixins as you want. At any level in the chain you can override methods and use super() and super.methodName() since this uses vanilla ES6 inheritence.

Right now I don't see any major drawbacks to this pattern. This said, I've yet to use this extensively in production. Are there drawbacks I haven't thought of? If so, please advise!

@ianobermiller
Copy link

Interesting approach to mixins! I like that people are looking at the problem of bringing back the functionality of mixins in new and different ways for react+classes.

One downside of the mixin approach is that component users must remember to call super.xyz() if they are using any of the lifecycle hooks. In your example, if MyComponent needs to use componentDidMount, it will also need to call super. componentDidMount.

Additionally, the mixins won't be able to take over the render method, as is done in Radium and React-DND.

@aldendaniels
Copy link
Author

@ianobermiller - Thanks for the feedback! Responses inline.

One downside of the mixin approach is that component users must remember to call super.xyz()

Yes, this is a valid point. OTOH, having to call super.xyz() makes invocation more explicit. More importantly, this allows the wrapped component to decide when to invoke the mixin handler, something which is impossible to accomplish with higher-order components.

Additionally, the mixins won't be able to take over the render method, as is done in Radium and React-DND.

I'm not sure this is good practice - I personally prefer something more explicit. Something like this (adapted from the Radium example):

// Component
render: function() {
   return super.resolveStyles(
      <h1>Hello World</h1>;
   );
}

@ianobermiller
Copy link

That's a fair point about being explicit, and an interesting one for Radium since I recently revamped the API to remove most of the magic. In Radium's case, the resolveStyles thing, along with a couple lifestyle hooks, is the only thing it does; simply decorating or enhancing your component in the first place is explicit enough. Additionally, there are two problems with wrapping the result returned from render:

  1. You have to remember to do it
  2. If you have early returns (e.g. render a spinner if data is loading), you must wrap each return

Since the goals of the API were to be as simple, intuitive, and unobtrusive as possible, wrapping in render was a deal breaker (it was the original API actually, since it was the best you could do with a createClass mixin :).

@aldendaniels
Copy link
Author

Very good points. Here's two other ways of accomplishing this:

Method 1: Decorating mixin

function enhance(ParentClass) {
  var render = ParentClass.prototype.render;
  ParentClass.prototype.render = function() {
    return resolveStyles(render.apply(this, arguments));
  }
}

class MyComponent extends mixin(enhance, React.Component) {
  render() {
    ...
  }
}

Method 2: Standalone decorator function

function resolveStyles(target, name, descriptor) {
  return function() {
    return _resolveStyles(descriptor.value.apply(this, arguments));
  }
}

class MyComponent extends React.Component {
  @resolveStyles
  render() {
    ...
  }
}

@Faizahmad-21099
Copy link

good

@xtrasmal
Copy link

xtrasmal commented Sep 5, 2015

@aldendaniels

having to call super.xyz() makes invocation more explicit.

@ianobermiller

You have to remember to do it

Both are valid and obvious, because we are creating a decorator right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment