Switch first-class refs to use functions
Closes #1373. Test Plan: jest
Ben Alpert committed
Jan 22, 2015 at 17:11 UTC
322bde6eb0077673e34865d9d4602a03ba672a68
3 files changed
+146
-164
src/core/ReactCompositeComponent.js
+39
-19
@@ -161,8 +161,6 @@ var ReactCompositeComponentMixin = assign({},
161
context
162
);
163
164
- ReactRef.attachRefs(this, this._currentElement);
165
-
164
this._context = context;
165
this._mountOrder = nextMountID++;
166
this._rootNodeID = rootID;
@@ -254,9 +252,18 @@ var ReactCompositeComponentMixin = assign({},
252
if (inst.componentDidMount) {
253
transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
254
}
255
+ transaction.getReactMountReady().enqueue(this.attachRefs, this);
256
return markup;
257
},
258
259
+ /**
260
+ * Helper to call ReactRef.attachRefs with this composite component, split out
261
+ * to avoid allocations in the transaction mount-ready queue.
262
+ */
263
+ attachRefs: function() {
264
+ ReactRef.attachRefs(this, this._currentElement);
265
+ },
266
+
267
/**
268
* Releases any resources allocated by `mountComponent`.
269
*
@@ -266,6 +273,8 @@ var ReactCompositeComponentMixin = assign({},
273
unmountComponent: function() {
274
var inst = this._instance;
275
276
+ ReactRef.detachRefs(this, this._currentElement);
277
+
278
this._compositeLifeCycleState = CompositeLifeCycle.UNMOUNTING;
279
if (inst.componentWillUnmount) {
280
inst.componentWillUnmount();
@@ -281,8 +290,6 @@ var ReactCompositeComponentMixin = assign({},
290
this._pendingCallbacks = null;
291
this._pendingElement = null;
292
284
- ReactRef.detachRefs(this, this._currentElement);
285
-
293
ReactComponent.Mixin.unmountComponent.call(this);
294
295
ReactComponentEnvironment.unmountIDFromEnvironment(this._rootNodeID);
@@ -728,15 +735,23 @@ var ReactCompositeComponentMixin = assign({},
735
nextUnmaskedContext
736
);
737
731
- // Update refs regardless of what shouldComponentUpdate returns
732
- ReactRef.updateRefs(this, prevParentElement, nextParentElement);
733
-
738
var inst = this._instance;
739
740
var prevContext = inst.context;
741
var prevProps = inst.props;
742
var nextContext = prevContext;
743
var nextProps = prevProps;
744
+
745
+ var refsChanged = ReactRef.shouldUpdateRefs(
746
+ this,
747
+ prevParentElement,
748
+ nextParentElement
749
+ );
750
+
751
+ if (refsChanged) {
752
+ ReactRef.detachRefs(this, prevParentElement);
753
+ }
754
+
755
// Distinguish between a props update versus a simple state update
756
if (prevParentElement !== nextParentElement) {
757
nextContext = this._processContext(nextParentElement._context);
@@ -774,7 +789,18 @@ var ReactCompositeComponentMixin = assign({},
789
}
790
}
791
777
- if (!shouldUpdate) {
792
+ if (shouldUpdate) {
793
+ this._pendingForceUpdate = false;
794
+ // Will set `this.props`, `this.state` and `this.context`.
795
+ this._performComponentUpdate(
796
+ nextParentElement,
797
+ nextProps,
798
+ nextState,
799
+ nextContext,
800
+ transaction,
801
+ nextUnmaskedContext
802
+ );
803
+ } else {
804
// If it's determined that a component should not update, we still want
805
// to set props and state but we shortcut the rest of the update.
806
this._currentElement = nextParentElement;
@@ -782,19 +808,12 @@ var ReactCompositeComponentMixin = assign({},
808
inst.props = nextProps;
809
inst.state = nextState;
810
inst.context = nextContext;
785
- return;
811
}
812
788
- this._pendingForceUpdate = false;
789
- // Will set `this.props`, `this.state` and `this.context`.
790
- this._performComponentUpdate(
791
- nextParentElement,
792
- nextProps,
793
- nextState,
794
- nextContext,
795
- transaction,
796
- nextUnmaskedContext
797
- );
813
+ // Update refs regardless of what shouldComponentUpdate returns
814
+ if (refsChanged) {
815
+ transaction.getReactMountReady().enqueue(this.attachRefs, this);
816
+ }
817
},
818
819
/**
@@ -819,6 +838,7 @@ var ReactCompositeComponentMixin = assign({},
838
) {
839
var inst = this._instance;
840
841
+ var prevElement = this._currentElement;
842
var prevProps = inst.props;
843
var prevState = inst.state;
844
var prevContext = inst.context;
src/core/ReactRef.js
+12
-94
@@ -14,98 +14,22 @@
14
var ReactOwner = require('ReactOwner');
15
var ReactUpdates = require('ReactUpdates');
16
17
-var accumulate = require('accumulate');
18
-var assign = require('Object.assign');
19
-var forEachAccumulated = require('forEachAccumulated');
20
-var invariant = require('invariant');
21
-
22
-function ReactRef() {
23
- this._value = null;
24
- this._successCallbacks = null;
25
- this._failureCallbacks = null;
26
-}
27
-
28
-/**
29
- * Call the enqueued success or failure callbacks for a ref, as appropriate.
30
- */
31
-function dispatchCallbacks() {
32
- /*jshint validthis:true */
33
- var successCallbacks = this._successCallbacks;
34
- var failureCallbacks = this._failureCallbacks;
35
- this._successCallbacks = null;
36
- this._failureCallbacks = null;
37
-
38
- if (this._value) {
39
- forEachAccumulated(successCallbacks, callSuccess, this);
40
- } else {
41
- forEachAccumulated(failureCallbacks, callFailure);
42
- }
43
-}
44
-
45
-/**
46
- * Call a single success callback, passing the ref's value.
47
- */
48
-function callSuccess(cb) {
49
- /*jshint validthis:true */
50
- cb(this._value);
51
-}
52
-
53
-/**
54
- * Call a single failure callback, passing no arguments.
55
- */
56
-function callFailure(cb) {
57
- cb();
58
-}
59
-
60
-assign(ReactRef.prototype, {
61
- /**
62
- * Get the value of a ref asynchronously. Accepts a success callback and an
63
- * optional failure callback. If the ref has been rendered, the success
64
- * callback will be called with the component instance; otherwise, the failure
65
- * callback will be executed.
66
- *
67
- * @param {function} success Callback in case of success
68
- * @param {?function} failure Callback in case of failure
69
- */
70
- then: function(success, failure) {
71
- invariant(
72
- typeof success === 'function',
73
- 'ReactRef.then(...): Must provide a success callback.'
74
- );
75
- if (this._successCallbacks == null) {
76
- ReactUpdates.asap(dispatchCallbacks, this);
77
- }
78
- this._successCallbacks = accumulate(this._successCallbacks, success);
79
- if (failure) {
80
- this._failureCallbacks = accumulate(this._failureCallbacks, failure);
81
- }
82
- }
83
-});
84
-
85
-function attachFirstClassRef(ref, value) {
86
- ref._value = value.getPublicInstance();
87
-}
88
-
89
-function detachFirstClassRef(ref, value) {
90
- // Check that `component` is still the current ref because we do not want to
91
- // detach the ref if another component stole it.
92
- if (ref._value === value) {
93
- ref._value = null;
94
- }
95
-}
17
+var ReactRef = {};
18
19
function attachRef(ref, component, owner) {
98
- if (ref instanceof ReactRef) {
99
- attachFirstClassRef(ref, component);
20
+ if (typeof ref === 'function') {
21
+ ref(component.getPublicInstance());
22
} else {
23
+ // Legacy ref
24
ReactOwner.addComponentAsRefTo(component, ref, owner);
25
}
26
}
27
28
function detachRef(ref, component, owner) {
106
- if (ref instanceof ReactRef) {
107
- detachFirstClassRef(ref, component);
29
+ if (typeof ref === 'function') {
30
+ ref(null);
31
} else {
32
+ // Legacy ref
33
ReactOwner.removeComponentAsRefFrom(component, ref, owner);
34
}
35
}
@@ -117,7 +41,7 @@ ReactRef.attachRefs = function(instance, element) {
41
}
42
};
43
120
-ReactRef.updateRefs = function(instance, prevElement, nextElement) {
44
+ReactRef.shouldUpdateRefs = function(instance, prevElement, nextElement) {
45
// If either the owner or a `ref` has changed, make sure the newest owner
46
// has stored a reference to `this`, and the previous owner (if different)
47
// has forgotten the reference to `this`. We use the element instead
@@ -130,16 +54,10 @@ ReactRef.updateRefs = function(instance, prevElement, nextElement) {
54
// is made. It probably belongs where the key checking and
55
// instantiateReactComponent is done.
56
133
- if (nextElement._owner !== prevElement._owner ||
134
- nextElement.ref !== prevElement.ref) {
135
- if (prevElement.ref != null) {
136
- detachRef(prevElement.ref, instance, prevElement._owner);
137
- }
138
- // Correct, even if the owner is the same, and only the ref has changed.
139
- if (nextElement.ref != null) {
140
- attachRef(nextElement.ref, instance, nextElement._owner);
141
- }
142
- }
57
+ return (
58
+ nextElement._owner !== prevElement._owner ||
59
+ nextElement.ref !== prevElement.ref
60
+ );
61
};
62
63
ReactRef.detachRefs = function(instance, element) {
src/core/__tests__/ReactComponent-test.js
+95
-51
@@ -112,50 +112,27 @@ describe('ReactComponent', function() {
112
}
113
});
114
115
- var refsResolved = 0;
116
- var refsErrored = 0;
115
+ var mounted = false;
116
var Component = React.createClass({
118
- componentWillMount: function() {
119
- this.innerRef = React.createRef();
120
- this.outerRef = React.createRef();
121
- this.unusedRef = React.createRef();
122
- },
117
render: function() {
124
- var inner = <Wrapper object={innerObj} ref={this.innerRef} />;
118
+ var inner = <Wrapper object={innerObj} ref={(c) => this.innerRef = c} />;
119
var outer = (
126
- <Wrapper object={outerObj} ref={this.outerRef}>
120
+ <Wrapper object={outerObj} ref={(c) => this.outerRef = c}>
121
{inner}
122
</Wrapper>
123
);
124
return outer;
125
},
126
componentDidMount: function() {
133
- // TODO: Currently new refs aren't available on initial render
134
- },
135
- componentDidUpdate: function() {
136
- this.innerRef.then(function(inner) {
137
- expect(inner.getObject()).toEqual(innerObj);
138
- refsResolved++;
139
- });
140
- this.outerRef.then(function(outer) {
141
- expect(outer.getObject()).toEqual(outerObj);
142
- refsResolved++;
143
- });
144
- this.unusedRef.then(function() {
145
- throw new Error("Unused ref should not be resolved");
146
- }, function() {
147
- refsErrored++;
148
- });
149
- expect(refsResolved).toBe(0);
150
- expect(refsErrored).toBe(0);
127
+ expect(this.innerRef.getObject()).toEqual(innerObj);
128
+ expect(this.outerRef.getObject()).toEqual(outerObj);
129
+ mounted = true;
130
}
131
});
132
133
var instance = <Component />;
134
instance = ReactTestUtils.renderIntoDocument(instance);
156
- instance.forceUpdate();
157
- expect(refsResolved).toBe(2);
158
- expect(refsErrored).toBe(1);
135
+ expect(mounted).toBe(true);
136
});
137
138
it('should support new-style refs with mixed-up owners', function() {
@@ -165,47 +142,114 @@ describe('ReactComponent', function() {
142
}
143
});
144
168
- var refsResolved = 0;
145
+ var mounted = false;
146
var Component = React.createClass({
170
- componentWillMount: function() {
171
- this.wrapperRef = React.createRef();
172
- this.innerRef = React.createRef();
173
- },
147
getInner: function() {
148
// (With old-style refs, it's impossible to get a ref to this div
149
// because Wrapper is the current owner when this function is called.)
177
- return <div title="inner" ref={this.innerRef} />;
150
+ return <div title="inner" ref={(c) => this.innerRef = c} />;
151
},
152
render: function() {
153
return (
154
<Wrapper
155
title="wrapper"
183
- ref={this.wrapperRef}
156
+ ref={(c) => this.wrapperRef = c}
157
getContent={this.getInner}
158
/>
159
);
160
},
161
componentDidMount: function() {
189
- // TODO: Currently new refs aren't available on initial render
190
- },
191
- componentDidUpdate: function() {
162
// Check .props.title to make sure we got the right elements back
193
- this.wrapperRef.then(function(wrapper) {
194
- expect(wrapper.props.title).toBe("wrapper");
195
- refsResolved++;
196
- });
197
- this.innerRef.then(function(inner) {
198
- expect(inner.props.title).toEqual("inner");
199
- refsResolved++;
200
- });
201
- expect(refsResolved).toBe(0);
163
+ expect(this.wrapperRef.props.title).toBe('wrapper');
164
+ expect(this.innerRef.props.title).toBe('inner');
165
+ mounted = true;
166
}
167
});
168
169
var instance = <Component />;
170
instance = ReactTestUtils.renderIntoDocument(instance);
207
- instance.forceUpdate();
208
- expect(refsResolved).toBe(2);
171
+ expect(mounted).toBe(true);
172
+ });
173
+
174
+ it('should call refs at the correct time', function() {
175
+ var log = [];
176
+
177
+ var Inner = React.createClass({
178
+ render: function() {
179
+ log.push(`inner ${this.props.id} render`);
180
+ return <div />;
181
+ },
182
+ componentDidMount: function() {
183
+ log.push(`inner ${this.props.id} componentDidMount`);
184
+ },
185
+ componentDidUpdate: function() {
186
+ log.push(`inner ${this.props.id} componentDidUpdate`);
187
+ },
188
+ componentWillUnmount: function() {
189
+ log.push(`inner ${this.props.id} componentWillUnmount`);
190
+ }
191
+ });
192
+
193
+ var Outer = React.createClass({
194
+ render: function() {
195
+ return (
196
+ <div>
197
+ <Inner id={1} ref={(c) => {
198
+ log.push(`ref 1 got ${c ? `instance ${c.props.id}` : 'null'}`);
199
+ }}/>
200
+ <Inner id={2} ref={(c) => {
201
+ log.push(`ref 2 got ${c ? `instance ${c.props.id}` : 'null'}`);
202
+ }}/>
203
+ </div>
204
+ );
205
+ },
206
+ componentDidMount: function() {
207
+ log.push('outer componentDidMount');
208
+ },
209
+ componentDidUpdate: function() {
210
+ log.push('outer componentDidUpdate');
211
+ },
212
+ componentWillUnmount: function() {
213
+ log.push('outer componentWillUnmount');
214
+ }
215
+ });
216
+
217
+ // mount, update, unmount
218
+ var el = document.createElement('div');
219
+ log.push('start mount');
220
+ React.render(<Outer />, el);
221
+ log.push('start update');
222
+ React.render(<Outer />, el);
223
+ log.push('start unmount');
224
+ React.unmountComponentAtNode(el);
225
+
226
+ expect(log).toEqual([
227
+ 'start mount',
228
+ 'inner 1 render',
229
+ 'inner 2 render',
230
+ 'inner 1 componentDidMount',
231
+ 'ref 1 got instance 1',
232
+ 'inner 2 componentDidMount',
233
+ 'ref 2 got instance 2',
234
+ 'outer componentDidMount',
235
+ 'start update',
236
+ // Previous (equivalent) refs get cleared
237
+ 'ref 1 got null',
238
+ 'inner 1 render',
239
+ 'ref 2 got null',
240
+ 'inner 2 render',
241
+ 'inner 1 componentDidUpdate',
242
+ 'ref 1 got instance 1',
243
+ 'inner 2 componentDidUpdate',
244
+ 'ref 2 got instance 2',
245
+ 'outer componentDidUpdate',
246
+ 'start unmount',
247
+ 'outer componentWillUnmount',
248
+ 'ref 1 got null',
249
+ 'inner 1 componentWillUnmount',
250
+ 'ref 2 got null',
251
+ 'inner 2 componentWillUnmount'
252
+ ]);
253
});
254
255
it('should correctly determine if a component is mounted', function() {