diff --git a/packages/react-dom/src/__tests__/ReactComponent-test.js b/packages/react-dom/src/__tests__/ReactComponent-test.js index 0a63915429c2a..c892b3debeccd 100644 --- a/packages/react-dom/src/__tests__/ReactComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactComponent-test.js @@ -145,7 +145,7 @@ describe('ReactComponent', () => { ReactTestUtils.renderIntoDocument(} />); }); - it('should support new-style refs', () => { + it('should support callback-style refs', () => { var innerObj = {}; var outerObj = {}; @@ -185,6 +185,49 @@ describe('ReactComponent', () => { expect(mounted).toBe(true); }); + it('should support object-style refs', () => { + var innerObj = {}; + var outerObj = {}; + + class Wrapper extends React.Component { + getObject = () => { + return this.props.object; + }; + + render() { + return
{this.props.children}
; + } + } + + var mounted = false; + + class Component extends React.Component { + constructor() { + super(); + this.innerRef = React.createRef(); + this.outerRef = React.createRef(); + } + render() { + var inner = ; + var 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 01b2b33f3431b..a0835d2256845 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(); + + var 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', () => { var container = document.createElement('div'); ReactDOM.render( diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 8f88719e3b9e3..abec1ae39856d 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -123,7 +123,11 @@ function getIteratorFn(maybeIterable: ?any): ?() => ?Iterator<*> { function coerceRef(current: Fiber | null, element: ReactElement) { let mixedRef = element.ref; - if (mixedRef !== null && typeof mixedRef !== 'function') { + if ( + mixedRef !== null && + typeof mixedRef !== 'function' && + typeof mixedRef !== 'object' + ) { if (element._owner) { const owner: ?Fiber = (element._owner: any); let inst; diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index af2ce6478ea90..5cd4226c5cba2 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -8,6 +8,7 @@ import type {ReactElement, Source} from 'shared/ReactElementType'; import type { + RefObject, ReactCall, ReactFragment, ReactPortal, @@ -95,7 +96,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 68b990c1d23be..5d148c0d1bded 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -73,18 +73,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; } } } @@ -162,12 +166,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; } } } @@ -175,7 +185,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 f25181ea80394..4cf9b648b3eb2 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -9,6 +9,7 @@ import assign from 'object-assign'; import ReactVersion from 'shared/ReactVersion'; import {enableReactFragment} from 'shared/ReactFeatureFlags'; +import {createRef} from './ReactCreateRef'; import {Component, PureComponent, AsyncComponent} from './ReactBaseClasses'; import {forEach, map, count, toArray, only} from './ReactChildren'; import ReactCurrentOwner from './ReactCurrentOwner'; @@ -31,7 +32,7 @@ const REACT_FRAGMENT_TYPE = Symbol.for('react.fragment')) || 0xeacb; -var React = { +const React = { Children: { map, forEach, @@ -40,6 +41,7 @@ var React = { only, }, + createRef, Component, PureComponent, unstable_AsyncComponent: AsyncComponent, diff --git a/packages/react/src/ReactCreateRef.js b/packages/react/src/ReactCreateRef.js new file mode 100644 index 0000000000000..8af60100e64d7 --- /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/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 4efa9a3851dd6..1da1d4e666a1a 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -45,3 +45,7 @@ export type ReactPortal = { // TODO: figure out the API for cross-renderer implementation. implementation: any, }; + +export type RefObject = {| + value: any, +|};