Adds createRef() as per RFC (#12162)
* Adds createRef() as per RFC
Dominic Gannaway committed
Feb 6, 2018 at 20:19 UTC
8dc8f88d5ae9fb96934ba43e3842b5dcf4074afd
9 files changed
+146
-21
packages/react-dom/src/__tests__/ReactComponent-test.js
+44
-1
@@ -162,7 +162,7 @@ describe('ReactComponent', () => {
162
ReactTestUtils.renderIntoDocument(<Parent child={<span />} />);
163
});
164
165
- it('should support new-style refs', () => {
165
+ it('should support callback-style refs', () => {
166
const innerObj = {};
167
const outerObj = {};
168
@@ -202,6 +202,49 @@ describe('ReactComponent', () => {
202
expect(mounted).toBe(true);
203
});
204
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
+
248
it('should support new-style refs with mixed-up owners', () => {
249
class Wrapper extends React.Component {
250
getTitle = () => {
packages/react-dom/src/__tests__/ReactErrorBoundaries-test.js
+39
-1
@@ -937,7 +937,7 @@ describe('ReactErrorBoundaries', () => {
937
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
938
});
939
940
- it('resets refs if mounting aborts', () => {
940
+ it('resets callback refs if mounting aborts', () => {
941
function childRef(x) {
942
log.push('Child ref is set to ' + x);
943
}
@@ -981,6 +981,44 @@ describe('ReactErrorBoundaries', () => {
981
]);
982
});
983
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
+
1022
it('successfully mounts if no error occurs', () => {
1023
const container = document.createElement('div');
1024
ReactDOM.render(
packages/react-reconciler/src/ReactChildFiber.js
+6
-2
@@ -104,7 +104,11 @@ function coerceRef(
104
element: ReactElement,
105
) {
106
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
+ ) {
112
if (__DEV__) {
113
if (returnFiber.mode & StrictMode) {
114
const componentName = getComponentName(returnFiber) || 'Component';
@@ -113,7 +117,7 @@ function coerceRef(
117
false,
118
'A string ref, "%s", has been found within a strict mode tree. ' +
119
'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.' +
121
'\n%s' +
122
'\n\nLearn more about using refs safely here:' +
123
'\nhttps://fb.me/react-strict-mode-string-ref',
packages/react-reconciler/src/ReactFiber.js
+2
-2
@@ -7,7 +7,7 @@
7
*/
8
9
import type {ReactElement, Source} from 'shared/ReactElementType';
10
-import type {ReactPortal} from 'shared/ReactTypes';
10
+import type {ReactPortal, RefObject} from 'shared/ReactTypes';
11
import type {TypeOfWork} from 'shared/ReactTypeOfWork';
12
import type {TypeOfMode} from './ReactTypeOfMode';
13
import type {TypeOfSideEffect} from 'shared/ReactTypeOfSideEffect';
@@ -107,7 +107,7 @@ export type Fiber = {|
107
108
// The ref last used to attach this node.
109
// 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,
111
112
// Input is the data coming into process this fiber. Arguments. Props.
113
pendingProps: any, // This type will be more specific once we overload the tag.
packages/react-reconciler/src/ReactFiberCommitWork.js
+27
-13
@@ -77,18 +77,22 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
77
function safelyDetachRef(current: Fiber) {
78
const ref = current.ref;
79
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
+ }
93
}
94
} else {
87
- try {
88
- ref(null);
89
- } catch (refError) {
90
- captureError(current, refError);
91
- }
95
+ ref.value = null;
96
}
97
}
98
}
@@ -175,12 +179,18 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
179
const ref = finishedWork.ref;
180
if (ref !== null) {
181
const instance = finishedWork.stateNode;
182
+ let instanceToUse;
183
switch (finishedWork.tag) {
184
case HostComponent:
180
- ref(getPublicInstance(instance));
185
+ instanceToUse = getPublicInstance(instance);
186
break;
187
default:
183
- ref(instance);
188
+ instanceToUse = instance;
189
+ }
190
+ if (typeof ref === 'function') {
191
+ ref(instanceToUse);
192
+ } else {
193
+ ref.value = instanceToUse;
194
}
195
}
196
}
@@ -188,7 +198,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
198
function commitDetachRef(current: Fiber) {
199
const currentRef = current.ref;
200
if (currentRef !== null) {
191
- currentRef(null);
201
+ if (typeof currentRef === 'function') {
202
+ currentRef(null);
203
+ } else {
204
+ currentRef.value = null;
205
+ }
206
}
207
}
208
packages/react/src/React.js
+2
@@ -14,6 +14,7 @@ import {
14
} from 'shared/ReactSymbols';
15
16
import {Component, PureComponent} from './ReactBaseClasses';
17
+import {createRef} from './ReactCreateRef';
18
import {forEach, map, count, toArray, only} from './ReactChildren';
19
import ReactCurrentOwner from './ReactCurrentOwner';
20
import {
@@ -39,6 +40,7 @@ const React = {
40
only,
41
},
42
43
+ createRef,
44
Component,
45
PureComponent,
46
packages/react/src/ReactCreateRef.js
new
+20
@@ -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
@@ -778,7 +778,7 @@ describe('ReactStrictMode', () => {
778
}).toWarnDev(
779
'Warning: A string ref, "somestring", has been found within a strict mode tree. ' +
780
'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' +
782
' in OuterComponent (at **)\n\n' +
783
'Learn more about using refs safely here:\n' +
784
'https://fb.me/react-strict-mode-string-ref',
@@ -819,7 +819,7 @@ describe('ReactStrictMode', () => {
819
}).toWarnDev(
820
'Warning: A string ref, "somestring", has been found within a strict mode tree. ' +
821
'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' +
823
' in InnerComponent (at **)\n' +
824
' in OuterComponent (at **)\n\n' +
825
'Learn more about using refs safely here:\n' +
packages/shared/ReactTypes.js
+4
@@ -97,3 +97,7 @@ export type ReactPortal = {
97
// TODO: figure out the API for cross-renderer implementation.
98
implementation: any,
99
};
100
+
101
+export type RefObject = {|
102
+ value: any,
103
+|};