@samitouri / QOS-React-2 / commits / 9edc6260a7

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