diff --git a/packages/create-component-with-subscriptions/README.md b/packages/create-component-with-subscriptions/README.md
new file mode 100644
index 0000000000000..baaf70a916665
--- /dev/null
+++ b/packages/create-component-with-subscriptions/README.md
@@ -0,0 +1,81 @@
+# create-component-with-subscriptions
+
+Better docs coming soon...
+
+```js
+// Here is an example of using the subscribable HOC.
+// It shows a couple of potentially common subscription types.
+function ExampleComponent(props: Props) {
+ const {
+ observedValue,
+ relayData,
+ scrollTop,
+ } = props;
+
+ // The rendered output is not interesting.
+ // The interesting thing is the incoming props/values.
+}
+
+function getDataFor(subscribable, propertyName) {
+ switch (propertyName) {
+ case 'fragmentResolver':
+ return subscribable.resolve();
+ case 'observableStream':
+ // This only works for some observable types (e.g. BehaviorSubject)
+ // It's okay to just return null/undefined here for other types.
+ return subscribable.getValue();
+ case 'scrollTarget':
+ return subscribable.scrollTop;
+ default:
+ throw Error(`Invalid subscribable, "${propertyName}", specified.`);
+ }
+}
+
+function subscribeTo(valueChangedCallback, subscribable, propertyName) {
+ switch (propertyName) {
+ case 'fragmentResolver':
+ subscribable.setCallback(
+ () => valueChangedCallback(subscribable.resolve()
+ );
+ break;
+ case 'observableStream':
+ // Return the subscription; it's necessary to unsubscribe.
+ return subscribable.subscribe(valueChangedCallback);
+ case 'scrollTarget':
+ const onScroll = () => valueChangedCallback(subscribable.scrollTop);
+ subscribable.addEventListener(onScroll);
+ return onScroll;
+ default:
+ throw Error(`Invalid subscribable, "${propertyName}", specified.`);
+ }
+}
+
+function unsubscribeFrom(subscribable, propertyName, subscription) {
+ switch (propertyName) {
+ case 'fragmentResolver':
+ subscribable.dispose();
+ break;
+ case 'observableStream':
+ // Unsubscribe using the subscription rather than the subscribable.
+ subscription.unsubscribe();
+ case 'scrollTarget':
+ // In this case, 'subscription', is the event handler/function.
+ subscribable.removeEventListener(subscription);
+ break;
+ default:
+ throw Error(`Invalid subscribable, "${propertyName}", specified.`);
+ }
+}
+
+// 3: This is the component you would export.
+createSubscribable({
+ subscribablePropertiesMap: {
+ fragmentResolver: 'relayData',
+ observableStream: 'observedValue',
+ scrollTarget: 'scrollTop',
+ },
+ getDataFor,
+ subscribeTo,
+ unsubscribeFrom,
+}, ExampleComponent);
+```
\ No newline at end of file
diff --git a/packages/create-component-with-subscriptions/index.js b/packages/create-component-with-subscriptions/index.js
new file mode 100644
index 0000000000000..e0ee91920b554
--- /dev/null
+++ b/packages/create-component-with-subscriptions/index.js
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+'use strict';
+
+export * from './src/createComponentWithSubscriptions';
diff --git a/packages/create-component-with-subscriptions/npm/index.js b/packages/create-component-with-subscriptions/npm/index.js
new file mode 100644
index 0000000000000..7262038596185
--- /dev/null
+++ b/packages/create-component-with-subscriptions/npm/index.js
@@ -0,0 +1,7 @@
+'use strict';
+
+if (process.env.NODE_ENV === 'production') {
+ module.exports = require('./cjs/create-component-with-subscriptions.production.min.js');
+} else {
+ module.exports = require('./cjs/create-component-with-subscriptions.development.js');
+}
diff --git a/packages/create-component-with-subscriptions/package.json b/packages/create-component-with-subscriptions/package.json
new file mode 100644
index 0000000000000..eeda6f59c5f8f
--- /dev/null
+++ b/packages/create-component-with-subscriptions/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "create-component-with-subscriptions",
+ "description": "HOC for creating async-safe React components with subscriptions",
+ "version": "0.0.1",
+ "repository": "facebook/react",
+ "files": ["LICENSE", "README.md", "index.js", "cjs/"],
+ "dependencies": {
+ "fbjs": "^0.8.16"
+ },
+ "peerDependencies": {
+ "react": "16.3.0-alpha.1"
+ }
+}
\ No newline at end of file
diff --git a/packages/create-component-with-subscriptions/src/__tests__/createComponentWithSubscriptions-test.js b/packages/create-component-with-subscriptions/src/__tests__/createComponentWithSubscriptions-test.js
new file mode 100644
index 0000000000000..aeb8511fcd55e
--- /dev/null
+++ b/packages/create-component-with-subscriptions/src/__tests__/createComponentWithSubscriptions-test.js
@@ -0,0 +1,171 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+let createComponent;
+let React;
+let ReactTestRenderer;
+
+describe('CreateComponentWithSubscriptions', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ createComponent = require('create-component-with-subscriptions')
+ .createComponent;
+ React = require('react');
+ ReactTestRenderer = require('react-test-renderer');
+ });
+
+ function createFauxObservable() {
+ let currentValue;
+ let subscribedCallback = null;
+ return {
+ getValue: () => currentValue,
+ subscribe: callback => {
+ expect(subscribedCallback).toBe(null);
+ subscribedCallback = callback;
+ return {
+ unsubscribe: () => {
+ expect(subscribedCallback).not.toBe(null);
+ subscribedCallback = null;
+ },
+ };
+ },
+ update: value => {
+ currentValue = value;
+ if (typeof subscribedCallback === 'function') {
+ subscribedCallback(value);
+ }
+ },
+ };
+ }
+
+ it('supports basic subscription pattern', () => {
+ const renderedValues = [];
+
+ const Component = createComponent(
+ {
+ subscribablePropertiesMap: {observable: 'value'},
+ getDataFor: (subscribable, propertyName) => {
+ expect(propertyName).toBe('observable');
+ return observable.getValue();
+ },
+ subscribeTo: (valueChangedCallback, subscribable, propertyName) => {
+ expect(propertyName).toBe('observable');
+ return subscribable.subscribe(valueChangedCallback);
+ },
+ unsubscribeFrom: (subscribable, propertyName, subscription) => {
+ expect(propertyName).toBe('observable');
+ subscription.unsubscribe();
+ },
+ },
+ ({value}) => {
+ renderedValues.push(value);
+ return null;
+ },
+ );
+
+ const observable = createFauxObservable();
+ const render = ReactTestRenderer.create(
+ ,
+ );
+
+ // Updates while subscribed should re-render the child component
+ expect(renderedValues).toEqual([undefined]);
+ renderedValues.length = 0;
+ observable.update(123);
+ expect(renderedValues).toEqual([123]);
+ renderedValues.length = 0;
+ observable.update('abc');
+ expect(renderedValues).toEqual(['abc']);
+
+ // Unsetting the subscriber prop should reset subscribed values
+ renderedValues.length = 0;
+ render.update();
+ expect(renderedValues).toEqual([undefined]);
+
+ // Updates while unsubscribed should not re-render the child component
+ renderedValues.length = 0;
+ observable.update(789);
+ expect(renderedValues).toEqual([]);
+ });
+
+ it('supports multiple subscriptions', () => {
+ const renderedValues = [];
+
+ const Component = createComponent(
+ {
+ subscribablePropertiesMap: {
+ foo: 'foo',
+ bar: 'bar',
+ },
+ getDataFor: (subscribable, propertyName) => {
+ switch (propertyName) {
+ case 'foo':
+ return foo.getValue();
+ case 'bar':
+ return bar.getValue();
+ default:
+ throw Error('Unexpected propertyName ' + propertyName);
+ }
+ },
+ subscribeTo: (valueChangedCallback, subscribable, propertyName) => {
+ switch (propertyName) {
+ case 'foo':
+ return foo.subscribe(valueChangedCallback);
+ case 'bar':
+ return bar.subscribe(valueChangedCallback);
+ default:
+ throw Error('Unexpected propertyName ' + propertyName);
+ }
+ },
+ unsubscribeFrom: (subscribable, propertyName, subscription) => {
+ switch (propertyName) {
+ case 'foo':
+ case 'bar':
+ subscription.unsubscribe();
+ break;
+ default:
+ throw Error('Unexpected propertyName ' + propertyName);
+ }
+ },
+ },
+ ({foo, bar}) => {
+ renderedValues.push({foo, bar});
+ return null;
+ },
+ );
+
+ const foo = createFauxObservable();
+ const bar = createFauxObservable();
+ const render = ReactTestRenderer.create();
+
+ // Updates while subscribed should re-render the child component
+ expect(renderedValues).toEqual([{bar: undefined, foo: undefined}]);
+ renderedValues.length = 0;
+ foo.update(123);
+ expect(renderedValues).toEqual([{bar: undefined, foo: 123}]);
+ renderedValues.length = 0;
+ bar.update('abc');
+ expect(renderedValues).toEqual([{bar: 'abc', foo: 123}]);
+ renderedValues.length = 0;
+ foo.update(456);
+ expect(renderedValues).toEqual([{bar: 'abc', foo: 456}]);
+
+ // Unsetting the subscriber prop should reset subscribed values
+ renderedValues.length = 0;
+ render.update();
+ expect(renderedValues).toEqual([{bar: undefined, foo: undefined}]);
+
+ // Updates while unsubscribed should not re-render the child component
+ renderedValues.length = 0;
+ foo.update(789);
+ expect(renderedValues).toEqual([]);
+ });
+});
diff --git a/packages/create-component-with-subscriptions/src/createComponentWithSubscriptions.js b/packages/create-component-with-subscriptions/src/createComponentWithSubscriptions.js
new file mode 100644
index 0000000000000..8a47d2731b51a
--- /dev/null
+++ b/packages/create-component-with-subscriptions/src/createComponentWithSubscriptions.js
@@ -0,0 +1,188 @@
+/**
+ * Copyright (c) 2014-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import React from 'react';
+
+type SubscribableConfig = {
+ // Maps property names of subscribable data sources (e.g. 'someObservable'),
+ // To state names for subscribed values (e.g. 'someValue').
+ subscribablePropertiesMap: {[subscribableProperty: string]: string},
+
+ // Synchronously get data for a given subscribable property.
+ // It is okay to return null if the subscribable does not support sync value reading.
+ getDataFor: (subscribable: any, propertyName: string) => any,
+
+ // Subscribe to a given subscribable.
+ // Due to the variety of change event types, subscribers should provide their own handlers.
+ // Those handlers should NOT update state though; they should call the valueChangedCallback() instead.
+ subscribeTo: (
+ valueChangedCallback: (value: any) => void,
+ subscribable: any,
+ propertyName: string,
+ ) => any,
+
+ // Unsubscribe from a given subscribable.
+ // The optional subscription object returned by subscribeTo() is passed as a third parameter.
+ unsubscribeFrom: (
+ subscribable: any,
+ propertyName: string,
+ subscription: any,
+ ) => void,
+};
+
+// TODO Decide how to handle missing subscribables.
+
+export function createComponent(
+ config: SubscribableConfig,
+ Component: React$ComponentType<*>,
+): React$ComponentType<*> {
+ const {
+ getDataFor,
+ subscribablePropertiesMap,
+ subscribeTo,
+ unsubscribeFrom,
+ } = config;
+
+ class SubscribableContainer extends React.Component {
+ state = {};
+
+ static getDerivedStateFromProps(nextProps, prevState) {
+ const nextState = {};
+
+ let hasUpdates = false;
+
+ // Read value (if sync read is possible) for upcoming render
+ for (let propertyName in subscribablePropertiesMap) {
+ const prevSubscribable = prevState[propertyName];
+ const nextSubscribable = nextProps[propertyName];
+
+ if (prevSubscribable !== nextSubscribable) {
+ nextState[propertyName] = {
+ ...prevState[propertyName],
+ subscribable: nextSubscribable,
+ value:
+ nextSubscribable != null
+ ? getDataFor(nextSubscribable, propertyName)
+ : undefined,
+ };
+
+ hasUpdates = true;
+ }
+ }
+
+ return hasUpdates ? nextState : null;
+ }
+
+ componentDidMount() {
+ for (let propertyName in subscribablePropertiesMap) {
+ const subscribable = this.props[propertyName];
+ this.subscribeTo(subscribable, propertyName);
+ }
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ for (let propertyName in subscribablePropertiesMap) {
+ const prevSubscribable = prevProps[propertyName];
+ const nextSubscribable = this.props[propertyName];
+ if (prevSubscribable !== nextSubscribable) {
+ this.unsubscribeFrom(prevSubscribable, propertyName);
+ this.subscribeTo(nextSubscribable, propertyName);
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ for (let propertyName in subscribablePropertiesMap) {
+ const subscribable = this.props[propertyName];
+ this.unsubscribeFrom(subscribable, propertyName);
+ }
+ }
+
+ // Event listeners are only safe to add during the commit phase,
+ // So they won't leak if render is interrupted or errors.
+ subscribeTo(subscribable, propertyName) {
+ if (subscribable != null) {
+ const wrapper = this.state[propertyName];
+
+ const valueChangedCallback = value => {
+ this.setState(state => {
+ const currentWrapper = state[propertyName];
+
+ // If this event belongs to the current data source, update state.
+ // Otherwise we should ignore it.
+ if (subscribable === currentWrapper.subscribable) {
+ return {
+ [propertyName]: {
+ ...currentWrapper,
+ value,
+ },
+ };
+ }
+
+ return null;
+ });
+ };
+
+ // Store subscription for later (in case it's needed to unsubscribe).
+ // This is safe to do via mutation since:
+ // 1) It does not impact render.
+ // 2) This method will only be called during the "commit" phase.
+ wrapper.subscription = subscribeTo(
+ valueChangedCallback,
+ subscribable,
+ propertyName,
+ );
+
+ // External values could change between render and mount,
+ // In some cases it may be important to handle this case.
+ const value = getDataFor(subscribable, propertyName);
+ if (value !== wrapper.value) {
+ this.setState({
+ [propertyName]: {
+ ...wrapper,
+ value,
+ },
+ });
+ }
+ }
+ }
+
+ unsubscribeFrom(subscribable, propertyName) {
+ if (subscribable != null) {
+ const wrapper = this.state[propertyName];
+
+ unsubscribeFrom(subscribable, propertyName, wrapper.subscription);
+
+ wrapper.subscription = null;
+ }
+ }
+
+ render() {
+ const filteredProps = {};
+ const subscribedValues = {};
+
+ for (let key in this.props) {
+ if (!subscribablePropertiesMap.hasOwnProperty(key)) {
+ filteredProps[key] = this.props[key];
+ }
+ }
+
+ for (let fromProperty in subscribablePropertiesMap) {
+ const toProperty = subscribablePropertiesMap[fromProperty];
+ const wrapper = this.state[fromProperty];
+ subscribedValues[toProperty] =
+ wrapper != null ? wrapper.value : undefined;
+ }
+
+ return ;
+ }
+ }
+
+ return SubscribableContainer;
+}