@samitouri / QOS-React-2 / commits / 322bde6eb0

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() {