Initial implementation of new-style refs
cf. #1373 This implementation can be used in any situation that refs can currently be used (and can also be used without an owner, which is a plus).
Ben Alpert committed
Nov 5, 2014 at 19:24 UTC
9edc6260a7db25f888259fddedb4aa2250f6385f
4 files changed
+236
-10
src/browser/ui/React.js
+4
@@ -28,6 +28,7 @@ var ReactMount = require('ReactMount');
28
var ReactMultiChild = require('ReactMultiChild');
29
var ReactPerf = require('ReactPerf');
30
var ReactPropTypes = require('ReactPropTypes');
31
+var ReactRef = require('ReactRef');
32
var ReactServerRendering = require('ReactServerRendering');
33
var ReactTextComponent = require('ReactTextComponent');
34
@@ -62,6 +63,9 @@ var React = {
63
createClass: ReactClass.createClass,
64
createElement: createElement,
65
createFactory: createFactory,
66
+ createRef: function() {
67
+ return new ReactRef();
68
+ },
69
constructAndRenderComponent: ReactMount.constructAndRenderComponent,
70
constructAndRenderComponentByID: ReactMount.constructAndRenderComponentByID,
71
render: render,
src/core/ReactComponent.js
+21
-10
@@ -13,6 +13,7 @@
13
14
var ReactElement = require('ReactElement');
15
var ReactOwner = require('ReactOwner');
16
+var ReactRef = require('ReactRef');
17
var ReactUpdates = require('ReactUpdates');
18
19
var assign = require('Object.assign');
@@ -56,6 +57,22 @@ var unmountIDFromEnvironment = null;
57
*/
58
var mountImageIntoNode = null;
59
60
+function attachRef(ref, component, owner) {
61
+ if (ref instanceof ReactRef) {
62
+ ReactRef.attachRef(ref, component);
63
+ } else {
64
+ ReactOwner.addComponentAsRefTo(component, ref, owner);
65
+ }
66
+}
67
+
68
+function detachRef(ref, component, owner) {
69
+ if (ref instanceof ReactRef) {
70
+ ReactRef.detachRef(ref, component);
71
+ } else {
72
+ ReactOwner.removeComponentAsRefFrom(component, ref, owner);
73
+ }
74
+}
75
+
76
/**
77
* Components are the basic units of composition in React.
78
*
@@ -255,7 +272,7 @@ var ReactComponent = {
272
var ref = this._currentElement.ref;
273
if (ref != null) {
274
var owner = this._currentElement._owner;
258
- ReactOwner.addComponentAsRefTo(this, ref, owner);
275
+ attachRef(ref, this, owner);
276
}
277
this._rootNodeID = rootID;
278
this._lifeCycleState = ComponentLifeCycle.MOUNTED;
@@ -280,7 +297,7 @@ var ReactComponent = {
297
);
298
var ref = this._currentElement.ref;
299
if (ref != null) {
283
- ReactOwner.removeComponentAsRefFrom(this, ref, this._owner);
300
+ detachRef(ref, this, this._owner);
301
}
302
unmountIDFromEnvironment(this._rootNodeID);
303
this._rootNodeID = null;
@@ -350,17 +367,11 @@ var ReactComponent = {
367
if (nextElement._owner !== prevElement._owner ||
368
nextElement.ref !== prevElement.ref) {
369
if (prevElement.ref != null) {
353
- ReactOwner.removeComponentAsRefFrom(
354
- this, prevElement.ref, prevElement._owner
355
- );
370
+ detachRef(prevElement.ref, this, prevElement._owner);
371
}
372
// Correct, even if the owner is the same, and only the ref has changed.
373
if (nextElement.ref != null) {
359
- ReactOwner.addComponentAsRefTo(
360
- this,
361
- nextElement.ref,
362
- nextElement._owner
363
- );
374
+ attachRef(nextElement.ref, this, nextElement._owner);
375
}
376
}
377
},
src/core/ReactRef.js
new
+103
@@ -0,0 +1,103 @@
1
+/**
2
+ * Copyright 2013-2014 Facebook, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ *
16
+ * @providesModule ReactRef
17
+ */
18
+
19
+"use strict";
20
+
21
+var ReactUpdates = require('ReactUpdates');
22
+
23
+var accumulate = require('accumulate');
24
+var assign = require('Object.assign');
25
+var forEachAccumulated = require('forEachAccumulated');
26
+var invariant = require('invariant');
27
+
28
+function ReactRef() {
29
+ this._value = null;
30
+ this._successCallbacks = null;
31
+ this._failureCallbacks = null;
32
+}
33
+
34
+/**
35
+ * Call the enqueued success or failure callbacks for a ref, as appropriate.
36
+ */
37
+function dispatchCallbacks() {
38
+ /*jshint validthis:true */
39
+ var successCallbacks = this._successCallbacks;
40
+ var failureCallbacks = this._failureCallbacks;
41
+ this._successCallbacks = null;
42
+ this._failureCallbacks = null;
43
+
44
+ if (this._value) {
45
+ forEachAccumulated(successCallbacks, callSuccess, this);
46
+ } else {
47
+ forEachAccumulated(failureCallbacks, callFailure);
48
+ }
49
+}
50
+
51
+/**
52
+ * Call a single success callback, passing the ref's value.
53
+ */
54
+function callSuccess(cb) {
55
+ /*jshint validthis:true */
56
+ cb(this._value);
57
+}
58
+
59
+/**
60
+ * Call a single failure callback, passing no arguments.
61
+ */
62
+function callFailure(cb) {
63
+ cb();
64
+}
65
+
66
+assign(ReactRef.prototype, {
67
+ /**
68
+ * Get the value of a ref asynchronously. Accepts a success callback and an
69
+ * optional failure callback. If the ref has been rendered, the success
70
+ * callback will be called with the component instance; otherwise, the failure
71
+ * callback will be executed.
72
+ *
73
+ * @param {function} success Callback in case of success
74
+ * @param {?function} failure Callback in case of failure
75
+ */
76
+ then: function(success, failure) {
77
+ invariant(
78
+ typeof success === 'function',
79
+ 'ReactRef.then(...): Must provide a success callback.'
80
+ );
81
+ if (this._successCallbacks == null) {
82
+ ReactUpdates.asap(dispatchCallbacks, this);
83
+ }
84
+ this._successCallbacks = accumulate(this._successCallbacks, success);
85
+ if (failure) {
86
+ this._failureCallbacks = accumulate(this._failureCallbacks, failure);
87
+ }
88
+ }
89
+});
90
+
91
+ReactRef.attachRef = function(ref, value) {
92
+ ref._value = value;
93
+};
94
+
95
+ReactRef.detachRef = function(ref, value) {
96
+ // Check that `component` is still the current ref because we do not want to
97
+ // detach the ref if another component stole it.
98
+ if (ref._value === value) {
99
+ ref._value = null;
100
+ }
101
+};
102
+
103
+module.exports = ReactRef;
src/core/__tests__/ReactComponent-test.js
+108
@@ -118,6 +118,114 @@ describe('ReactComponent', function() {
118
instance = ReactTestUtils.renderIntoDocument(instance);
119
});
120
121
+ it('should support new-style refs', function() {
122
+ var innerObj = {}, outerObj = {};
123
+
124
+ var Wrapper = React.createClass({
125
+ getObject: function() {
126
+ return this.props.object;
127
+ },
128
+ render: function() {
129
+ return <div>{this.props.children}</div>;
130
+ }
131
+ });
132
+
133
+ var refsResolved = 0;
134
+ var refsErrored = 0;
135
+ var Component = React.createClass({
136
+ componentWillMount: function() {
137
+ this.innerRef = React.createRef();
138
+ this.outerRef = React.createRef();
139
+ this.unusedRef = React.createRef();
140
+ },
141
+ render: function() {
142
+ var inner = <Wrapper object={innerObj} ref={this.innerRef} />;
143
+ var outer = (
144
+ <Wrapper object={outerObj} ref={this.outerRef}>
145
+ {inner}
146
+ </Wrapper>
147
+ );
148
+ return outer;
149
+ },
150
+ componentDidMount: function() {
151
+ // TODO: Currently new refs aren't available on initial render
152
+ },
153
+ componentDidUpdate: function() {
154
+ this.innerRef.then(function(inner) {
155
+ expect(inner.getObject()).toEqual(innerObj);
156
+ refsResolved++;
157
+ });
158
+ this.outerRef.then(function(outer) {
159
+ expect(outer.getObject()).toEqual(outerObj);
160
+ refsResolved++;
161
+ });
162
+ this.unusedRef.then(function() {
163
+ throw new Error("Unused ref should not be resolved");
164
+ }, function() {
165
+ refsErrored++;
166
+ });
167
+ expect(refsResolved).toBe(0);
168
+ expect(refsErrored).toBe(0);
169
+ }
170
+ });
171
+
172
+ var instance = <Component />;
173
+ instance = ReactTestUtils.renderIntoDocument(instance);
174
+ instance.forceUpdate();
175
+ expect(refsResolved).toBe(2);
176
+ expect(refsErrored).toBe(1);
177
+ });
178
+
179
+ it('should support new-style refs with mixed-up owners', function() {
180
+ var Wrapper = React.createClass({
181
+ render: function() {
182
+ return this.props.getContent();
183
+ }
184
+ });
185
+
186
+ var refsResolved = 0;
187
+ var Component = React.createClass({
188
+ componentWillMount: function() {
189
+ this.wrapperRef = React.createRef();
190
+ this.innerRef = React.createRef();
191
+ },
192
+ getInner: function() {
193
+ // (With old-style refs, it's impossible to get a ref to this div
194
+ // because Wrapper is the current owner when this function is called.)
195
+ return <div title="inner" ref={this.innerRef} />;
196
+ },
197
+ render: function() {
198
+ return (
199
+ <Wrapper
200
+ title="wrapper"
201
+ ref={this.wrapperRef}
202
+ getContent={this.getInner}
203
+ />
204
+ );
205
+ },
206
+ componentDidMount: function() {
207
+ // TODO: Currently new refs aren't available on initial render
208
+ },
209
+ componentDidUpdate: function() {
210
+ // Check .props.title to make sure we got the right elements back
211
+ this.wrapperRef.then(function(wrapper) {
212
+ expect(wrapper.props.title).toBe("wrapper");
213
+ refsResolved++;
214
+ });
215
+ this.innerRef.then(function(inner) {
216
+ expect(inner.props.title).toEqual("inner");
217
+ refsResolved++;
218
+ });
219
+ expect(refsResolved).toBe(0);
220
+ }
221
+ });
222
+
223
+ var instance = <Component />;
224
+ instance = ReactTestUtils.renderIntoDocument(instance);
225
+ instance.forceUpdate();
226
+ expect(refsResolved).toBe(2);
227
+ });
228
+
229
it('should correctly determine if a component is mounted', function() {
230
var Component = React.createClass({
231
componentWillMount: function() {