diff --git a/packages/react-dom/src/__tests__/ReactComponent-test.js b/packages/react-dom/src/__tests__/ReactComponent-test.js
index 94d16543e43..d08ba34a27d 100644
--- a/packages/react-dom/src/__tests__/ReactComponent-test.js
+++ b/packages/react-dom/src/__tests__/ReactComponent-test.js
@@ -162,7 +162,7 @@ describe('ReactComponent', () => {
ReactTestUtils.renderIntoDocument(} />);
});
- it('should support new-style refs', () => {
+ it('should support callback-style refs', () => {
const innerObj = {};
const outerObj = {};
@@ -202,6 +202,49 @@ describe('ReactComponent', () => {
expect(mounted).toBe(true);
});
+ it('should support object-style refs', () => {
+ const innerObj = {};
+ const outerObj = {};
+
+ class Wrapper extends React.Component {
+ getObject = () => {
+ return this.props.object;
+ };
+
+ render() {
+ return
{this.props.children}
;
+ }
+ }
+
+ let mounted = false;
+
+ class Component extends React.Component {
+ constructor() {
+ super();
+ this.innerRef = React.createRef();
+ this.outerRef = React.createRef();
+ }
+ render() {
+ const inner = ;
+ const outer = (
+
+ {inner}
+
+ );
+ return outer;
+ }
+
+ componentDidMount() {
+ expect(this.innerRef.value.getObject()).toEqual(innerObj);
+ expect(this.outerRef.value.getObject()).toEqual(outerObj);
+ mounted = true;
+ }
+ }
+
+ ReactTestUtils.renderIntoDocument();
+ expect(mounted).toBe(true);
+ });
+
it('should support new-style refs with mixed-up owners', () => {
class Wrapper extends React.Component {
getTitle = () => {
diff --git a/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.js b/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.js
index 42fc69cca00..78d50ee8806 100644
--- a/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.js
+++ b/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.js
@@ -937,7 +937,7 @@ describe('ReactErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
- it('resets refs if mounting aborts', () => {
+ it('resets callback refs if mounting aborts', () => {
function childRef(x) {
log.push('Child ref is set to ' + x);
}
@@ -981,6 +981,44 @@ describe('ReactErrorBoundaries', () => {
]);
});
+ it('resets object refs if mounting aborts', () => {
+ let childRef = React.createRef();
+ let errorMessageRef = React.createRef();
+
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // Handle error:
+ // Finish mounting with null children
+ 'ErrorBoundary componentDidMount',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ // Render the error message
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+ expect(errorMessageRef.value.toString()).toEqual('[object HTMLDivElement]');
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ expect(errorMessageRef.value).toEqual(null);
+ });
+
it('successfully mounts if no error occurs', () => {
const container = document.createElement('div');
ReactDOM.render(
diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js
index 5af06211a75..e65872dd002 100644
--- a/packages/react-reconciler/src/ReactChildFiber.js
+++ b/packages/react-reconciler/src/ReactChildFiber.js
@@ -104,7 +104,11 @@ function coerceRef(
element: ReactElement,
) {
let mixedRef = element.ref;
- if (mixedRef !== null && typeof mixedRef !== 'function') {
+ if (
+ mixedRef !== null &&
+ typeof mixedRef !== 'function' &&
+ typeof mixedRef !== 'object'
+ ) {
if (__DEV__) {
if (returnFiber.mode & StrictMode) {
const componentName = getComponentName(returnFiber) || 'Component';
@@ -113,7 +117,7 @@ function coerceRef(
false,
'A string ref has been found within a strict mode tree. ' +
'String refs are a source of potential bugs and should be avoided. ' +
- 'We recommend using a ref callback instead.' +
+ 'We recommend using createRef() instead.' +
'\n%s' +
'\n\nLearn more about using refs safely here:' +
'\nhttps://fb.me/react-strict-mode-string-ref',
diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js
index a5f7cd7a3e6..578986fcf22 100644
--- a/packages/react-reconciler/src/ReactFiber.js
+++ b/packages/react-reconciler/src/ReactFiber.js
@@ -7,7 +7,7 @@
*/
import type {ReactElement, Source} from 'shared/ReactElementType';
-import type {ReactPortal} from 'shared/ReactTypes';
+import type {ReactPortal, RefObject} from 'shared/ReactTypes';
import type {TypeOfWork} from 'shared/ReactTypeOfWork';
import type {TypeOfMode} from './ReactTypeOfMode';
import type {TypeOfSideEffect} from 'shared/ReactTypeOfSideEffect';
@@ -107,7 +107,7 @@ export type Fiber = {|
// The ref last used to attach this node.
// I'll avoid adding an owner field for prod and model that as functions.
- ref: null | (((handle: mixed) => void) & {_stringRef: ?string}),
+ ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,
// Input is the data coming into process this fiber. Arguments. Props.
pendingProps: any, // This type will be more specific once we overload the tag.
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js
index cf6d598f7ef..b18c8d275c9 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.js
@@ -77,18 +77,22 @@ export default function(
function safelyDetachRef(current: Fiber) {
const ref = current.ref;
if (ref !== null) {
- if (__DEV__) {
- invokeGuardedCallback(null, ref, null, null);
- if (hasCaughtError()) {
- const refError = clearCaughtError();
- captureError(current, refError);
+ if (typeof ref === 'function') {
+ if (__DEV__) {
+ invokeGuardedCallback(null, ref, null, null);
+ if (hasCaughtError()) {
+ const refError = clearCaughtError();
+ captureError(current, refError);
+ }
+ } else {
+ try {
+ ref(null);
+ } catch (refError) {
+ captureError(current, refError);
+ }
}
} else {
- try {
- ref(null);
- } catch (refError) {
- captureError(current, refError);
- }
+ ref.value = null;
}
}
}
@@ -175,12 +179,18 @@ export default function(
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
+ let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
- ref(getPublicInstance(instance));
+ instanceToUse = getPublicInstance(instance);
break;
default:
- ref(instance);
+ instanceToUse = instance;
+ }
+ if (typeof ref === 'function') {
+ ref(instanceToUse);
+ } else {
+ ref.value = instanceToUse;
}
}
}
@@ -188,7 +198,11 @@ export default function(
function commitDetachRef(current: Fiber) {
const currentRef = current.ref;
if (currentRef !== null) {
- currentRef(null);
+ if (typeof currentRef === 'function') {
+ currentRef(null);
+ } else {
+ currentRef.value = null;
+ }
}
}
diff --git a/packages/react/src/React.js b/packages/react/src/React.js
index d6845d8d328..89c777c38c6 100644
--- a/packages/react/src/React.js
+++ b/packages/react/src/React.js
@@ -14,6 +14,7 @@ import {
} from 'shared/ReactSymbols';
import {Component, PureComponent} from './ReactBaseClasses';
+import {createRef} from './ReactCreateRef';
import {forEach, map, count, toArray, only} from './ReactChildren';
import ReactCurrentOwner from './ReactCurrentOwner';
import {
@@ -39,6 +40,7 @@ const React = {
only,
},
+ createRef,
Component,
PureComponent,
diff --git a/packages/react/src/ReactCreateRef.js b/packages/react/src/ReactCreateRef.js
new file mode 100644
index 00000000000..8af60100e64
--- /dev/null
+++ b/packages/react/src/ReactCreateRef.js
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ * @flow
+ */
+
+import type {RefObject} from 'shared/ReactTypes';
+
+// an immutable object with a single mutable value
+export function createRef(): RefObject {
+ const refObject = {
+ value: null,
+ };
+ if (__DEV__) {
+ Object.seal(refObject);
+ }
+ return refObject;
+}
diff --git a/packages/react/src/__tests__/ReactStrictMode-test.internal.js b/packages/react/src/__tests__/ReactStrictMode-test.internal.js
index 9276dc18bde..5639e6f6f56 100644
--- a/packages/react/src/__tests__/ReactStrictMode-test.internal.js
+++ b/packages/react/src/__tests__/ReactStrictMode-test.internal.js
@@ -778,7 +778,7 @@ describe('ReactStrictMode', () => {
}).toWarnDev(
'Warning: A string ref has been found within a strict mode tree. ' +
'String refs are a source of potential bugs and should be avoided. ' +
- 'We recommend using a ref callback instead.\n\n' +
+ 'We recommend using createRef() instead.\n\n' +
' in OuterComponent (at **)\n\n' +
'Learn more about using refs safely here:\n' +
'https://fb.me/react-strict-mode-string-ref',
@@ -819,7 +819,7 @@ describe('ReactStrictMode', () => {
}).toWarnDev(
'Warning: A string ref has been found within a strict mode tree. ' +
'String refs are a source of potential bugs and should be avoided. ' +
- 'We recommend using a ref callback instead.\n\n' +
+ 'We recommend using createRef() instead.\n\n' +
' in InnerComponent (at **)\n' +
' in OuterComponent (at **)\n\n' +
'Learn more about using refs safely here:\n' +
diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js
index 9f0dbcff867..26ada3c5e84 100644
--- a/packages/shared/ReactTypes.js
+++ b/packages/shared/ReactTypes.js
@@ -97,3 +97,7 @@ export type ReactPortal = {
// TODO: figure out the API for cross-renderer implementation.
implementation: any,
};
+
+export type RefObject = {|
+ value: any,
+|};