diff --git a/src/addons/ReactWithAddons.js b/src/addons/ReactWithAddons.js
index bcf5f06ceaab2..c74144737fb26 100644
--- a/src/addons/ReactWithAddons.js
+++ b/src/addons/ReactWithAddons.js
@@ -23,6 +23,7 @@ var React = require('React');
var ReactComponentWithPureRenderMixin =
require('ReactComponentWithPureRenderMixin');
var ReactCSSTransitionGroup = require('ReactCSSTransitionGroup');
+var ReactDataTracker = require('ReactDataTracker');
var ReactFragment = require('ReactFragment');
var ReactTransitionGroup = require('ReactTransitionGroup');
var ReactUpdates = require('ReactUpdates');
@@ -43,7 +44,15 @@ React.addons = {
createFragment: ReactFragment.create,
renderSubtreeIntoContainer: renderSubtreeIntoContainer,
shallowCompare: shallowCompare,
- update: update
+ update: update,
+ observeRead: function(reactDataEntity) {
+ ReactDataTracker.startRead(reactDataEntity);
+ ReactDataTracker.endRead(reactDataEntity);
+ },
+ observeWrite: function(reactDataEntity) {
+ ReactDataTracker.startWrite(reactDataEntity);
+ ReactDataTracker.endWrite(reactDataEntity);
+ }
};
if (__DEV__) {
diff --git a/src/addons/__tests__/ReactDataTracker-test.js b/src/addons/__tests__/ReactDataTracker-test.js
new file mode 100644
index 0000000000000..7e44dfd851487
--- /dev/null
+++ b/src/addons/__tests__/ReactDataTracker-test.js
@@ -0,0 +1,51 @@
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+var React = require('ReactWithAddons');
+
+describe('ReactDataTrack', function() {
+
+ it('should update component when a write fires', function () {
+
+ class Person {
+ constructor(name) {
+ this.setName(name);
+ }
+
+ setName(name) {
+ this.name = name;
+ React.addons.observeWrite(this);
+ }
+
+ getName() {
+ React.addons.observeRead(this);
+ return this.name;
+ }
+ }
+
+ class PersonView extends React.Component {
+ render() {
+ return
{this.props.person.getName()}
;
+ }
+ }
+
+ var container = document.createElement('div');
+
+ var person = new Person("jimfb");
+ React.render(, container);
+ expect(container.children[0].innerHTML).toBe('jimfb');
+ person.setName("Jim");
+ expect(container.children[0].innerHTML).toBe('Jim');
+ React.unmountComponentAtNode(container);
+ });
+});
diff --git a/src/renderers/dom/client/ReactMount.js b/src/renderers/dom/client/ReactMount.js
index 090ae1624ca44..96d6721337a77 100644
--- a/src/renderers/dom/client/ReactMount.js
+++ b/src/renderers/dom/client/ReactMount.js
@@ -698,6 +698,9 @@ var ReactMount = {
);
delete instancesByReactRootID[reactRootID];
delete containersByReactRootID[reactRootID];
+ if (component._instance._tracker) {
+ component._instance._tracker.destroy();
+ }
if (__DEV__) {
delete rootElementsByReactRootID[reactRootID];
}
diff --git a/src/renderers/shared/reconciler/ReactCompositeComponent.js b/src/renderers/shared/reconciler/ReactCompositeComponent.js
index 1f6b2a635c4d2..539e20af7e571 100644
--- a/src/renderers/shared/reconciler/ReactCompositeComponent.js
+++ b/src/renderers/shared/reconciler/ReactCompositeComponent.js
@@ -14,6 +14,7 @@
var ReactComponentEnvironment = require('ReactComponentEnvironment');
var ReactContext = require('ReactContext');
var ReactCurrentOwner = require('ReactCurrentOwner');
+var ReactDataTracker = require('ReactDataTracker');
var ReactElement = require('ReactElement');
var ReactElementValidator = require('ReactElementValidator');
var ReactInstanceMap = require('ReactInstanceMap');
@@ -739,7 +740,20 @@ var ReactCompositeComponentMixin = {
*/
_renderValidatedComponentWithoutOwnerOrContext: function() {
var inst = this._instance;
- var renderedComponent = inst.render();
+
+ // Setup data tracker (TODO: Singleton tracker for faster perf)
+ if (inst._tracker === undefined) {
+ inst._tracker = new ReactDataTracker(function() {
+ return inst.render();
+ });
+ inst._tracker.setCallback(function() {
+ inst.setState({});
+ });
+ } else {
+ inst._tracker._cacheValid = false;
+ }
+
+ var renderedComponent = inst._tracker.read();
if (__DEV__) {
// We allow auto-mocks to proceed as if they're returning null.
if (typeof renderedComponent === 'undefined' &&
diff --git a/src/renderers/shared/reconciler/ReactDataTracker.js b/src/renderers/shared/reconciler/ReactDataTracker.js
new file mode 100644
index 0000000000000..d2972d5bf5ec7
--- /dev/null
+++ b/src/renderers/shared/reconciler/ReactDataTracker.js
@@ -0,0 +1,172 @@
+/**
+ * Copyright 2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+* @providesModule ReactDataTracker
+*/
+'use strict';
+
+// TODO: Using the ES6 Polyfill
+// Using expando properties might be a possibility, but I opted away from this because:
+// 1. This code isn't production-ready yet anyway, this code is mostly to demo purposes
+// 2. New browsers support ES6 maps, so this only has perf ramifications on legacy browsers
+// 3. Perhaps most importantly: The data entities are user data objects, meaning that
+// they could be frozen, or iterated over, or any number of other edge cases that
+// would make adding expando properties a fairly unfriendly thing to do.
+var Es6Map = (typeof Map !== 'undefined' ? Map : require('es6-collections').Map);
+
+var ReactDataTracker = function(dataFunction) {
+ var tracker = {
+ _cacheValid: false,
+ _cachedResult: undefined,
+ _dataFunction: dataFunction,
+ read: function() {
+ ReactDataTracker.startRead();
+ ReactDataTracker.endRead();
+ if (!tracker._cacheValid) {
+ ReactDataTracker.startRender(tracker);
+ tracker._cachedResult = tracker._dataFunction();
+ ReactDataTracker.endRender(tracker);
+ tracker._cacheValid = true;
+ }
+ return tracker._cachedResult;
+ },
+ setCallback: function(callback) {
+ tracker._callback = callback;
+ },
+ destroy: function() {
+ ReactDataTracker.unmount(tracker);
+ }
+ };
+ return tracker;
+};
+
+ReactDataTracker.startRender = function(component) {
+ ReactDataTracker.currentContext = [];
+ if (ReactDataTracker.listeners === undefined) {
+ ReactDataTracker.listeners = new Es6Map();
+ }
+ if (ReactDataTracker.dataSources === undefined) {
+ ReactDataTracker.dataSources = new Es6Map();
+ }
+ if (!ReactDataTracker.dataSources.has(component)) {
+ ReactDataTracker.dataSources.set(component, []);
+ }
+ };
+
+ReactDataTracker.endRender = function(component) {
+ var oldDataSources = ReactDataTracker.dataSources.get(component);
+ var newDataSources = ReactDataTracker.currentContext;
+ var index = 0;
+
+ for (index = 0; index < oldDataSources.length; index++) {
+ if (newDataSources.indexOf(oldDataSources[index]) === -1) {
+ var oldListeners = ReactDataTracker.listeners.get(oldDataSources[index]);
+ oldListeners.splice(oldListeners.indexOf(component), 1);
+ oldDataSources.splice(index, 1);
+ index--;
+ }
+ }
+ for (index = 0; index < newDataSources.length; index++) {
+ if (oldDataSources.indexOf(newDataSources[index]) === -1) {
+ if (!ReactDataTracker.listeners.has(newDataSources[index])) {
+ ReactDataTracker.listeners.set(newDataSources[index], []);
+ }
+ ReactDataTracker.listeners.get(newDataSources[index]).push(component);
+ ReactDataTracker.dataSources.get(component).push(newDataSources[index]);
+ }
+ }
+ };
+
+ReactDataTracker.startRead = function(entity) {
+ if (ReactDataTracker.activeReaders === undefined) {
+ ReactDataTracker.activeReaders = 0;
+ }
+ ReactDataTracker.activeReaders++;
+ };
+
+ReactDataTracker.endRead = function(entity) {
+ if (ReactDataTracker.currentContext !== undefined && ReactDataTracker.currentContext.indexOf(entity) === -1) {
+ ReactDataTracker.currentContext.push(entity);
+ }
+ ReactDataTracker.activeReaders--;
+ if (ReactDataTracker.activeReaders < 0) {
+ throw new Error('Number of active readers dropped below zero');
+ }
+ };
+
+ReactDataTracker.startWrite = function(entity) {
+ if (ReactDataTracker.writers === undefined) {
+ ReactDataTracker.writers = [];
+ }
+ if (ReactDataTracker.writers.indexOf(entity) === -1) {
+ ReactDataTracker.writers.push(entity);
+ }
+ if (ReactDataTracker.activeWriters === undefined) {
+ ReactDataTracker.activeWriters = 0;
+ }
+ ReactDataTracker.activeWriters++;
+ };
+
+ReactDataTracker.endWrite = function(entity) {
+ if (ReactDataTracker.activeWriters === undefined) {
+ throw new Error('Can not end write without starting write');
+ }
+ if (ReactDataTracker.writers.indexOf(entity) === -1) {
+ throw new Error('Can not end write without starting write');
+ }
+ ReactDataTracker.activeWriters--;
+
+ if (ReactDataTracker.activeWriters === 0) {
+ // for each writer that wrote during this batch
+ var componentsToNotify = [];
+ for (var writerIndex = 0; writerIndex < ReactDataTracker.writers.length; writerIndex++) {
+ var writer = ReactDataTracker.writers[writerIndex];
+ if (ReactDataTracker.listeners === undefined) {
+ continue;
+ }
+ if (!ReactDataTracker.listeners.has(writer)) {
+ continue;
+ }
+ var listenersList = ReactDataTracker.listeners.get(writer);
+ for (var index = 0; index < listenersList.length; index++) {
+ if (componentsToNotify.indexOf(listenersList[index]) === -1) {
+ componentsToNotify.push(listenersList[index]);
+ }
+ }
+ }
+
+ for (var componentIndex = 0; componentIndex < componentsToNotify.length; componentIndex++) {
+ var component = componentsToNotify[componentIndex];
+ var invokeCallback = component._cacheValid && component._callback !== undefined;
+ component._cacheValid = false; // Invalidate cache before calling callback
+ if (invokeCallback) {
+ component._callback();
+ }
+ }
+ ReactDataTracker.writers = [];
+ }
+ };
+
+ReactDataTracker.unmount = function(component) {
+ var oldDataSources = ReactDataTracker.dataSources.get(component);
+ if (oldDataSources === undefined) {
+ return;
+ }
+ for (var index = 0; index < oldDataSources.length; index++) {
+ var entityListeners = ReactDataTracker.listeners.get(oldDataSources[index]);
+ var entityListenerPosition = entityListeners.indexOf(component);
+ if (entityListenerPosition > -1) {
+ entityListeners.splice(entityListeners.indexOf(component), 1);
+ } else {
+ throw new Error('Unable to find listener when unmounting component');
+ }
+ }
+ ReactDataTracker.dataSources.delete(component);
+ };
+
+module.exports = ReactDataTracker;
diff --git a/src/shared/vendor/third_party/es6-collections.js b/src/shared/vendor/third_party/es6-collections.js
new file mode 100644
index 0000000000000..13d55e4b2a315
--- /dev/null
+++ b/src/shared/vendor/third_party/es6-collections.js
@@ -0,0 +1,31 @@
+/**
+ *
+ * Copyright (C) 2011 by Andrea Giammarchi, @WebReflection
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @providesModule es6-collections
+ */
+
+(function(e){function f(a,c){function b(a){if(!this||this.constructor!==b)return new b(a);this._keys=[];this._values=[];this._itp=[];this.objectOnly=c;a&&v.call(this,a)}c||w(a,"size",{get:x});a.constructor=b;b.prototype=a;return b}function v(a){this.add?a.forEach(this.add,this):a.forEach(function(a){this.set(a[0],a[1])},this)}function d(a){this.has(a)&&(this._keys.splice(b,1),this._values.splice(b,1),this._itp.forEach(function(a){b