Skip to content

Commit 8dc8f88

Browse files
authored
Adds createRef() as per RFC (#12162)
* Adds createRef() as per RFC
1 parent 3d8f465 commit 8dc8f88

File tree

9 files changed

+146
-21
lines changed

9 files changed

+146
-21
lines changed

packages/react-dom/src/__tests__/ReactComponent-test.js

+44-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ describe('ReactComponent', () => {
162162
ReactTestUtils.renderIntoDocument(<Parent child={<span />} />);
163163
});
164164

165-
it('should support new-style refs', () => {
165+
it('should support callback-style refs', () => {
166166
const innerObj = {};
167167
const outerObj = {};
168168

@@ -202,6 +202,49 @@ describe('ReactComponent', () => {
202202
expect(mounted).toBe(true);
203203
});
204204

205+
it('should support object-style refs', () => {
206+
const innerObj = {};
207+
const outerObj = {};
208+
209+
class Wrapper extends React.Component {
210+
getObject = () => {
211+
return this.props.object;
212+
};
213+
214+
render() {
215+
return <div>{this.props.children}</div>;
216+
}
217+
}
218+
219+
let mounted = false;
220+
221+
class Component extends React.Component {
222+
constructor() {
223+
super();
224+
this.innerRef = React.createRef();
225+
this.outerRef = React.createRef();
226+
}
227+
render() {
228+
const inner = <Wrapper object={innerObj} ref={this.innerRef} />;
229+
const outer = (
230+
<Wrapper object={outerObj} ref={this.outerRef}>
231+
{inner}
232+
</Wrapper>
233+
);
234+
return outer;
235+
}
236+
237+
componentDidMount() {
238+
expect(this.innerRef.value.getObject()).toEqual(innerObj);
239+
expect(this.outerRef.value.getObject()).toEqual(outerObj);
240+
mounted = true;
241+
}
242+
}
243+
244+
ReactTestUtils.renderIntoDocument(<Component />);
245+
expect(mounted).toBe(true);
246+
});
247+
205248
it('should support new-style refs with mixed-up owners', () => {
206249
class Wrapper extends React.Component {
207250
getTitle = () => {

packages/react-dom/src/__tests__/ReactErrorBoundaries-test.js

+39-1
Original file line numberDiff line numberDiff line change
@@ -937,7 +937,7 @@ describe('ReactErrorBoundaries', () => {
937937
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
938938
});
939939

940-
it('resets refs if mounting aborts', () => {
940+
it('resets callback refs if mounting aborts', () => {
941941
function childRef(x) {
942942
log.push('Child ref is set to ' + x);
943943
}
@@ -981,6 +981,44 @@ describe('ReactErrorBoundaries', () => {
981981
]);
982982
});
983983

984+
it('resets object refs if mounting aborts', () => {
985+
let childRef = React.createRef();
986+
let errorMessageRef = React.createRef();
987+
988+
const container = document.createElement('div');
989+
ReactDOM.render(
990+
<ErrorBoundary errorMessageRef={errorMessageRef}>
991+
<div ref={childRef} />
992+
<BrokenRender />
993+
</ErrorBoundary>,
994+
container,
995+
);
996+
expect(container.textContent).toBe('Caught an error: Hello.');
997+
expect(log).toEqual([
998+
'ErrorBoundary constructor',
999+
'ErrorBoundary componentWillMount',
1000+
'ErrorBoundary render success',
1001+
'BrokenRender constructor',
1002+
'BrokenRender componentWillMount',
1003+
'BrokenRender render [!]',
1004+
// Handle error:
1005+
// Finish mounting with null children
1006+
'ErrorBoundary componentDidMount',
1007+
// Handle the error
1008+
'ErrorBoundary componentDidCatch',
1009+
// Render the error message
1010+
'ErrorBoundary componentWillUpdate',
1011+
'ErrorBoundary render error',
1012+
'ErrorBoundary componentDidUpdate',
1013+
]);
1014+
expect(errorMessageRef.value.toString()).toEqual('[object HTMLDivElement]');
1015+
1016+
log.length = 0;
1017+
ReactDOM.unmountComponentAtNode(container);
1018+
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1019+
expect(errorMessageRef.value).toEqual(null);
1020+
});
1021+
9841022
it('successfully mounts if no error occurs', () => {
9851023
const container = document.createElement('div');
9861024
ReactDOM.render(

packages/react-reconciler/src/ReactChildFiber.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,11 @@ function coerceRef(
104104
element: ReactElement,
105105
) {
106106
let mixedRef = element.ref;
107-
if (mixedRef !== null && typeof mixedRef !== 'function') {
107+
if (
108+
mixedRef !== null &&
109+
typeof mixedRef !== 'function' &&
110+
typeof mixedRef !== 'object'
111+
) {
108112
if (__DEV__) {
109113
if (returnFiber.mode & StrictMode) {
110114
const componentName = getComponentName(returnFiber) || 'Component';
@@ -113,7 +117,7 @@ function coerceRef(
113117
false,
114118
'A string ref, "%s", has been found within a strict mode tree. ' +
115119
'String refs are a source of potential bugs and should be avoided. ' +
116-
'We recommend using a ref callback instead.' +
120+
'We recommend using createRef() instead.' +
117121
'\n%s' +
118122
'\n\nLearn more about using refs safely here:' +
119123
'\nhttps://fb.me/react-strict-mode-string-ref',

packages/react-reconciler/src/ReactFiber.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import type {ReactElement, Source} from 'shared/ReactElementType';
10-
import type {ReactPortal} from 'shared/ReactTypes';
10+
import type {ReactPortal, RefObject} from 'shared/ReactTypes';
1111
import type {TypeOfWork} from 'shared/ReactTypeOfWork';
1212
import type {TypeOfMode} from './ReactTypeOfMode';
1313
import type {TypeOfSideEffect} from 'shared/ReactTypeOfSideEffect';
@@ -107,7 +107,7 @@ export type Fiber = {|
107107

108108
// The ref last used to attach this node.
109109
// I'll avoid adding an owner field for prod and model that as functions.
110-
ref: null | (((handle: mixed) => void) & {_stringRef: ?string}),
110+
ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,
111111

112112
// Input is the data coming into process this fiber. Arguments. Props.
113113
pendingProps: any, // This type will be more specific once we overload the tag.

packages/react-reconciler/src/ReactFiberCommitWork.js

+27-13
Original file line numberDiff line numberDiff line change
@@ -77,18 +77,22 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
7777
function safelyDetachRef(current: Fiber) {
7878
const ref = current.ref;
7979
if (ref !== null) {
80-
if (__DEV__) {
81-
invokeGuardedCallback(null, ref, null, null);
82-
if (hasCaughtError()) {
83-
const refError = clearCaughtError();
84-
captureError(current, refError);
80+
if (typeof ref === 'function') {
81+
if (__DEV__) {
82+
invokeGuardedCallback(null, ref, null, null);
83+
if (hasCaughtError()) {
84+
const refError = clearCaughtError();
85+
captureError(current, refError);
86+
}
87+
} else {
88+
try {
89+
ref(null);
90+
} catch (refError) {
91+
captureError(current, refError);
92+
}
8593
}
8694
} else {
87-
try {
88-
ref(null);
89-
} catch (refError) {
90-
captureError(current, refError);
91-
}
95+
ref.value = null;
9296
}
9397
}
9498
}
@@ -175,20 +179,30 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
175179
const ref = finishedWork.ref;
176180
if (ref !== null) {
177181
const instance = finishedWork.stateNode;
182+
let instanceToUse;
178183
switch (finishedWork.tag) {
179184
case HostComponent:
180-
ref(getPublicInstance(instance));
185+
instanceToUse = getPublicInstance(instance);
181186
break;
182187
default:
183-
ref(instance);
188+
instanceToUse = instance;
189+
}
190+
if (typeof ref === 'function') {
191+
ref(instanceToUse);
192+
} else {
193+
ref.value = instanceToUse;
184194
}
185195
}
186196
}
187197

188198
function commitDetachRef(current: Fiber) {
189199
const currentRef = current.ref;
190200
if (currentRef !== null) {
191-
currentRef(null);
201+
if (typeof currentRef === 'function') {
202+
currentRef(null);
203+
} else {
204+
currentRef.value = null;
205+
}
192206
}
193207
}
194208

packages/react/src/React.js

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from 'shared/ReactSymbols';
1515

1616
import {Component, PureComponent} from './ReactBaseClasses';
17+
import {createRef} from './ReactCreateRef';
1718
import {forEach, map, count, toArray, only} from './ReactChildren';
1819
import ReactCurrentOwner from './ReactCurrentOwner';
1920
import {
@@ -39,6 +40,7 @@ const React = {
3940
only,
4041
},
4142

43+
createRef,
4244
Component,
4345
PureComponent,
4446

packages/react/src/ReactCreateRef.js

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
* @flow
7+
*/
8+
9+
import type {RefObject} from 'shared/ReactTypes';
10+
11+
// an immutable object with a single mutable value
12+
export function createRef(): RefObject {
13+
const refObject = {
14+
value: null,
15+
};
16+
if (__DEV__) {
17+
Object.seal(refObject);
18+
}
19+
return refObject;
20+
}

packages/react/src/__tests__/ReactStrictMode-test.internal.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -778,7 +778,7 @@ describe('ReactStrictMode', () => {
778778
}).toWarnDev(
779779
'Warning: A string ref, "somestring", has been found within a strict mode tree. ' +
780780
'String refs are a source of potential bugs and should be avoided. ' +
781-
'We recommend using a ref callback instead.\n\n' +
781+
'We recommend using createRef() instead.\n\n' +
782782
' in OuterComponent (at **)\n\n' +
783783
'Learn more about using refs safely here:\n' +
784784
'https://fb.me/react-strict-mode-string-ref',
@@ -819,7 +819,7 @@ describe('ReactStrictMode', () => {
819819
}).toWarnDev(
820820
'Warning: A string ref, "somestring", has been found within a strict mode tree. ' +
821821
'String refs are a source of potential bugs and should be avoided. ' +
822-
'We recommend using a ref callback instead.\n\n' +
822+
'We recommend using createRef() instead.\n\n' +
823823
' in InnerComponent (at **)\n' +
824824
' in OuterComponent (at **)\n\n' +
825825
'Learn more about using refs safely here:\n' +

packages/shared/ReactTypes.js

+4
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,7 @@ export type ReactPortal = {
9797
// TODO: figure out the API for cross-renderer implementation.
9898
implementation: any,
9999
};
100+
101+
export type RefObject = {|
102+
value: any,
103+
|};

0 commit comments

Comments
 (0)