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, +|};