Description
Starting with 0.14 we will be able to start treating ReactElements and their props objects as value types. I.e. any instance is conceptually equivalent if all their values are the same. This allow us to reuse any ReactElement whose inputs are deeply immutable or effectively constant.
Take this function for example:
function render() {
return <div className="foo" />;
}
This can be optimized by moving the JSX out of the function body so that each time it is called the same instance is returned:
var foo = <div className="foo" />;
function render() {
return foo;
}
Not only does it allow us to reuse the same objects, React will automatically bail out any reconciliation of constant components - without a manual shouldComponentUpdate
.
Reference Equality
Objects in JavaScript have reference equality. Meaning that this optimization can actually change behavior of code. If any of your calls to render() uses object equality or uses the ReactElement as the key in a Map, this optimization will break that use case. So don't rely on that.
This is a change in the semantic contract of ReactElements. This is difficult to enforce, but hopefully a future version of JavaScript will have the notion of value equality for custom objects so this can be enforced.
What is Constant?
The simplest assumption is if the entire expression, including all the props (and children), are all literal value types (strings, booleans, null, undefined or JSX), then the result is constant.
function render() {
return <div className="foo"><input type="checkbox" checked={true} /></div>;
}
If a variable is used in the expression, then you must first ensure that it is not ever mutated since then the timing can affect the behavior.
var Foo = require('Foo');
function createComponent(text) {
return function render() {
return <Foo>{text}</Foo>;
};
}
It is safe to move a constant to a higher closure if the variable is never mutated. You can only move it to a scope that is shared by all variables.
var Foo = require('Foo');
function createComponent(text) {
var foo = <Foo>{text}</Foo>;
return function render() {
return foo;
};
}
Are Objects Constant?
Arbitrary objects are not considered constant. A transpiler should NEVER move a ReactElement scope if any of the parameters is a mutable object. React will silently ignore updates and it will change behavior.
function render() {
return <div style={{ width: 100 }} />; // Not safe to reuse...
}
// ...because I might do:
render().props.style.width = 200;
expect(render().props.style.width).toBe(100);
If an object is provably deeply immutable (or effectively immutable by never being mutated), the transpiler may only move it to the scope where the object was created or received.
function render() {
var style = Object.freeze({ __proto__: null, width: 100 });
return <div style={style} />; // Not safe to reuse...
}
// ...because I might do:
expect(render().props.style).not.toBe(render().props.style);
// However this is...
function createComponent(width) {
var style = Object.freeze({ __proto__: null, width: +width });
return function render() {
return <div style={style} />; // ...safe to move this one level up
};
}
This is due to the fact that arbitrary objects have referential identity in JavaScript. However, if the semantics of an immutable object is expected to have value equality, it might be ok to treat them as value types. For example any data structure created by immutable-js may be treated as a value type if it is deeply immutable.
Exception: ref="string"
There is unfortunately one exception. If the ref
prop might potentially might have a string value. Then it is never safe to reuse the element. This is due to the fact that we capture the React owner at the time of creation. This is an unfortunate artifact and we're looking at various options of changing the refs semantics to fix it.
render() {
// Neither of these...
return <div ref="str" />;
// ...are safe to reuse...
return <div ref={possibleStringValue} />;
// ...because they might contain a ref.
return <div {...objectThatMightContainARef} />;
}
Non-JSX
This can work on JSX, React.createElement or functions created by React.createFactory.
For example, it is safe to assume that this function call generates a constant ReactElement.
var Foo = React.createFactory(FooClass);
function render() {
return Foo({ bar: 1 });
}
Therefore it is safe to reuse:
var Foo = React.createFactory(FooClass);
var foo = Foo({ bar: 1 }};
function render() {
return foo;
}
Advanced Optimizations
You can also imagine even more clever optimizations that optimize per-instance elements by memoizing it on the instance. This allows auto-bound methods to be treated as effectively constant.
If you can track pure-functions, you can even treat calculated values as constants if the input to the pure function is constant.
Static analysis tools like Flow makes it possible to detect that even more elements are constant.