Skip to content

Commit

Permalink
Initial implementation of new-style refs
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
sophiebits committed Nov 6, 2014
1 parent dca7ffb commit 9edc626
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 10 deletions.
4 changes: 4 additions & 0 deletions src/browser/ui/React.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var ReactMount = require('ReactMount');
var ReactMultiChild = require('ReactMultiChild');
var ReactPerf = require('ReactPerf');
var ReactPropTypes = require('ReactPropTypes');
var ReactRef = require('ReactRef');
var ReactServerRendering = require('ReactServerRendering');
var ReactTextComponent = require('ReactTextComponent');

Expand Down Expand Up @@ -62,6 +63,9 @@ var React = {
createClass: ReactClass.createClass,
createElement: createElement,
createFactory: createFactory,
createRef: function() {
return new ReactRef();
},
constructAndRenderComponent: ReactMount.constructAndRenderComponent,
constructAndRenderComponentByID: ReactMount.constructAndRenderComponentByID,
render: render,
Expand Down
31 changes: 21 additions & 10 deletions src/core/ReactComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

var ReactElement = require('ReactElement');
var ReactOwner = require('ReactOwner');
var ReactRef = require('ReactRef');
var ReactUpdates = require('ReactUpdates');

var assign = require('Object.assign');
Expand Down Expand Up @@ -56,6 +57,22 @@ var unmountIDFromEnvironment = null;
*/
var mountImageIntoNode = null;

function attachRef(ref, component, owner) {
if (ref instanceof ReactRef) {
ReactRef.attachRef(ref, component);
} else {
ReactOwner.addComponentAsRefTo(component, ref, owner);
}
}

function detachRef(ref, component, owner) {
if (ref instanceof ReactRef) {
ReactRef.detachRef(ref, component);
} else {
ReactOwner.removeComponentAsRefFrom(component, ref, owner);
}
}

/**
* Components are the basic units of composition in React.
*
Expand Down Expand Up @@ -255,7 +272,7 @@ var ReactComponent = {
var ref = this._currentElement.ref;
if (ref != null) {
var owner = this._currentElement._owner;
ReactOwner.addComponentAsRefTo(this, ref, owner);
attachRef(ref, this, owner);
}
this._rootNodeID = rootID;
this._lifeCycleState = ComponentLifeCycle.MOUNTED;
Expand All @@ -280,7 +297,7 @@ var ReactComponent = {
);
var ref = this._currentElement.ref;
if (ref != null) {
ReactOwner.removeComponentAsRefFrom(this, ref, this._owner);
detachRef(ref, this, this._owner);
}
unmountIDFromEnvironment(this._rootNodeID);
this._rootNodeID = null;
Expand Down Expand Up @@ -350,17 +367,11 @@ var ReactComponent = {
if (nextElement._owner !== prevElement._owner ||
nextElement.ref !== prevElement.ref) {
if (prevElement.ref != null) {
ReactOwner.removeComponentAsRefFrom(
this, prevElement.ref, prevElement._owner
);
detachRef(prevElement.ref, this, prevElement._owner);
}
// Correct, even if the owner is the same, and only the ref has changed.
if (nextElement.ref != null) {
ReactOwner.addComponentAsRefTo(
this,
nextElement.ref,
nextElement._owner
);
attachRef(nextElement.ref, this, nextElement._owner);
}
}
},
Expand Down
103 changes: 103 additions & 0 deletions src/core/ReactRef.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Copyright 2013-2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @providesModule ReactRef
*/

"use strict";

var ReactUpdates = require('ReactUpdates');

var accumulate = require('accumulate');
var assign = require('Object.assign');
var forEachAccumulated = require('forEachAccumulated');
var invariant = require('invariant');

function ReactRef() {
this._value = null;
this._successCallbacks = null;
this._failureCallbacks = null;
}

/**
* Call the enqueued success or failure callbacks for a ref, as appropriate.
*/
function dispatchCallbacks() {
/*jshint validthis:true */
var successCallbacks = this._successCallbacks;
var failureCallbacks = this._failureCallbacks;
this._successCallbacks = null;
this._failureCallbacks = null;

if (this._value) {
forEachAccumulated(successCallbacks, callSuccess, this);
} else {
forEachAccumulated(failureCallbacks, callFailure);
}
}

/**
* Call a single success callback, passing the ref's value.
*/
function callSuccess(cb) {
/*jshint validthis:true */
cb(this._value);
}

/**
* Call a single failure callback, passing no arguments.
*/
function callFailure(cb) {
cb();
}

assign(ReactRef.prototype, {
/**
* Get the value of a ref asynchronously. Accepts a success callback and an
* optional failure callback. If the ref has been rendered, the success
* callback will be called with the component instance; otherwise, the failure
* callback will be executed.
*
* @param {function} success Callback in case of success
* @param {?function} failure Callback in case of failure
*/
then: function(success, failure) {
invariant(
typeof success === 'function',
'ReactRef.then(...): Must provide a success callback.'
);
if (this._successCallbacks == null) {
ReactUpdates.asap(dispatchCallbacks, this);
}
this._successCallbacks = accumulate(this._successCallbacks, success);
if (failure) {
this._failureCallbacks = accumulate(this._failureCallbacks, failure);
}
}
});

ReactRef.attachRef = function(ref, value) {
ref._value = value;
};

ReactRef.detachRef = function(ref, value) {
// Check that `component` is still the current ref because we do not want to
// detach the ref if another component stole it.
if (ref._value === value) {
ref._value = null;
}
};

module.exports = ReactRef;
108 changes: 108 additions & 0 deletions src/core/__tests__/ReactComponent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,114 @@ describe('ReactComponent', function() {
instance = ReactTestUtils.renderIntoDocument(instance);
});

it('should support new-style refs', function() {
var innerObj = {}, outerObj = {};

var Wrapper = React.createClass({
getObject: function() {
return this.props.object;
},
render: function() {
return <div>{this.props.children}</div>;
}
});

var refsResolved = 0;
var refsErrored = 0;
var Component = React.createClass({
componentWillMount: function() {
this.innerRef = React.createRef();
this.outerRef = React.createRef();
this.unusedRef = React.createRef();
},
render: function() {
var inner = <Wrapper object={innerObj} ref={this.innerRef} />;
var outer = (
<Wrapper object={outerObj} ref={this.outerRef}>
{inner}
</Wrapper>
);
return outer;
},
componentDidMount: function() {
// TODO: Currently new refs aren't available on initial render
},
componentDidUpdate: function() {
this.innerRef.then(function(inner) {
expect(inner.getObject()).toEqual(innerObj);
refsResolved++;
});
this.outerRef.then(function(outer) {
expect(outer.getObject()).toEqual(outerObj);
refsResolved++;
});
this.unusedRef.then(function() {
throw new Error("Unused ref should not be resolved");
}, function() {
refsErrored++;
});
expect(refsResolved).toBe(0);
expect(refsErrored).toBe(0);
}
});

var instance = <Component />;
instance = ReactTestUtils.renderIntoDocument(instance);
instance.forceUpdate();
expect(refsResolved).toBe(2);
expect(refsErrored).toBe(1);
});

it('should support new-style refs with mixed-up owners', function() {
var Wrapper = React.createClass({
render: function() {
return this.props.getContent();
}
});

var refsResolved = 0;
var Component = React.createClass({
componentWillMount: function() {
this.wrapperRef = React.createRef();
this.innerRef = React.createRef();
},
getInner: function() {
// (With old-style refs, it's impossible to get a ref to this div
// because Wrapper is the current owner when this function is called.)
return <div title="inner" ref={this.innerRef} />;
},
render: function() {
return (
<Wrapper
title="wrapper"
ref={this.wrapperRef}
getContent={this.getInner}
/>
);
},
componentDidMount: function() {
// TODO: Currently new refs aren't available on initial render
},
componentDidUpdate: function() {
// Check .props.title to make sure we got the right elements back
this.wrapperRef.then(function(wrapper) {
expect(wrapper.props.title).toBe("wrapper");
refsResolved++;
});
this.innerRef.then(function(inner) {
expect(inner.props.title).toEqual("inner");
refsResolved++;
});
expect(refsResolved).toBe(0);
}
});

var instance = <Component />;
instance = ReactTestUtils.renderIntoDocument(instance);
instance.forceUpdate();
expect(refsResolved).toBe(2);
});

it('should correctly determine if a component is mounted', function() {
var Component = React.createClass({
componentWillMount: function() {
Expand Down

0 comments on commit 9edc626

Please sign in to comment.