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.

@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