Skip to content

Commit

Permalink
Inline fbjs/lib/emptyObject (#13055)
Browse files Browse the repository at this point in the history
* Inline fbjs/lib/emptyObject

* Explicit naming

* Compare to undefined

* Another approach for detecting whether we can mutate

Each renderer would have its own local LegacyRefsObject function.

While in general we don't want `instanceof`, here it lets us do a simple check: did *we* create the refs object?
Then we can mutate it.

If the check didn't pass, either we're attaching ref for the first time (so we know to use the constructor),
or (unlikely) we're attaching a ref to a component owned by another renderer. In this case, to avoid "losing"
refs, we assign them onto the new object. Even in that case it shouldn't "hop" between renderers anymore.

* Clearer naming

* Add test case for strings refs across renderers

* Use a shared empty object for refs by reading it from React

* Remove string refs from ReactART test

It's not currently possible to resetModules() between several renderers
without also resetting the `React` module. However, that leads to losing
the referential identity of the empty ref object, and thus subsequent
checks in the renderers for whether it is pooled fail (and cause assignments
to a frozen object).

This has always been the case, but we used to work around it by shimming
fbjs/lib/emptyObject in tests and preserving its referential identity.
This won't work anymore because we've inlined it. And preserving referential
identity of React itself wouldn't be great because it could be confusing during
testing (although we might want to revisit this in the future by moving its
stateful parts into a separate package).

For now, I'm removing string ref usage from this test because only this is
the only place in our tests where we hit this problem, and it's only
related to string refs, and not just ref mechanism in general.

* Simplify the condition
  • Loading branch information
gaearon authored Jun 19, 2018
1 parent ae14317 commit b1b3acb
Show file tree
Hide file tree
Showing 15 changed files with 136 additions and 46 deletions.
10 changes: 7 additions & 3 deletions packages/react-art/src/ReactARTHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ import * as ReactScheduler from 'shared/ReactScheduler';
import Transform from 'art/core/transform';
import Mode from 'art/modes/current';
import invariant from 'fbjs/lib/invariant';
import emptyObject from 'fbjs/lib/emptyObject';

import {TYPES, EVENT_TYPES, childrenAsString} from './ReactARTInternals';

const pooledTransform = new Transform();

const NO_CONTEXT = {};
const UPDATE_SIGNAL = {};
if (__DEV__) {
Object.freeze(NO_CONTEXT);
Object.freeze(UPDATE_SIGNAL);
}

/** Helper Methods */

Expand Down Expand Up @@ -318,11 +322,11 @@ export function shouldDeprioritizeSubtree(type, props) {
}

export function getRootHostContext() {
return emptyObject;
return NO_CONTEXT;
}

export function getChildHostContext() {
return emptyObject;
return NO_CONTEXT;
}

export const scheduleDeferredCallback = ReactScheduler.scheduleWork;
Expand Down
22 changes: 14 additions & 8 deletions packages/react-art/src/__tests__/ReactART-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ describe('ReactART', () => {
Surface = ReactART.Surface;

TestComponent = class extends React.Component {
group = React.createRef();

render() {
const a = (
<Shape
Expand Down Expand Up @@ -104,7 +106,7 @@ describe('ReactART', () => {

return (
<Surface width={150} height={200}>
<Group ref="group">
<Group ref={this.group}>
{this.props.flipped ? [b, a, c] : [a, b, c]}
</Group>
</Surface>
Expand All @@ -121,7 +123,7 @@ describe('ReactART', () => {
it('should have the correct lifecycle state', () => {
let instance = <TestComponent />;
instance = ReactTestUtils.renderIntoDocument(instance);
const group = instance.refs.group;
const group = instance.group.current;
// Duck type test for an ART group
expect(typeof group.indicate).toBe('function');
});
Expand Down Expand Up @@ -260,15 +262,17 @@ describe('ReactART', () => {
let ref = null;

class Outer extends React.Component {
test = React.createRef();

componentDidMount() {
ref = this.refs.test;
ref = this.test.current;
}

render() {
return (
<Surface>
<Group>
<CustomShape ref="test" />
<CustomShape ref={this.test} />
</Group>
</Surface>
);
Expand All @@ -289,26 +293,28 @@ describe('ReactART', () => {
let ref = {};

class Outer extends React.Component {
test = React.createRef();

componentDidMount() {
ref = this.refs.test;
ref = this.test.current;
}

componentDidUpdate() {
ref = this.refs.test;
ref = this.test.current;
}

render() {
return (
<Surface>
<Group>
{this.props.mountCustomShape && <CustomShape ref="test" />}
{this.props.mountCustomShape && <CustomShape ref={this.test} />}
</Group>
</Surface>
);
}
}
ReactDOM.render(<Outer />, container);
expect(ref).not.toBeDefined();
expect(ref).toBe(null);
ReactDOM.render(<Outer mountCustomShape={true} />, container);
expect(ref.constructor).toBe(CustomShape);
});
Expand Down
43 changes: 43 additions & 0 deletions packages/react-dom/src/__tests__/refs-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -412,3 +412,46 @@ describe('creating element with ref in constructor', () => {
);
});
});

describe('strings refs across renderers', () => {
it('does not break', () => {
class Parent extends React.Component {
render() {
// This component owns both refs.
return (
<Indirection
child1={<div ref="child1" />}
child2={<div ref="child2" />}
/>
);
}
}

class Indirection extends React.Component {
componentDidUpdate() {
// One ref is being rendered later using another renderer copy.
jest.resetModules();
const AnotherCopyOfReactDOM = require('react-dom');
AnotherCopyOfReactDOM.render(this.props.child2, div2);
}
render() {
// The other one is being rendered directly.
return this.props.child1;
}
}

const div1 = document.createElement('div');
const div2 = document.createElement('div');
const inst = ReactDOM.render(<Parent />, div1);
// Only the first ref has rendered yet.
expect(inst.refs.child1.tagName).toBe('DIV');
expect(inst.refs.child1).toBe(div1.firstChild);

// Now both refs should be rendered.
ReactDOM.render(<Parent />, div1);
expect(inst.refs.child1.tagName).toBe('DIV');
expect(inst.refs.child1).toBe(div1.firstChild);
expect(inst.refs.child2.tagName).toBe('DIV');
expect(inst.refs.child2).toBe(div2.firstChild);
});
});
6 changes: 5 additions & 1 deletion packages/react-dom/src/server/ReactPartialRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import type {
} from 'shared/ReactTypes';

import React from 'react';
import emptyObject from 'fbjs/lib/emptyObject';
import invariant from 'fbjs/lib/invariant';
import lowPriorityWarning from 'shared/lowPriorityWarning';
import warning from 'fbjs/lib/warning';
Expand Down Expand Up @@ -284,6 +283,11 @@ function flattenOptionChildren(children: mixed): string {
return content;
}

const emptyObject = {};
if (__DEV__) {
Object.freeze(emptyObject);
}

function maskContext(type, context) {
const contextTypes = type.contextTypes;
if (!contextTypes) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ import {
} from 'react-reconciler/reflection';
import getComponentName from 'shared/getComponentName';
import {HostComponent} from 'shared/ReactTypeOfWork';
import emptyObject from 'fbjs/lib/emptyObject';
import invariant from 'fbjs/lib/invariant';
// Module provided by RN:
import UIManager from 'UIManager';

import {getClosestInstanceFromNode} from './ReactNativeComponentTree';

const emptyObject = {};
if (__DEV__) {
Object.freeze(emptyObject);
}

let getInspectorDataForViewTag;

if (__DEV__) {
Expand Down
8 changes: 6 additions & 2 deletions packages/react-native-renderer/src/ReactNativeHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import type {ReactNativeBaseComponentViewConfig} from './ReactNativeTypes';

import emptyObject from 'fbjs/lib/emptyObject';
import invariant from 'fbjs/lib/invariant';

// Modules provided by RN:
Expand Down Expand Up @@ -43,6 +42,11 @@ export type HostContext = $ReadOnly<{|
export type UpdatePayload = Object; // Unused
export type ChildSet = void; // Unused

const UPDATE_SIGNAL = {};
if (__DEV__) {
Object.freeze(UPDATE_SIGNAL);
}

// Counter for uniquely identifying views.
// % 10 === 1 means it is a rootTag.
// % 2 === 0 means it is a Fabric tag.
Expand Down Expand Up @@ -218,7 +222,7 @@ export function prepareUpdate(
rootContainerInstance: Container,
hostContext: HostContext,
): null | Object {
return emptyObject;
return UPDATE_SIGNAL;
}

export function resetAfterCommit(containerInfo: Container): void {
Expand Down
13 changes: 9 additions & 4 deletions packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue';
import type {ReactNodeList} from 'shared/ReactTypes';

import * as ReactPortal from 'shared/ReactPortal';
import emptyObject from 'fbjs/lib/emptyObject';
import expect from 'expect';

type Container = {rootID: string, children: Array<Instance | TextInstance>};
Expand All @@ -32,8 +31,14 @@ type Instance = {|
|};
type TextInstance = {|text: string, id: number|};

const NO_CONTEXT = {};
const UPDATE_SIGNAL = {};
if (__DEV__) {
Object.freeze(NO_CONTEXT);
Object.freeze(UPDATE_SIGNAL);
}

function createReactNoop(reconciler: Function, useMutation: boolean) {
const UPDATE_SIGNAL = {};
let scheduledCallback = null;

let instanceCounter = 0;
Expand Down Expand Up @@ -84,11 +89,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
if (failInBeginPhase) {
throw new Error('Error in host config.');
}
return emptyObject;
return NO_CONTEXT;
},

getChildHostContext() {
return emptyObject;
return NO_CONTEXT;
},

getPublicInstance(instance) {
Expand Down
8 changes: 6 additions & 2 deletions packages/react-reconciler/src/ReactChildFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
Fragment,
} from 'shared/ReactTypeOfWork';
import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook';
import emptyObject from 'fbjs/lib/emptyObject';
import invariant from 'fbjs/lib/invariant';
import warning from 'fbjs/lib/warning';

Expand All @@ -39,6 +38,7 @@ import {
createFiberFromText,
createFiberFromPortal,
} from './ReactFiber';
import {emptyRefsObject} from './ReactFiberClassComponent';
import ReactDebugCurrentFiber from './ReactDebugCurrentFiber';
import {StrictMode} from './ReactTypeOfMode';

Expand Down Expand Up @@ -157,7 +157,11 @@ function coerceRef(
return current.ref;
}
const ref = function(value) {
const refs = inst.refs === emptyObject ? (inst.refs = {}) : inst.refs;
let refs = inst.refs;
if (refs === emptyRefsObject) {
// This is a lazy pooled frozen object, so we need to initialize.
refs = inst.refs = {};
}
if (value === null) {
delete refs[stringRef];
} else {
Expand Down
11 changes: 8 additions & 3 deletions packages/react-reconciler/src/ReactFiberClassComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import type {Fiber} from './ReactFiber';
import type {ExpirationTime} from './ReactFiberExpirationTime';

import React from 'react';
import {Update, Snapshot} from 'shared/ReactTypeOfSideEffect';
import {
debugRenderPhaseSideEffects,
Expand All @@ -20,7 +21,6 @@ import ReactStrictModeWarnings from './ReactStrictModeWarnings';
import {isMounted} from 'react-reconciler/reflection';
import * as ReactInstanceMap from 'shared/ReactInstanceMap';
import shallowEqual from 'shared/shallowEqual';
import emptyObject from 'fbjs/lib/emptyObject';
import getComponentName from 'shared/getComponentName';
import invariant from 'fbjs/lib/invariant';
import warning from 'fbjs/lib/warning';
Expand All @@ -43,6 +43,7 @@ import {
getUnmaskedContext,
isContextConsumer,
hasContextChanged,
emptyContextObject,
} from './ReactFiberContext';
import {
recalculateCurrentTime,
Expand All @@ -53,6 +54,10 @@ import {
const fakeInternalInstance = {};
const isArray = Array.isArray;

// React.Component uses a shared frozen object by default.
// We'll use it to determine whether we need to initialize legacy refs.
export const emptyRefsObject = new React.Component().refs;

let didWarnAboutStateAssignmentForComponent;
let didWarnAboutUninitializedState;
let didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate;
Expand Down Expand Up @@ -469,7 +474,7 @@ function constructClassInstance(
const needsContext = isContextConsumer(workInProgress);
const context = needsContext
? getMaskedContext(workInProgress, unmaskedContext)
: emptyObject;
: emptyContextObject;

// Instantiate twice to help detect side-effects.
if (__DEV__) {
Expand Down Expand Up @@ -658,7 +663,7 @@ function mountClassInstance(

instance.props = props;
instance.state = workInProgress.memoizedState;
instance.refs = emptyObject;
instance.refs = emptyRefsObject;
instance.context = getMaskedContext(workInProgress, unmaskedContext);

if (__DEV__) {
Expand Down
Loading

0 comments on commit b1b3acb

Please sign in to comment.