facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
229.15k stars 46.89k forks source link

Proposal for porting React's Mixin APIs to a generic primitive #1380

Closed sebmarkbage closed 7 years ago

sebmarkbage commented 10 years ago

Currently React's mixins allow multiple mixins to implement the same method in multiple mixin, if it's a whitelist. We would like to decouple the mixin system from React and therefore we need a way to solve it without a whitelist.

The idea is to have every mixin call super() to allow predictable chaining.

The mixin(...mixins) function would essentially create a new prototype chain with each mixin's set of functions stacked on top of each other in order. An object is treated as a prototype. A function is treated as a class/constructor and gets all their static methods + their prototype merged.

var A = {

  componentDidMount() {
    super(); // This will end up calling an empty function, placed by mixin()
    console.log('A');
  }

};

class B {

  static getQueries() {
    super(); // This will end up calling an empty function, placed by mixin()
    console.log('B')
  }

  componentDidMount() {
    console.log('B');
    super();
  }

}

class C extends mixin(A, B) {

  static getQueries() {
    super();
    console.log('C');
  }

  componentDidMount() {
    super();
    console.log('C');
  }

}

C.getQueries(); // B, C
new C().componentDidMount(); // B, A, C

We can issue warnings when the mixin function is called and some of the overlapping methods are missing super calls.

Solvable but confusing/complex issues:

class C extends mixin(A, B) {

  // This state intializer overrides the state initializer in the base class.
  // The current React class system merges the two. This is also not valid ES6
  // since we don't have property initializers yet. This is based on the
  // TypeScript syntax.
  state = {
    b: true
  }

  componentDidMount() {
    // You forgot to put a super call here but there's no warning since
    // the mixin logic happens before this class is created.
  }

}

To be clear, mixins is an escape hatch to work around reusability limitations in the system. It's not idiomatic React.

Idiomatic React reusable code should primarily be implemented in terms of composition and not inheritance.

basarat commented 10 years ago

Just pointing out that mixin(A, B) will not compile in TypeScript as it is.

class C extends mixin(A, B) { // TS: Error

This will be useful for ES6 classes. But will not make it easy for people using TypeScript.

sebmarkbage commented 10 years ago

That's a good point. I don't think there is a way to create composable interfaces in this way in TypeScript. Do you see any alternatives?

fdecampredon commented 10 years ago

In typescript the only way I see to implements something similar to what you propose would be something like that :

declare function applyMixins(target: any, mixins: any []) : void;
declare function superMixin<T>(t:T): T;

class A {
   componentDidMount() {  }
   myMethodA() { return 'A' }
}

class B {
   componentDidMount() {  }
   myMethodB() { return 'B' }
}

class C implements A,B {
    componentDidMount(): void {
       superMixin(this).componentDidMount()
       console.log('C');
    }
    myMethodA: () => string;
    myMethodB: () => string;
}

applyMixins(C, [A, B]);

I already tried to wrap react logic with es6 class for typescript usage and obtained the following result : https://github.com/fdecampredon/react-typescript with similar way of doing that.

By the way mixin are in the roadmap of typescript for 1.x, but I have no clue when they plan to implement that. (I guess not soon)

basarat commented 10 years ago

I don't see a better way than what @fdecampredon is recommending. Notice his superMixin because super is a contextual keyword and would have given a TS error as well if used in a class.

sebmarkbage commented 10 years ago

Luckily mixins are completely optional and you can use whatever system you want. Presumably TypeScript would have to build in similar features and you can just use that.

They're even frowned upon when they're overused so they're completely optional.

handwave, punt

pspeter3 commented 10 years ago

So is this on the road map for React soon then?

ctaggart commented 10 years ago

@pspeter3, my understanding is that it is targeted for version 0.12 as part of Components as ES6 Classes. To guestimate the timeframe, take a look at the tags. Really looking forward to this! We want to create React components in code using TypeScript.

pspeter3 commented 10 years ago

@ctaggart I'm in the same place. Is there anyway to help get this out faster? (Eg, which issues would need to be resolved so I can create Pull Requests for them)

jquense commented 10 years ago

Is there a possibility for something less inheritence based in this? Seems like doing something a la Traits would be a more composition-ally focused approach.

My worry is that this makes for a Ruby-esque problem of clever module ordering in order to get the right combination of super calls. It also means that you need to know that you are overriding a method in your mixin which makes mixins less reusable. An alternative would be to just require the class definer to resolve conflicts for non lifestyle methods/props and provide some utility functions for making that really easy? Admittedly I don't know the limitations of the es6 class syntax but in the current one

var Comp = React.createClass({

    mixins: [ A, B ],

    statics: {
       getQueries: util.chain(function(){ //or merge or after, before, around, etc 
           console.log('C')
       })
    },
    componentDidMount: function() {
        console.log('C');
    }
})
syranide commented 10 years ago

@theporchrat The code in OP is based on ECMAScript proposals (although changing from day to day it seems).

jquense commented 10 years ago

@syranide the class syntax is from es6 proposals, but a mixin() method is all React. Until the language has some solid approach to mixins/modules/traits/whatever React will be providing some mechanism for flattening a bunch of object literals/prototypes and adding them to the a derived component prototype, no?

Sure one can mixin however they want, since the component is just a constructor and prototype, which is great, except that it destroys the interoperability of mixins.

It is definitely possible to have a mixin system that is not es6, but works with es6 (which is what I took goal here to be). All that is needed for the class syntax is a mixin function that returns a func or object

It just strikes me that the above mixin method doesn't seem any more in keeping with the language as it is progressing then the current methods, while also being very un-reactlike. Mixins aren't an inheritence based concept, it seems odd to try and push them that way.

syranide commented 10 years ago

@theporchrat The mixin() showed by sebmarkbage would probably not be exposed by React and if it was, it would be very simple and not in any way tied to React core. But it's likely that React won't go the way of classes, https://github.com/reactjs/react-future/blob/master/06%20-%20Returning%20State/02%20-%20Module%20Pattern.js

jquense commented 10 years ago

ooo that is interesting thanks for the link

sebmarkbage commented 10 years ago

React Core itself is not opinionated about how you build your classes. Generally we prefer composition. This is already possible using components.

This proposal is about providing feature parity with current React classes. It's not an encouraged pattern.

Native trait features in the ECMAScript language will likely have similar feature sets.

On Jul 20, 2014, at 2:11 PM, theporchrat notifications@github.com wrote:

ooo that is interesting thanks for the link

— Reply to this email directly or view it on GitHub.

pspeter3 commented 10 years ago

So for third party languages, http://facebook.github.io/react/blog/2014/10/14/introducing-react-elements.html#third-party-languages, will the class in type be the ES6 class or the result of React.createClass(EsClassHere)

sophiebits commented 10 years ago

@pspeter3 0.12 doesn't support classes in this way yet, but once we support ES6 classes then type will be the class, not the factory.

pspeter3 commented 10 years ago

@spicyj I understand that, I meant for 0.13. Will I need to pass the ES6 class into React.createClass?

sebmarkbage commented 10 years ago

@pspeter3, no you will not need to do that. type will be the ES6 class.

pspeter3 commented 10 years ago

@sebmarkbage Oh that's awesome!

jbrantly commented 9 years ago

It seems that ES6 classes are being actively worked on and I presume targeted for v0.13 (can anyone confirm that?). If so, what is the current plan for ES6 classes and mixins in that release? I'm assuming React Core will not ship with any magic to somehow handle mixins with createElement like it does today with createClass, so any mixin functionality will need to be handled outside of React. Will something along the lines of the OP be shipped?

sebmarkbage commented 9 years ago

Yes, they are scheduled for 0.13. In the 0.13 release, we will not ship this mixin helper. It requires toMethod() support which is unusual in current ES6 transpilers. You will need to roll your own or use createClass.

rtorr commented 9 years ago

This proposal is about providing feature parity with current React classes. It's not an encouraged pattern.

@sebmarkbage is this still the case? React.createClass is still the preferred way of doing things in 0.13? I don't think http://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html really illustrates that opinion, and maybe that is why there is a lot of confusion.

jimfb commented 9 years ago

@rtorr We are moving towards native ES6 classes as the "modern" way of doing things. That said, many people (including a large portion of Facebook) will continue to use React.createClass for some time, so both options will be supported in the near term.

alexeygolev commented 9 years ago

Was wondering if subclassing can solve it for now. A 'mixin' can extend React.Component and all the components we need to use this 'mixin' could just extend it. Seems to be working. Am I missing something? (besides the fact that the mixins already created are not compatible with this pattern)

brigand commented 9 years ago

toMethod is now officially not in ES6 (planned for ES7).

sebmarkbage commented 9 years ago

Even without toMethod it could be built like this: https://gist.github.com/sebmarkbage/fac0830dbb13ccbff596

However, I think that making composition easier is a higher priority than making arbitrary mixins work.

gsklee commented 9 years ago

I've played with @sebmarkbage's example above a bit but don't really like the fact that super calls become mandatory in each augmented method. Also played around with @brigand's react-mixin which looks to be one of the more reasonable solutions for now, but I don't like the syntax.

What's the status quo of mixins/traits in ES7? Would it be better to fill the syntax using something like sweet.js? For example Scala has something like this: class A extends B with X with Y with Z.

donaldpipowitch commented 9 years ago

Scala syntax looks good o_O

gsklee commented 9 years ago

@sebmarkbage I think I've come up with a feasible solution:

traits.js

import traits from 'es6-traits';
import React from 'react/addons';

export const {on, using} = traits();

export const autobind = {
  [Symbol.toStringTag]: 'autobind',

  constructor() {
    Object.getOwnPropertyNames(this.constructor.prototype)
          .filter(x => x.startsWith('on'))
          .map(x => this[x] = this[x].bind(this));
  }
};

export const purerender = Object.assign(React.addons.PureRenderMixin, {
  [Symbol.toStringTag]: 'purerender'
});

some-component.js

import React from 'react';
import {on, using, autobind, purerender} from './traits';

export default class SomeComponent extends (on (React.Component), using (autobind, purerender)) {
  ...
}

The syntax is a bit longer than the proposed mixins(React.Component, X, Y, Z) but is more concise IMHO. I actually went through numerous API ideas before finally settling on this one.

The package is available at es6-traits and is based on @brigand's excellent smart-mixin. Since smart-mixin is offering a composition conflict resolution mechanism I'm calling this traits instead of mixins.

brigand commented 9 years ago

Those syntax hacks are impressive :-)

I like it, but looking over the source I think the second example needs to be this because traits() can't be shared by multiple classes.

import React from 'react';
import traits from 'es6-traits';
import {autobind, purerender} from './traits';
const {on, using} = traits();

export default class SomeComponent extends (on (React.Component), using (autobind, purerender)) {
  ...
}

I also like the use of constructor over componentWillMount in the traits. It feels much more pure.

gsklee commented 9 years ago

@brigand It can be shared, you can also specify different resolution rulesets for different classes like this:

export const {on, using} = traits({
  ruleset: {
    Cat: {
      sleep: 'ONCE',
      play: 'MANY'
    },
    Dog: {
      sleep: 'ONCE',
      play: 'ONCE'
    }
  }
});

I need to find some time to come up with a document and add browser support so it can be played with inside JSBin etc. but while we are at it, any suggestion to the API and how the lib should work are welcome.

brigand commented 9 years ago

I followed up in https://github.com/gsklee/es6-traits/pull/2.

mlrawlings commented 9 years ago

It would seem to me that unless React is going to move to an event or hook based system for the lifecycle methods, those methods need to be handled specifically. You can't just mix them into the prototype, which means whatever the solution it's going to be React specific. I like something like the following where it is clear that I am extending a React.Component with PureRenderMixin mixed in.

// user's code
class MyComponent extends React.Component(PureRenderMixin) {
   //...
}

// ReactComponent.js
function ReactComponent(props, context) {
  if(!(this instanceof ReactComponent)) {
    if(arguments.length) return createMixedComponent(arguments)
    return ReactComponent
  }
  this.props = props;
  this.context = context;
}
BerkeleyTrue commented 9 years ago

@mlrawlings That looks great!

jjoos commented 9 years ago

@mlrawlings :+1:

brigand commented 9 years ago

Here's an implementation of @mlrawlings's syntax http://jsbin.com/tofidoxucu/2/edit?js,console,output

It does look much cleaner. If I switch to es6 classes, I'll probably write it like that, and have a small transform to move it out of there. Early errors aren't worth giving up imo, but luckily we don't need to compromise :-)

gaearon commented 9 years ago

In my opinion higher order components and the new observe hook in 0.14 solve almost all use cases I had for mixins.

gsklee commented 9 years ago

@gaearon Looks promising, but a little bit too React-specific. @mlrawlings's example is the most ideal but it'd require manually wrapping all classes with a function; I wonder if there is any possible way to abstract that part out into a generic lib?

chicoxyzzy commented 9 years ago

there will be TC39 proposal for annotations https://github.com/Microsoft/TypeScript/issues/1557#issuecomment-77709527 can we use something like that to abstract mixins?

chicoxyzzy commented 9 years ago

https://github.com/wycats/javascript-decorators/blob/master/README.md

brigand commented 9 years ago

@chicoxyzzy it doesn't really add any value, other than being able to declare the mixins before the class in code. Annotations don't do anything, so you need something else to act on them.

With classes, either directly on the class via Class.prototype or on instances via this.constructor.annotate, Object.getPrototypeOf(this).constructor.annotate, etc.

// used for an instanceof check, because other annotations may be used
class ReactMixin {
    constructor(mixin){ this.mixin = mixin }
}

@ReactMixin(PureRenderMixin)
class Foo extends React.Component {
   ...
}
applyMixins(Foo);

function applyMixins(Class){
    if (Class.annotate) {
       Class.annotate.forEach(maybeMixin => {
           // apply the mixin behavior for each mixin annotation
           if (maybeMixin instanceof ReactMixin) 
               reactMixin(Class.prototype, maybeMixin.mixin);
       })
    }
}

As @gaearon pointed out, you can expect alternatives to mixins in the coming months... maybe before we get annotation support in babel.

donaldpipowitch commented 9 years ago

I think Babel already has annotation support behind a flag.

andreypopp commented 9 years ago

@brigand I think the idea is to add decorators, not annotations to ES7. Class decorators are code so it's possible to have withMixins decorator which works like:

@withMixins(PureRenderMixin)
class Foo extends React.Component {
   ...
}

I also think that decorator syntax will be useful for higher order components, so instead of:

class Foo extends React.Component {
  ...
}
Foo = Enhance(Foo)

we can write:

@Enhance
class Foo extends React.Component {
  ...
}
rstuven commented 9 years ago

@brigand @andreypopp Here's a description of decorators that includes API and desugaring: https://github.com/wycats/javascript-decorators

This is already implemented in Babel 5+ and TypeScript 1.5

jedwards1211 commented 9 years ago

Will we ever have automatic autobinding again?

sophiebits commented 9 years ago

@jedwards1211 Unlikely. We'll probably recommend using a ES-future syntax feature when it lands: https://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding.

jedwards1211 commented 9 years ago

Hopefully the come up with something nice for that. More often than not, whenever I've passed functions around, I've wished they were autobound, even outside of React code.

I understand why React devs don't "think you're in the business of designing a class system," but when you're going to start using an entire view framework, it's not that much cognitive burden to learn its class system. Certainly less cognitive burden for me than keeping track of all this churn.

gaearon commented 7 years ago

I guess this ship has sailed.