Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation of new-style refs #1554

Merged
merged 1 commit into from
Nov 6, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sebmarkbage's property descriptor API idea will make declaring refs really elegant. For now, we have this componentWillMount method which is as close to a constructor as we'll get until ES6.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, ES6 classes will make this feel a lot nicer.

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