Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inline fbjs/lib/emptyObject #13055

Merged
merged 9 commits into from Jun 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 7 additions & 3 deletions packages/react-art/src/ReactARTHostConfig.js
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
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
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
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
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
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
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
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;
Copy link
Collaborator Author

@gaearon gaearon Jun 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part was a bit shady.

By default we set inst.refs to a frozen, shared object (in fact, we do this in two places: Component constructor, and later in the renderer). Whenever we want to mutate a ref for the first time, we check whether it's still "the same" pooled object, and if it is, we swap it out with the mutable one.

In some obscure cases where emptyObject identity changes over time (e.g. due to a module reset in Jest, or due to a duplicate fbjs), this wouldn't work and cause very confusing errors.

I've changed this to use an explicit flag instead. It is only set and read in this module. By default it's not set.

When the flag is not set, the first attempt to set a legacy ref will know that we need to create a new mutable refs object. Then we set the flag. Next time we attach a ref, the flag tells us it's safe to mutate now.

I could have put the flag on the mutable refs object instead. But that could break code like Object.keys(this.refs).map(...). I could make it non-enumerable. But Object.defineProperty is kinda slow. I figured an instance property is a fair game because we already use a bunch of those for legacy context.

This only affects classes using string refs. There is no cost for classes that don't use them. Even for those that do, it's a single boolean field.

This lets us get rid of the dependency on strict equality.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You’re messing with the hidden classes here so there can still be a cost.

I’m not sure this is worth it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're already messing with them for legacy context. The only kind of components I'm worried about regressing (Relay) has both legacy context and legacy refs. This gives an incentive to migrate off both without regressing the current situation.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess context is more localized in practice than legacy refs. Maybe not worth risking.

I can keep it on the React object but I think this will require some mocking of React itself in tests to ensure the object is preserved throughout resetModules. It was a bit easier for a separate module.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t see how the problems you mentioned trying to fix are still problems after this change even without this.

Even before I’m not sure how that could happen before but since it is now inline here, how would this happen? Somehow try to reuse the instance in the same renderer?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed an alternative fix

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
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