Skip to content

Instantly share code, notes, and snippets.

@sebmarkbage
Last active May 17, 2022 11:06
Show Gist options
  • Save sebmarkbage/d7bce729f38730399d28 to your computer and use it in GitHub Desktop.
Save sebmarkbage/d7bce729f38730399d28 to your computer and use it in GitHub Desktop.
New React Element Factories and JSX

New React Element Factories and JSX

In React 0.12, we're making a core change to how React.createClass(...) and JSX works.

If you're using JSX in the typical way for all (and only) React components, then this transition will be seamless. Otherwise there are some minor breaking changes described below.

The Problem

Currently var Button = React.createClass(...) does two things. It creates a class and a helper function to create ReactElements. It is essentially equivalent to this:

class ButtonClass {
}

function ButtonFactory(...args) {
  return React.createElement(ButtonClass, ...args);
}

module.exports = ButtonFactory;

Then you access this in the consuming component by invoking the ButtonFactory.

var Button = require('Button');

class App {
  render() {
    return Button({ prop: 'foo '}); // ReactElement
  }
}

Conceptually this is the wrong model. The source component should not be responsible for the output of App.

There are a few problems with this:

  • ES6 classes can't be directly exported, they need to be wrapped.
  • There's no convenient way to access the actual class and it's confusing which one you're using.
  • Static methods are wrapped in helpers that are not real function. As a convenience.
  • Auto-mocking destroys the factory so there is no way to test the result of render without disabling mocking.
  • Factories can be wrapped by other factories that returns something different than ReactElements. Making testing and optimizations impossible.
  • Languages with specialized features for object management have to defer to React instead of using the built-in features.

The Solution

In future versions of React the same var Button = React.createClass(...) will start producing a simple class:

class Button {
}

module.exports = Button;

This means that you can't use that function directly to create a ReactElement. Instead, the consumer will have to create a helper:

var Button = React.createFactory(require('Button'));

class App {
  render() {
    return Button({ prop: 'foo '}); // ReactElement
  }
}

We simply move React.createFactory from inside React.createClass to your consuming module. As a convenience, JSX will automatically create a factory for you. If you're using JSX you don't need to change your code:

var Button = require('Button');
class App {
  render() {
    return <Button prop="foo" />; // ReactElement
  }
}

Breaking Changes

JSX Depends on React

You used to be able to use JSX without requiring React. That's no longer possible.

/** @jsx React.DOM **/
var Foo = require('foo');
function helper() {
  return <Foo />; // Throws an error "React is not defined"
}

You'll need to require React in the modules where you use JSX. However, you no longer need to use the @jsx docblock directive!

var React = require('react');
var Foo = require('foo');
function helper() {
  return <Foo />; // yay! It works.
}

JSX Doesn't Work With Arbitrary Functions

It used to be possible to use JSX with non-React components. I.e. simple function calls:

function Foo(props) { } // Not a React component

function helper() {
  return <Foo prop="foo" />; // Throws an error
}

This will no longer work since Foo is not a React component.

Non-JSX Function Calls Must Be Wrapped in createFactory

It will no longer be possible to invoke a React component as a function. You can either use JSX, or React.createFactory.

var Foo = require('foo'); // A React component
function helper() {
  return Foo({ props: 'foo' }); // Throws an error
}

You can use either JSX or wrap it in React.createFactory:

var React = require('react');
var Foo = React.createFactory(require('foo'));
function helper() {
  return Foo({ props: 'foo' }); // yay! It works.
}

Factories Will Not Have Static Methods

Unfortunately factories will not proxy static methods but you can still access them directly on the class:

var React = require('react');
var FooClass = require('foo');
var Foo = React.createFactory(FooClass);
function helper() {
  FooClass.foo(); // static method
  return Foo({ props: 'foo' }); // ReactElement
}

If you use JSX, this will be managed automatically for you.

Mock Components Won't Be Fired

You can't assert that a component is used by the number of time a mock is called.

var ComponentA = require('A'); // mock
var ComponentB = require('B'); // B renders A
render(<ComponentB />);
expect(ComponentA.mock.calls.length).toBe(1); // this is no longer called

Intermediate Help

We realize that you won't be able to upgrade all your code at once. To help you out, we're providing an intermediate version that works the same as before but it logs warnings when you're using an unsupported pattern. I.e. if you're using abitrary functions in JSX, or calling React classes directly.

What Does This Solve Exactly?

Simpler API for ES6 Classes

Future React will be able to export ES6 classes directly from modules. Instead of:

class FooClass {
}
export var Foo = React.createFactory(FooClass); // ugly

You can just export the class directly:

export class Foo {
}

It's easy to access and instantiate the class if you need to. E.g. for testing.

Static Methods are Just Static Method

Static methods are currently exposed as wrappers around the inner class's function.

var Foo = React.createClass({
  statics: {
    foo: function() {
      return this === Foo;
    }
  }
});
Foo.foo(); // false!

With this change, static methods can be exposed directly:

class Foo {
  static foo() {
    return this === Foo;
  }
}
Foo.foo(); // true!

Auto-mocking Just Works

If you use jest for unit testing, then mocks are automatically created for every module. This means that if you try to invoke a module as a function it will just return undefined.

var Button = require('Button'); // mock
class App {
  render() {
    return <Button prop="foo" />;
  }
}

If you currently call new App().render() it will return undefined in a jest environment. With the new changes, this will return a ReactElement that you can assert on. E.g.

expect(new App().render().type).toBe(Button); // This just works

Optimizations

Since it's now the responsibility of the consumer to create a ReactElement efficiently, that opens up a lot of opportunities for us to do optimizations in the JS transforms that may be dependent on how the consumer uses them. For example inline objects, pooling and reusing constant objects.

var Button = require('Button'); // mock
var sameEveryTime = { type: Button, props: { prop: "foo" } };
class App {
  render() {
    return sameEveryTime;
  }
}

This should enable significant performance improvements to large React application.

Language Interop

Certain languages that compile to JS have various language features that can be used to create ReactElements efficiently. By moving it into the consumer, these don't have to learn how to interop with other components written in other languages. Each component is stand-alone and as long as it works with React it doesn't have to know how other components are built.

@jquense
Copy link

jquense commented Sep 15, 2014

can anyone assert that I have this right? it seems that one of the advantages is that the below changes from

class FooClass {}
export var Foo = React.createFactory(FooClass); // ugly

to

  export class Foo {}

expect that in another file I still have to import and then wrap it in createFactory? that seems like the same thing, not a change, what am i missing. Wouldn't most of us just want to export a factory instead of a class, seems like extra work to require and wrap every class over and over again?

Can I make any constructor function a react descriptor?

@sebmarkbage
Copy link
Author

@theporchrat that is a good point. You could for example do your own helper that does that internally:

function createClass(spec) {
  function MyComponent() {}
  MyComponent.prototype = spec;
  return React.createFactory(MyComponent);
}

However, we very explicitly don't want to encourage that pattern. It means that your components are no longer compatible with other React components that don't do this. You can certainly do that yourself if you want to, but you won't be compatible with the overall React ecosystem.

@mtscout6 we have an experimental place for ideas which we're considering here: https://github.com/reactjs/react-future I think we posted about it before but we might need to make some more noise about it. It's difficult to tell how much noise to make about these features before they're fully baked.

Sometimes people tend to go adopt the new stuff before it's fully baked and then it turns out that we can't adopt it. It's more dangerous to use the stuff that is not finalized than anything that was released. Because we always have an upgrade path/plan from anything that was released, but we can't promise that for some theoretical future change.

@riovv
Copy link

riovv commented Oct 2, 2014

I think maybe having a disclaimer in the top of these gists would be a nice gesture to all the people who came here directly from google (me included).

@rattrayalex
Copy link

Agreed that this should be on the react site. It sounds like the stuff described here isn't just some nebulous future for React but the actual hard plan, if not already the reality. Should be clear to all. Once it's up on the site I hope that the gist will be edited to point people there.

@rattrayalex
Copy link

Perhaps React could provide a base class for ES6 extends so that non-JSX users don't have to call React.createFactory every time they want to import/use a component? I don't think a pattern like class MyButton extends React.ReactElement would be strange to anyone, and I assume it could include its own factory function that React could call as it does now. There could also be other conveniences added for things like testing

@Janekk
Copy link

Janekk commented Oct 24, 2014

When using v0.12-rc1 I get confusing warnings from React about calling a React component directly. Consider a following sample:

var React = require('react');

var Test = React.createClass({
  render: function() {
    return (
      <b>{this.props.name}</b>
    );
  }
});

var HelloWorld = React.createClass({
  render: function() {
    return (
      <p>{"Hello " + this.props.name}
        <Test name={this.props.name} />
      </p>
    );
  }
});

module.exports = HelloWorld;

After runnig this I get a console warning:
Warning: HelloWorld is calling a React component directly. Use a factory or JSX instead. See: http://fb.me/react-legacyfactory

Shouldn't it be perfectly fine according to what's written in this Gist?

@Janekk
Copy link

Janekk commented Oct 26, 2014

Update to my last comment: it turned out that the problem was reactify module for browserify uses v0.11. Meanwhile, till 0.12 is officially released this github version can be used: andreypopp/reactify

@sheerun
Copy link

sheerun commented Oct 29, 2014

@sebmarkbage Did you consider using annotations the same way as Angular will do for directives?

import ReactElement from 'react'

export class Button {
}

Button.annotations = [
  new ReactElement()
]

React could check if element returned from render has ReactElement instance in annotations, and automatically instantiate it using reactElement.createFactory().

An additional bonus is ability to parametrize ReactElement in future and not breaking 0.11 API ;/

I think creating factories for elements shouldn't be the consumer job. Annotations are better solution.

Also note the current traceur already implemented experimental syntax for annotations (java-like syntax).

@sheerun
Copy link

sheerun commented Oct 29, 2014

Ah, right. Button needs to be a factory, not a class. Annotations probably won't work ;/ (unless you're willing deprecate Button() and use new Button() everywhere).

@antris
Copy link

antris commented Oct 31, 2014

Wow... A 1146 word tutorial for just upgrading a supposedly "simple" framework. :(

@noahgrant
Copy link

Using the factory loses the instanceof connection; how is it proposed we check if a component instance is an instance of a particular component class?

@awei01
Copy link

awei01 commented Nov 21, 2014

@noahgrant. currently having same problem. been flailing around trying to find a solution especially w/ regard to testing.

@awei01
Copy link

awei01 commented Nov 21, 2014

@noahgrant, not sure if you were running into what i had been, but, FWIW i found some workarounds. check out: facebook/react#2576 and https://github.com/awei01/react-jest-testing-illustration

@mlmorg
Copy link

mlmorg commented Dec 11, 2014

@rattrayalex I wrote a thin wrapper around React.createElement to create a nicer interface on top of React.DOM as well as prevent the need for React.createFactory - https://github.com/mlmorg/react-hyperscript

@augustl
Copy link

augustl commented Jan 22, 2015

This has had very little practical implications for me. What used to be:

var MyComp = React.createClass({
    // ...
});

is now:

var MyCompClass = React.createClass({
    // ...
});
var MyComp = React.createFactory(MyCompClass);

@marr
Copy link

marr commented Jun 9, 2015

Is something like this going to be possible with class syntax?
https://gist.github.com/marr/e2e85553fac822fb3adf
I want the child to be a subclass of the object returned from react.createClass

@topgun743
Copy link

Nice. Good elaboration especially in conjunction with ES6

@valetarton
Copy link

@marr did you ever have any luck on this?

@farsightsoftware
Copy link

At least this part is no longer correct, right?

JSX Doesn't Work With Arbitrary Functions

It used to be possible to use JSX with non-React components. I.e. simple function calls:

function Foo(props) { } // Not a React component

function helper() {
  return <Foo prop="foo" />; // Throws an error
}

This will no longer work since Foo is not a React component.

Because SFCs work just fine: Fiddle

<div id="react"></div>
const Foo = props => <div>{props.message}, it's {props.time}</div>;

ReactDOM.render(
  <Foo message="Hi" time={String(new Date())} />,
  document.getElementById("react")
);

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