From 9edc6260a7db25f888259fddedb4aa2250f6385f Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Wed, 5 Nov 2014 19:24:11 -0800 Subject: [PATCH] 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). --- src/browser/ui/React.js | 4 + src/core/ReactComponent.js | 31 +++++-- src/core/ReactRef.js | 103 +++++++++++++++++++++ src/core/__tests__/ReactComponent-test.js | 108 ++++++++++++++++++++++ 4 files changed, 236 insertions(+), 10 deletions(-) create mode 100644 src/core/ReactRef.js diff --git a/src/browser/ui/React.js b/src/browser/ui/React.js index 19de5a77ea907..c966c2db85db3 100644 --- a/src/browser/ui/React.js +++ b/src/browser/ui/React.js @@ -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'); @@ -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, diff --git a/src/core/ReactComponent.js b/src/core/ReactComponent.js index 9d4e4b7e58944..7a7b9688d1bd7 100644 --- a/src/core/ReactComponent.js +++ b/src/core/ReactComponent.js @@ -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'); @@ -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. * @@ -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; @@ -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; @@ -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); } } }, diff --git a/src/core/ReactRef.js b/src/core/ReactRef.js new file mode 100644 index 0000000000000..4463d54bf0fc2 --- /dev/null +++ b/src/core/ReactRef.js @@ -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; diff --git a/src/core/__tests__/ReactComponent-test.js b/src/core/__tests__/ReactComponent-test.js index 3da25a0567dc7..3f4129b72a755 100644 --- a/src/core/__tests__/ReactComponent-test.js +++ b/src/core/__tests__/ReactComponent-test.js @@ -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
{this.props.children}
; + } + }); + + 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 = ; + var outer = ( + + {inner} + + ); + 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 = ; + 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
; + }, + render: function() { + return ( + + ); + }, + 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 = ; + 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() {