diff --git a/packages/create-subscription/README.md b/packages/create-subscription/README.md
new file mode 100644
index 0000000000000..a55210d1d4b89
--- /dev/null
+++ b/packages/create-subscription/README.md
@@ -0,0 +1,184 @@
+# create-subscription
+
+`create-subscription` provides an async-safe interface to manage a subscription.
+
+## When should you NOT use this?
+
+This utility should be used for subscriptions to a single value that are typically only read in one place and may update frequently (e.g. a component that subscribes to a geolocation API to show a dot on a map).
+
+Other cases have **better long-term solutions**:
+* Redux/Flux stores should use the [context API](https://reactjs.org/docs/context.html) instead.
+* I/O subscriptions (e.g. notifications) that update infrequently should use [`simple-cache-provider`](https://github.com/facebook/react/blob/master/packages/simple-cache-provider/README.md) instead.
+* Complex libraries like Relay/Apollo should manage subscriptions manually with the same techniques which this library uses under the hood (as referenced [here](https://gist.github.com/bvaughn/d569177d70b50b58bff69c3c4a5353f3)) in a way that is most optimized for their library usage.
+
+## What types of subscriptions can this support?
+
+This abstraction can handle a variety of subscription types, including:
+* Event dispatchers like `HTMLInputElement`.
+* Custom pub/sub components like Relay's `FragmentSpecResolver`.
+* Observable types like RxJS `BehaviorSubject` and `ReplaySubject`. (Types like RxJS `Subject` or `Observable` are not supported, because they provide no way to read the "current" value after it has been emitted.)
+* Native Promises.
+
+# Installation
+
+```sh
+# Yarn
+yarn add create-subscription
+
+# NPM
+npm install create-subscription --save
+```
+
+# Usage
+
+To configure a subscription, you must provide two methods: `getCurrentValue` and `subscribe`.
+
+```js
+import { createSubscription } from "create-subscription";
+
+const Subscription = createSubscription({
+  getCurrentValue(source) {
+    // Return the current value of the subscription (source),
+    // or `undefined` if the value can't be read synchronously (e.g. native Promises).
+  },
+  subscribe(source, callback) {
+    // Subscribe (e.g. add an event listener) to the subscription (source).
+    // Call callback(newValue) whenever a subscription changes.
+    // Return an unsubscribe method,
+    // Or a no-op if unsubscribe is not supported (e.g. native Promises).
+  }
+});
+```
+
+To use the `Subscription` component, pass the subscribable property (e.g. an event dispatcher, Flux store, observable) as the `source` property and use a [render prop](https://reactjs.org/docs/render-props.html), `children`, to handle the subscribed value when it changes:
+
+```js
+<Subscription source={eventDispatcher}>
+  {value => <AnotherComponent value={value} />}
+</Subscription>
+```
+
+# Examples
+
+This API can be used to subscribe to a variety of "subscribable" sources, from event dispatchers to RxJS observables. Below are a few examples of how to subscribe to common types.
+
+## Subscribing to event dispatchers
+
+Below is an example showing how `create-subscription` can be used to subscribe to event dispatchers such as DOM elements.
+
+```js
+import React from "react";
+import { createSubscription } from "create-subscription";
+
+// Start with a simple component.
+// In this case, it's a functional component, but it could have been a class.
+function FollowerComponent({ followersCount }) {
+  return <div>You have {followersCount} followers!</div>;
+}
+
+// Create a wrapper component to manage the subscription.
+const EventHandlerSubscription = createSubscription({
+  getCurrentValue: eventDispatcher => eventDispatcher.value,
+  subscribe: (eventDispatcher, callback) => {
+    const onChange = event => callback(eventDispatcher.value);
+    eventDispatcher.addEventListener("change", onChange);
+    return () => eventDispatcher.removeEventListener("change", onChange);
+  }
+});
+
+// Your component can now be used as shown below.
+// In this example, 'eventDispatcher' represents a generic event dispatcher.
+<EventHandlerSubscription source={eventDispatcher}>
+  {value => <FollowerComponent followersCount={value} />}
+</EventHandlerSubscription>;
+```
+
+## Subscribing to observables
+
+Below are examples showing how `create-subscription` can be used to subscribe to certain types of observables (e.g. RxJS `BehaviorSubject` and `ReplaySubject`).
+
+**Note** that it is not possible to support all observable types (e.g. RxJS `Subject` or `Observable`) because some provide no way to read the "current" value after it has been emitted.
+
+### `BehaviorSubject`
+```js
+const BehaviorSubscription = createSubscription({
+  getCurrentValue: behaviorSubject => behaviorSubject.getValue(),
+  subscribe: (behaviorSubject, callback) => {
+    const subscription = behaviorSubject.subscribe(callback);
+    return () => subscription.unsubscribe();
+  }
+});
+```
+
+### `ReplaySubject`
+```js
+const ReplaySubscription = createSubscription({
+  getCurrentValue: replaySubject => {
+    let currentValue;
+    // ReplaySubject does not have a sync data getter,
+    // So we need to temporarily subscribe to retrieve the most recent value.
+    replaySubject
+      .subscribe(value => {
+        currentValue = value;
+      })
+      .unsubscribe();
+    return currentValue;
+  },
+  subscribe: (replaySubject, callback) => {
+    const subscription = replaySubject.subscribe(callback);
+    return () => subscription.unsubscribe();
+  }
+});
+```
+
+## Subscribing to a Promise
+
+Below is an example showing how `create-subscription` can be used with native Promises.
+
+**Note** that it an initial render value of `undefined` is unavoidable due to the fact that Promises provide no way to synchronously read their current value.
+
+**Note** the lack of a way to "unsubscribe" from a Promise can result in memory leaks as long as something has a reference to the Promise. This should be taken into considerationg when determining whether Promises are appropriate to use in this way within your application.
+
+```js
+import React from "react";
+import { createSubscription } from "create-subscription";
+
+// Start with a simple component.
+function LoadingComponent({ loadingStatus }) {
+  if (loadingStatus === undefined) {
+    // Loading
+  } else if (loadingStatus === null) {
+    // Error
+  } else {
+    // Success
+  }
+}
+
+// Wrap the functional component with a subscriber HOC.
+// This HOC will manage subscriptions and pass values to the decorated component.
+// It will add and remove subscriptions in an async-safe way when props change.
+const PromiseSubscription = createSubscription({
+  getCurrentValue: promise => {
+    // There is no way to synchronously read a Promise's value,
+    // So this method should return undefined.
+    return undefined;
+  },
+  subscribe: (promise, callback) => {
+    promise.then(
+      // Success
+      value => callback(value),
+      // Failure
+      () => callback(null)
+    );
+
+    // There is no way to "unsubscribe" from a Promise.
+    // create-subscription will still prevent stale values from rendering.
+    return () => {};
+  }
+});
+
+// Your component can now be used as shown below.
+<PromiseSubscription source={loadingPromise}>
+  {loadingStatus => <LoadingComponent loadingStatus={loadingStatus} />}
+</PromiseSubscription>
+```
diff --git a/packages/create-subscription/index.js b/packages/create-subscription/index.js
new file mode 100644
index 0000000000000..8e84321cc3186
--- /dev/null
+++ b/packages/create-subscription/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/createSubscription';
diff --git a/packages/create-subscription/npm/index.js b/packages/create-subscription/npm/index.js
new file mode 100644
index 0000000000000..6b7a5b017457d
--- /dev/null
+++ b/packages/create-subscription/npm/index.js
@@ -0,0 +1,7 @@
+'use strict';
+
+if (process.env.NODE_ENV === 'production') {
+  module.exports = require('./cjs/create-subscription.production.min.js');
+} else {
+  module.exports = require('./cjs/create-subscription.development.js');
+}
diff --git a/packages/create-subscription/package.json b/packages/create-subscription/package.json
new file mode 100644
index 0000000000000..bd8ae749f46bc
--- /dev/null
+++ b/packages/create-subscription/package.json
@@ -0,0 +1,21 @@
+{
+  "name": "create-subscription",
+  "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"
+  },
+  "devDependencies": {
+    "rxjs": "^5.5.6"
+  }
+}
diff --git a/packages/create-subscription/src/__tests__/createSubscription-test.internal.js b/packages/create-subscription/src/__tests__/createSubscription-test.internal.js
new file mode 100644
index 0000000000000..8dee9bfa5c9de
--- /dev/null
+++ b/packages/create-subscription/src/__tests__/createSubscription-test.internal.js
@@ -0,0 +1,457 @@
+/**
+ * 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 createSubscription;
+let BehaviorSubject;
+let ReactFeatureFlags;
+let React;
+let ReactNoop;
+let ReplaySubject;
+
+describe('createSubscription', () => {
+  beforeEach(() => {
+    jest.resetModules();
+    createSubscription = require('create-subscription').createSubscription;
+    ReactFeatureFlags = require('shared/ReactFeatureFlags');
+    ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
+    React = require('react');
+    ReactNoop = require('react-noop-renderer');
+
+    BehaviorSubject = require('rxjs/BehaviorSubject').BehaviorSubject;
+    ReplaySubject = require('rxjs/ReplaySubject').ReplaySubject;
+  });
+
+  function createBehaviorSubject(initialValue) {
+    const behaviorSubject = new BehaviorSubject();
+    if (initialValue) {
+      behaviorSubject.next(initialValue);
+    }
+    return behaviorSubject;
+  }
+
+  function createReplaySubject(initialValue) {
+    const replaySubject = new ReplaySubject();
+    if (initialValue) {
+      replaySubject.next(initialValue);
+    }
+    return replaySubject;
+  }
+
+  it('supports basic subscription pattern', () => {
+    const Subscription = createSubscription({
+      getCurrentValue: source => source.getValue(),
+      subscribe: (source, callback) => {
+        const subscription = source.subscribe(callback);
+        return () => subscription.unsubscribe;
+      },
+    });
+
+    const observable = createBehaviorSubject();
+    ReactNoop.render(
+      <Subscription source={observable}>
+        {(value = 'default') => {
+          ReactNoop.yield(value);
+          return null;
+        }}
+      </Subscription>,
+    );
+
+    // Updates while subscribed should re-render the child component
+    expect(ReactNoop.flush()).toEqual(['default']);
+    observable.next(123);
+    expect(ReactNoop.flush()).toEqual([123]);
+    observable.next('abc');
+    expect(ReactNoop.flush()).toEqual(['abc']);
+
+    // Unmounting the subscriber should remove listeners
+    ReactNoop.render(<div />);
+    observable.next(456);
+    expect(ReactNoop.flush()).toEqual([]);
+  });
+
+  it('should support observable types like RxJS ReplaySubject', () => {
+    const Subscription = createSubscription({
+      getCurrentValue: source => {
+        let currentValue;
+        source
+          .subscribe(value => {
+            currentValue = value;
+          })
+          .unsubscribe();
+        return currentValue;
+      },
+      subscribe: (source, callback) => {
+        const subscription = source.subscribe(callback);
+        return () => subscription.unsubscribe;
+      },
+    });
+
+    function render(value = 'default') {
+      ReactNoop.yield(value);
+      return null;
+    }
+
+    const observable = createReplaySubject('initial');
+
+    ReactNoop.render(<Subscription source={observable}>{render}</Subscription>);
+    expect(ReactNoop.flush()).toEqual(['initial']);
+    observable.next('updated');
+    expect(ReactNoop.flush()).toEqual(['updated']);
+
+    // Unsetting the subscriber prop should reset subscribed values
+    ReactNoop.render(<Subscription>{render}</Subscription>);
+    expect(ReactNoop.flush()).toEqual(['default']);
+  });
+
+  describe('Promises', () => {
+    it('should support Promises', async () => {
+      const Subscription = createSubscription({
+        getCurrentValue: source => undefined,
+        subscribe: (source, callback) => {
+          source.then(value => callback(value), value => callback(value));
+          // (Can't unsubscribe from a Promise)
+          return () => {};
+        },
+      });
+
+      function render(hasLoaded) {
+        if (hasLoaded === undefined) {
+          ReactNoop.yield('loading');
+        } else {
+          ReactNoop.yield(hasLoaded ? 'finished' : 'failed');
+        }
+        return null;
+      }
+
+      let resolveA, rejectB;
+      const promiseA = new Promise((resolve, reject) => {
+        resolveA = resolve;
+      });
+      const promiseB = new Promise((resolve, reject) => {
+        rejectB = reject;
+      });
+
+      // Test a promise that resolves after render
+      ReactNoop.render(<Subscription source={promiseA}>{render}</Subscription>);
+      expect(ReactNoop.flush()).toEqual(['loading']);
+      resolveA(true);
+      await promiseA;
+      expect(ReactNoop.flush()).toEqual(['finished']);
+
+      // Test a promise that resolves before render
+      // Note that this will require an extra render anyway,
+      // Because there is no way to syncrhonously get a Promise's value
+      rejectB(false);
+      ReactNoop.render(<Subscription source={promiseB}>{render}</Subscription>);
+      expect(ReactNoop.flush()).toEqual(['loading']);
+      await promiseB.catch(() => true);
+      expect(ReactNoop.flush()).toEqual(['failed']);
+    });
+
+    it('should still work if unsubscription is managed incorrectly', async () => {
+      const Subscription = createSubscription({
+        getCurrentValue: source => undefined,
+        subscribe: (source, callback) => {
+          source.then(callback);
+          // (Can't unsubscribe from a Promise)
+          return () => {};
+        },
+      });
+
+      function render(value = 'default') {
+        ReactNoop.yield(value);
+        return null;
+      }
+
+      let resolveA, resolveB;
+      const promiseA = new Promise(resolve => (resolveA = resolve));
+      const promiseB = new Promise(resolve => (resolveB = resolve));
+
+      // Subscribe first to Promise A then Promise B
+      ReactNoop.render(<Subscription source={promiseA}>{render}</Subscription>);
+      expect(ReactNoop.flush()).toEqual(['default']);
+      ReactNoop.render(<Subscription source={promiseB}>{render}</Subscription>);
+      expect(ReactNoop.flush()).toEqual(['default']);
+
+      // Resolve both Promises
+      resolveB(123);
+      resolveA('abc');
+      await Promise.all([promiseA, promiseB]);
+
+      // Ensure that only Promise B causes an update
+      expect(ReactNoop.flush()).toEqual([123]);
+    });
+  });
+
+  it('should unsubscribe from old subscribables and subscribe to new subscribables when props change', () => {
+    const Subscription = createSubscription({
+      getCurrentValue: source => source.getValue(),
+      subscribe: (source, callback) => {
+        const subscription = source.subscribe(callback);
+        return () => subscription.unsubscribe();
+      },
+    });
+
+    function render(value = 'default') {
+      ReactNoop.yield(value);
+      return null;
+    }
+
+    const observableA = createBehaviorSubject('a-0');
+    const observableB = createBehaviorSubject('b-0');
+
+    ReactNoop.render(
+      <Subscription source={observableA}>{render}</Subscription>,
+    );
+
+    // Updates while subscribed should re-render the child component
+    expect(ReactNoop.flush()).toEqual(['a-0']);
+
+    // Unsetting the subscriber prop should reset subscribed values
+    ReactNoop.render(
+      <Subscription source={observableB}>{render}</Subscription>,
+    );
+    expect(ReactNoop.flush()).toEqual(['b-0']);
+
+    // Updates to the old subscribable should not re-render the child component
+    observableA.next('a-1');
+    expect(ReactNoop.flush()).toEqual([]);
+
+    // Updates to the bew subscribable should re-render the child component
+    observableB.next('b-1');
+    expect(ReactNoop.flush()).toEqual(['b-1']);
+  });
+
+  it('should ignore values emitted by a new subscribable until the commit phase', () => {
+    const log = [];
+    let parentInstance;
+
+    function Child({value}) {
+      ReactNoop.yield('Child: ' + value);
+      return null;
+    }
+
+    const Subscription = createSubscription({
+      getCurrentValue: source => source.getValue(),
+      subscribe: (source, callback) => {
+        const subscription = source.subscribe(callback);
+        return () => subscription.unsubscribe();
+      },
+    });
+
+    class Parent extends React.Component {
+      state = {};
+
+      static getDerivedStateFromProps(nextProps, prevState) {
+        if (nextProps.observed !== prevState.observed) {
+          return {
+            observed: nextProps.observed,
+          };
+        }
+
+        return null;
+      }
+
+      componentDidMount() {
+        log.push('Parent.componentDidMount');
+      }
+
+      componentDidUpdate() {
+        log.push('Parent.componentDidUpdate');
+      }
+
+      render() {
+        parentInstance = this;
+
+        return (
+          <Subscription source={this.state.observed}>
+            {(value = 'default') => {
+              ReactNoop.yield('Subscriber: ' + value);
+              return <Child value={value} />;
+            }}
+          </Subscription>
+        );
+      }
+    }
+
+    const observableA = createBehaviorSubject('a-0');
+    const observableB = createBehaviorSubject('b-0');
+
+    ReactNoop.render(<Parent observed={observableA} />);
+    expect(ReactNoop.flush()).toEqual(['Subscriber: a-0', 'Child: a-0']);
+    expect(log).toEqual(['Parent.componentDidMount']);
+
+    // Start React update, but don't finish
+    ReactNoop.render(<Parent observed={observableB} />);
+    ReactNoop.flushThrough(['Subscriber: b-0']);
+    expect(log).toEqual(['Parent.componentDidMount']);
+
+    // Emit some updates from the uncommitted subscribable
+    observableB.next('b-1');
+    observableB.next('b-2');
+    observableB.next('b-3');
+
+    // Mimic a higher-priority interruption
+    parentInstance.setState({observed: observableA});
+
+    // Flush everything and ensure that the correct subscribable is used
+    // We expect the last emitted update to be rendered (because of the commit phase value check)
+    // But the intermediate ones should be ignored,
+    // And the final rendered output should be the higher-priority observable.
+    expect(ReactNoop.flush()).toEqual([
+      'Child: b-0',
+      'Subscriber: b-3',
+      'Child: b-3',
+      'Subscriber: a-0',
+      'Child: a-0',
+    ]);
+    expect(log).toEqual([
+      'Parent.componentDidMount',
+      'Parent.componentDidUpdate',
+      'Parent.componentDidUpdate',
+    ]);
+  });
+
+  it('should not drop values emitted between updates', () => {
+    const log = [];
+    let parentInstance;
+
+    function Child({value}) {
+      ReactNoop.yield('Child: ' + value);
+      return null;
+    }
+
+    const Subscription = createSubscription({
+      getCurrentValue: source => source.getValue(),
+      subscribe: (source, callback) => {
+        const subscription = source.subscribe(callback);
+        return () => subscription.unsubscribe();
+      },
+    });
+
+    class Parent extends React.Component {
+      state = {};
+
+      static getDerivedStateFromProps(nextProps, prevState) {
+        if (nextProps.observed !== prevState.observed) {
+          return {
+            observed: nextProps.observed,
+          };
+        }
+
+        return null;
+      }
+
+      componentDidMount() {
+        log.push('Parent.componentDidMount');
+      }
+
+      componentDidUpdate() {
+        log.push('Parent.componentDidUpdate');
+      }
+
+      render() {
+        parentInstance = this;
+
+        return (
+          <Subscription source={this.state.observed}>
+            {(value = 'default') => {
+              ReactNoop.yield('Subscriber: ' + value);
+              return <Child value={value} />;
+            }}
+          </Subscription>
+        );
+      }
+    }
+
+    const observableA = createBehaviorSubject('a-0');
+    const observableB = createBehaviorSubject('b-0');
+
+    ReactNoop.render(<Parent observed={observableA} />);
+    expect(ReactNoop.flush()).toEqual(['Subscriber: a-0', 'Child: a-0']);
+    expect(log).toEqual(['Parent.componentDidMount']);
+
+    // Start React update, but don't finish
+    ReactNoop.render(<Parent observed={observableB} />);
+    ReactNoop.flushThrough(['Subscriber: b-0']);
+    expect(log).toEqual(['Parent.componentDidMount']);
+
+    // Emit some updates from the old subscribable
+    observableA.next('a-1');
+    observableA.next('a-2');
+
+    // Mimic a higher-priority interruption
+    parentInstance.setState({observed: observableA});
+
+    // Flush everything and ensure that the correct subscribable is used
+    // We expect the new subscribable to finish rendering,
+    // But then the updated values from the old subscribable should be used.
+    expect(ReactNoop.flush()).toEqual([
+      'Child: b-0',
+      'Subscriber: a-2',
+      'Child: a-2',
+    ]);
+    expect(log).toEqual([
+      'Parent.componentDidMount',
+      'Parent.componentDidUpdate',
+      'Parent.componentDidUpdate',
+    ]);
+
+    // Updates from the new subsribable should be ignored.
+    observableB.next('b-1');
+    expect(ReactNoop.flush()).toEqual([]);
+    expect(log).toEqual([
+      'Parent.componentDidMount',
+      'Parent.componentDidUpdate',
+      'Parent.componentDidUpdate',
+    ]);
+  });
+
+  describe('warnings', () => {
+    it('should warn for invalid missing getCurrentValue', () => {
+      expect(() => {
+        createSubscription(
+          {
+            subscribe: () => () => {},
+          },
+          () => null,
+        );
+      }).toWarnDev('Subscription must specify a getCurrentValue function');
+    });
+
+    it('should warn for invalid missing subscribe', () => {
+      expect(() => {
+        createSubscription(
+          {
+            getCurrentValue: () => () => {},
+          },
+          () => null,
+        );
+      }).toWarnDev('Subscription must specify a subscribe function');
+    });
+
+    it('should warn if subscribe does not return an unsubscribe method', () => {
+      const Subscription = createSubscription({
+        getCurrentValue: source => undefined,
+        subscribe: (source, callback) => {},
+      });
+
+      const observable = createBehaviorSubject();
+      ReactNoop.render(
+        <Subscription source={observable}>{value => null}</Subscription>,
+      );
+
+      expect(ReactNoop.flush).toThrow(
+        'A subscription must return an unsubscribe function.',
+      );
+    });
+  });
+});
diff --git a/packages/create-subscription/src/createSubscription.js b/packages/create-subscription/src/createSubscription.js
new file mode 100644
index 0000000000000..748090d6cc961
--- /dev/null
+++ b/packages/create-subscription/src/createSubscription.js
@@ -0,0 +1,159 @@
+/**
+ * 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';
+import invariant from 'fbjs/lib/invariant';
+import warning from 'fbjs/lib/warning';
+
+type Unsubscribe = () => void;
+
+export function createSubscription<Property, Value>(
+  config: $ReadOnly<{|
+    // Synchronously gets the value for the subscribed property.
+    // Return undefined if the subscribable value is undefined,
+    // Or does not support synchronous reading (e.g. native Promise).
+    getCurrentValue: (source: Property) => Value | void,
+
+    // Setup a subscription for the subscribable value in props, and return an unsubscribe function.
+    // Return false to indicate the property cannot be unsubscribed from (e.g. native Promises).
+    // Due to the variety of change event types, subscribers should provide their own handlers.
+    // Those handlers should not attempt to update state though;
+    // They should call the callback() instead when a subscription changes.
+    subscribe: (
+      source: Property,
+      callback: (value: Value | void) => void,
+    ) => Unsubscribe,
+  |}>,
+): React$ComponentType<{
+  children: (value: Value | void) => React$Node,
+  source: Property,
+}> {
+  const {getCurrentValue, subscribe} = config;
+
+  warning(
+    typeof getCurrentValue === 'function',
+    'Subscription must specify a getCurrentValue function',
+  );
+  warning(
+    typeof subscribe === 'function',
+    'Subscription must specify a subscribe function',
+  );
+
+  type Props = {
+    children: (value: Value) => React$Element<any>,
+    source: Property,
+  };
+  type State = {
+    source: Property,
+    unsubscribeContainer: {
+      unsubscribe: Unsubscribe | null,
+    },
+    value: Value | void,
+  };
+
+  // Reference: https://gist.github.com/bvaughn/d569177d70b50b58bff69c3c4a5353f3
+  class Subscription extends React.Component<Props, State> {
+    state: State = {
+      source: this.props.source,
+      unsubscribeContainer: {
+        unsubscribe: null,
+      },
+      value:
+        this.props.source != null
+          ? getCurrentValue(this.props.source)
+          : undefined,
+    };
+
+    static getDerivedStateFromProps(nextProps, prevState) {
+      if (nextProps.source !== prevState.source) {
+        return {
+          source: nextProps.source,
+          unsubscribeContainer: {
+            unsubscribe: null,
+          },
+          value:
+            nextProps.source != null
+              ? getCurrentValue(nextProps.source)
+              : undefined,
+        };
+      }
+
+      return null;
+    }
+
+    componentDidMount() {
+      this.subscribe();
+    }
+
+    componentDidUpdate(prevProps, prevState) {
+      if (this.state.source !== prevState.source) {
+        this.unsubscribe(prevState);
+        this.subscribe();
+      }
+    }
+
+    componentWillUnmount() {
+      this.unsubscribe(this.state);
+    }
+
+    render() {
+      return this.props.children(this.state.value);
+    }
+
+    subscribe() {
+      const {source} = this.state;
+      if (source != null) {
+        const callback = (value: Value | void) => {
+          this.setState(state => {
+            // If the value is the same, skip the unnecessary state update.
+            if (value === state.value) {
+              return null;
+            }
+
+            // If this event belongs to an old or uncommitted data source, ignore it.
+            if (source !== state.source) {
+              return null;
+            }
+
+            return {value};
+          });
+        };
+
+        // 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.
+        const unsubscribe = subscribe(source, callback);
+
+        invariant(
+          typeof unsubscribe === 'function',
+          'A subscription must return an unsubscribe function.',
+        );
+
+        this.state.unsubscribeContainer.unsubscribe = unsubscribe;
+
+        // External values could change between render and mount,
+        // In some cases it may be important to handle this case.
+        const value = getCurrentValue(this.props.source);
+        if (value !== this.state.value) {
+          this.setState({value});
+        }
+      }
+    }
+
+    unsubscribe(state: State) {
+      const {unsubscribe} = state.unsubscribeContainer;
+      if (typeof unsubscribe === 'function') {
+        unsubscribe();
+      }
+    }
+  }
+
+  return Subscription;
+}
diff --git a/packages/react-dom/src/__tests__/ReactComponent-test.js b/packages/react-dom/src/__tests__/ReactComponent-test.js
index d08ba34a27db8..9c2d6447ff56b 100644
--- a/packages/react-dom/src/__tests__/ReactComponent-test.js
+++ b/packages/react-dom/src/__tests__/ReactComponent-test.js
@@ -235,8 +235,8 @@ describe('ReactComponent', () => {
       }
 
       componentDidMount() {
-        expect(this.innerRef.value.getObject()).toEqual(innerObj);
-        expect(this.outerRef.value.getObject()).toEqual(outerObj);
+        expect(this.innerRef.current.getObject()).toEqual(innerObj);
+        expect(this.outerRef.current.getObject()).toEqual(outerObj);
         mounted = true;
       }
     }
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationRefs-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationRefs-test.js
index 974d69497d8c0..37a83b2ff2581 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationRefs-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationRefs-test.js
@@ -96,4 +96,25 @@ describe('ReactDOMServerIntegration', () => {
       expect(component.refs.myDiv).toBe(root.firstChild);
     });
   });
+
+  it('should forward refs', async () => {
+    const divRef = React.createRef();
+
+    class InnerComponent extends React.Component {
+      render() {
+        return <div ref={this.props.forwardedRef}>{this.props.value}</div>;
+      }
+    }
+
+    const OuterComponent = React.forwardRef((props, ref) => (
+      <InnerComponent {...props} forwardedRef={ref} />
+    ));
+
+    await clientRenderOnServerString(
+      <OuterComponent ref={divRef} value="hello" />,
+    );
+
+    expect(divRef.current).not.toBe(null);
+    expect(divRef.current.textContent).toBe('hello');
+  });
 });
diff --git a/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js b/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
index defcd6228bdc4..bec420aeabc97 100644
--- a/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
@@ -1019,12 +1019,14 @@ describe('ReactErrorBoundaries', () => {
       'ErrorBoundary render error',
       'ErrorBoundary componentDidUpdate',
     ]);
-    expect(errorMessageRef.value.toString()).toEqual('[object HTMLDivElement]');
+    expect(errorMessageRef.current.toString()).toEqual(
+      '[object HTMLDivElement]',
+    );
 
     log.length = 0;
     ReactDOM.unmountComponentAtNode(container);
     expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
-    expect(errorMessageRef.value).toEqual(null);
+    expect(errorMessageRef.current).toEqual(null);
   });
 
   it('successfully mounts if no error occurs', () => {
diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js
index b7742df2f6b49..1cede6e411785 100644
--- a/packages/react-dom/src/server/ReactPartialRenderer.js
+++ b/packages/react-dom/src/server/ReactPartialRenderer.js
@@ -27,6 +27,7 @@ import describeComponentFrame from 'shared/describeComponentFrame';
 import {ReactDebugCurrentFrame} from 'shared/ReactGlobalSharedState';
 import {warnAboutDeprecatedLifecycles} from 'shared/ReactFeatureFlags';
 import {
+  REACT_FORWARD_REF_TYPE,
   REACT_FRAGMENT_TYPE,
   REACT_STRICT_MODE_TYPE,
   REACT_ASYNC_MODE_TYPE,
@@ -841,6 +842,25 @@ class ReactDOMServerRenderer {
       }
       if (typeof elementType === 'object' && elementType !== null) {
         switch (elementType.$$typeof) {
+          case REACT_FORWARD_REF_TYPE: {
+            const element: ReactElement = ((nextChild: any): ReactElement);
+            const nextChildren = toArray(
+              elementType.render(element.props, element.ref),
+            );
+            const frame: Frame = {
+              type: null,
+              domNamespace: parentNamespace,
+              children: nextChildren,
+              childIndex: 0,
+              context: context,
+              footer: '',
+            };
+            if (__DEV__) {
+              ((frame: any): FrameDev).debugElementStack = [];
+            }
+            this.stack.push(frame);
+            return '';
+          }
           case REACT_PROVIDER_TYPE: {
             const provider: ReactProvider<any> = (nextChild: any);
             const nextProps = provider.props;
diff --git a/packages/react-is/src/ReactIs.js b/packages/react-is/src/ReactIs.js
index d26bc82f5e8c1..0265055e020de 100644
--- a/packages/react-is/src/ReactIs.js
+++ b/packages/react-is/src/ReactIs.js
@@ -13,6 +13,7 @@ import {
   REACT_ASYNC_MODE_TYPE,
   REACT_CONTEXT_TYPE,
   REACT_ELEMENT_TYPE,
+  REACT_FORWARD_REF_TYPE,
   REACT_FRAGMENT_TYPE,
   REACT_PORTAL_TYPE,
   REACT_PROVIDER_TYPE,
@@ -37,6 +38,7 @@ export function typeOf(object: any) {
 
             switch ($$typeofType) {
               case REACT_CONTEXT_TYPE:
+              case REACT_FORWARD_REF_TYPE:
               case REACT_PROVIDER_TYPE:
                 return $$typeofType;
               default:
@@ -55,6 +57,7 @@ export const AsyncMode = REACT_ASYNC_MODE_TYPE;
 export const ContextConsumer = REACT_CONTEXT_TYPE;
 export const ContextProvider = REACT_PROVIDER_TYPE;
 export const Element = REACT_ELEMENT_TYPE;
+export const ForwardRef = REACT_FORWARD_REF_TYPE;
 export const Fragment = REACT_FRAGMENT_TYPE;
 export const Portal = REACT_PORTAL_TYPE;
 export const StrictMode = REACT_STRICT_MODE_TYPE;
@@ -75,6 +78,9 @@ export function isElement(object: any) {
     object.$$typeof === REACT_ELEMENT_TYPE
   );
 }
+export function isForwardRef(object: any) {
+  return typeOf(object) === REACT_FORWARD_REF_TYPE;
+}
 export function isFragment(object: any) {
   return typeOf(object) === REACT_FRAGMENT_TYPE;
 }
diff --git a/packages/react-is/src/__tests__/ReactIs-test.js b/packages/react-is/src/__tests__/ReactIs-test.js
index 6200388fd525d..ab7f6e0d4c24f 100644
--- a/packages/react-is/src/__tests__/ReactIs-test.js
+++ b/packages/react-is/src/__tests__/ReactIs-test.js
@@ -76,6 +76,15 @@ describe('ReactIs', () => {
     expect(ReactIs.isElement(<React.StrictMode />)).toBe(true);
   });
 
+  it('should identify ref forwarding component', () => {
+    const RefForwardingComponent = React.forwardRef((props, ref) => null);
+    expect(ReactIs.typeOf(<RefForwardingComponent />)).toBe(ReactIs.ForwardRef);
+    expect(ReactIs.isForwardRef(<RefForwardingComponent />)).toBe(true);
+    expect(ReactIs.isForwardRef({type: ReactIs.StrictMode})).toBe(false);
+    expect(ReactIs.isForwardRef(<React.unstable_AsyncMode />)).toBe(false);
+    expect(ReactIs.isForwardRef(<div />)).toBe(false);
+  });
+
   it('should identify fragments', () => {
     expect(ReactIs.typeOf(<React.Fragment />)).toBe(ReactIs.Fragment);
     expect(ReactIs.isFragment(<React.Fragment />)).toBe(true);
diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js
index eea42369de7ac..6c88c0aa18b02 100644
--- a/packages/react-reconciler/src/ReactFiber.js
+++ b/packages/react-reconciler/src/ReactFiber.js
@@ -25,6 +25,7 @@ import {
   HostPortal,
   CallComponent,
   ReturnComponent,
+  ForwardRef,
   Fragment,
   Mode,
   ContextProvider,
@@ -35,6 +36,7 @@ import getComponentName from 'shared/getComponentName';
 import {NoWork} from './ReactFiberExpirationTime';
 import {NoContext, AsyncMode, StrictMode} from './ReactTypeOfMode';
 import {
+  REACT_FORWARD_REF_TYPE,
   REACT_FRAGMENT_TYPE,
   REACT_RETURN_TYPE,
   REACT_CALL_TYPE,
@@ -357,6 +359,9 @@ export function createFiberFromElement(
               // This is a consumer
               fiberTag = ContextConsumer;
               break;
+            case REACT_FORWARD_REF_TYPE:
+              fiberTag = ForwardRef;
+              break;
             default:
               if (typeof type.tag === 'number') {
                 // Currently assumed to be a continuation and therefore is a
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index 95914814bc1e0..039fe24032e76 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -26,6 +26,7 @@ import {
   CallComponent,
   CallHandlerPhase,
   ReturnComponent,
+  ForwardRef,
   Fragment,
   Mode,
   ContextProvider,
@@ -153,6 +154,17 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
     }
   }
 
+  function updateForwardRef(current, workInProgress) {
+    const render = workInProgress.type.render;
+    const nextChildren = render(
+      workInProgress.pendingProps,
+      workInProgress.ref,
+    );
+    reconcileChildren(current, workInProgress, nextChildren);
+    memoizeProps(workInProgress, nextChildren);
+    return workInProgress.child;
+  }
+
   function updateFragment(current, workInProgress) {
     const nextChildren = workInProgress.pendingProps;
     if (hasLegacyContextChanged()) {
@@ -1130,6 +1142,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
           workInProgress,
           renderExpirationTime,
         );
+      case ForwardRef:
+        return updateForwardRef(current, workInProgress);
       case Fragment:
         return updateFragment(current, workInProgress);
       case Mode:
diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js
index 6d682cd2329a6..bf8abba92d2f8 100644
--- a/packages/react-reconciler/src/ReactFiberClassComponent.js
+++ b/packages/react-reconciler/src/ReactFiberClassComponent.js
@@ -789,20 +789,20 @@ export default function(
       // In order to support react-lifecycles-compat polyfilled components,
       // Unsafe lifecycles should not be invoked for any component with the new gDSFP.
       if (
-        (typeof instance.UNSAFE_componentWillUpdate === 'function' ||
-          typeof instance.componentWillUpdate === 'function') &&
+        (typeof instance.UNSAFE_componentWillMount === 'function' ||
+          typeof instance.componentWillMount === 'function') &&
         typeof ctor.getDerivedStateFromProps !== 'function'
       ) {
-        startPhaseTimer(workInProgress, 'componentWillUpdate');
-        if (typeof instance.componentWillUpdate === 'function') {
-          instance.componentWillUpdate(newProps, newState, newContext);
+        startPhaseTimer(workInProgress, 'componentWillMount');
+        if (typeof instance.componentWillMount === 'function') {
+          instance.componentWillMount();
         }
-        if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
-          instance.UNSAFE_componentWillUpdate(newProps, newState, newContext);
+        if (typeof instance.UNSAFE_componentWillMount === 'function') {
+          instance.UNSAFE_componentWillMount();
         }
         stopPhaseTimer();
       }
-      if (typeof instance.componentDidUpdate === 'function') {
+      if (typeof instance.componentDidMount === 'function') {
         workInProgress.effectTag |= Update;
       }
     } else {
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js
index 831a391d42ded..cd688556434cc 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.js
@@ -29,6 +29,7 @@ import {
 import ReactErrorUtils from 'shared/ReactErrorUtils';
 import {Placement, Update, ContentReset} from 'shared/ReactTypeOfSideEffect';
 import invariant from 'fbjs/lib/invariant';
+import warning from 'fbjs/lib/warning';
 
 import {commitCallbacks} from './ReactFiberUpdateQueue';
 import {onCommitUnmount} from './ReactFiberDevToolsHook';
@@ -147,7 +148,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
           }
         }
       } else {
-        ref.value = null;
+        ref.current = null;
       }
     }
   }
@@ -315,7 +316,19 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
       if (typeof ref === 'function') {
         ref(instanceToUse);
       } else {
-        ref.value = instanceToUse;
+        if (__DEV__) {
+          if (!ref.hasOwnProperty('current')) {
+            warning(
+              false,
+              'Unexpected ref object provided for %s. ' +
+                'Use either a ref-setter function or Reacte.createRef().%s',
+              getComponentName(finishedWork),
+              getStackAddendumByWorkInProgressFiber(finishedWork),
+            );
+          }
+        }
+
+        ref.current = instanceToUse;
       }
     }
   }
@@ -326,7 +339,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
       if (typeof currentRef === 'function') {
         currentRef(null);
       } else {
-        currentRef.value = null;
+        currentRef.current = null;
       }
     }
   }
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js
index 9e796dd53c152..3fb99ef2c2767 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js
@@ -32,6 +32,7 @@ import {
   ReturnComponent,
   ContextProvider,
   ContextConsumer,
+  ForwardRef,
   Fragment,
   Mode,
 } from 'shared/ReactTypeOfWork';
@@ -603,6 +604,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
       case ReturnComponent:
         // Does nothing.
         return null;
+      case ForwardRef:
+        return null;
       case Fragment:
         return null;
       case Mode:
diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
index ef44ce3bb11b0..cc45510c43883 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
@@ -1259,4 +1259,47 @@ describe('ReactIncrementalErrorHandling', () => {
     ]);
     expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops!')]);
   });
+
+  it('calls the correct lifecycles on the error boundary after catching an error (mixed)', () => {
+    // This test seems a bit contrived, but it's based on an actual regression
+    // where we checked for the existence of didUpdate instead of didMount, and
+    // didMount was not defined.
+    function BadRender() {
+      ReactNoop.yield('throw');
+      throw new Error('oops!');
+    }
+
+    let parent;
+    class Parent extends React.Component {
+      state = {error: null, other: false};
+      componentDidCatch(error) {
+        ReactNoop.yield('did catch');
+        this.setState({error});
+      }
+      componentDidUpdate() {
+        ReactNoop.yield('did update');
+      }
+      render() {
+        parent = this;
+        if (this.state.error) {
+          ReactNoop.yield('render error message');
+          return <span prop={`Caught an error: ${this.state.error.message}`} />;
+        }
+        ReactNoop.yield('render');
+        return <BadRender />;
+      }
+    }
+
+    ReactNoop.render(<Parent step={1} />);
+    ReactNoop.flushThrough(['render', 'throw']);
+    expect(ReactNoop.getChildren()).toEqual([]);
+
+    parent.setState({other: true});
+    expect(ReactNoop.flush()).toEqual([
+      'did catch',
+      'render error message',
+      'did update',
+    ]);
+    expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops!')]);
+  });
 });
diff --git a/packages/react/src/React.js b/packages/react/src/React.js
index a2feefed097f8..d3a52cf61c406 100644
--- a/packages/react/src/React.js
+++ b/packages/react/src/React.js
@@ -24,6 +24,7 @@ import {
   isValidElement,
 } from './ReactElement';
 import {createContext} from './ReactContext';
+import forwardRef from './forwardRef';
 import {
   createElementWithValidation,
   createFactoryWithValidation,
@@ -45,6 +46,7 @@ const React = {
   PureComponent,
 
   createContext,
+  forwardRef,
 
   Fragment: REACT_FRAGMENT_TYPE,
   StrictMode: REACT_STRICT_MODE_TYPE,
diff --git a/packages/react/src/ReactCreateRef.js b/packages/react/src/ReactCreateRef.js
index 8af60100e64d7..326caddae9324 100644
--- a/packages/react/src/ReactCreateRef.js
+++ b/packages/react/src/ReactCreateRef.js
@@ -11,7 +11,7 @@ import type {RefObject} from 'shared/ReactTypes';
 // an immutable object with a single mutable value
 export function createRef(): RefObject {
   const refObject = {
-    value: null,
+    current: null,
   };
   if (__DEV__) {
     Object.seal(refObject);
diff --git a/packages/react/src/ReactElementValidator.js b/packages/react/src/ReactElementValidator.js
index 471e6d6f8b677..fff09423d3e04 100644
--- a/packages/react/src/ReactElementValidator.js
+++ b/packages/react/src/ReactElementValidator.js
@@ -22,6 +22,7 @@ import {
   REACT_ASYNC_MODE_TYPE,
   REACT_PROVIDER_TYPE,
   REACT_CONTEXT_TYPE,
+  REACT_FORWARD_REF_TYPE,
 } from 'shared/ReactSymbols';
 import checkPropTypes from 'prop-types/checkPropTypes';
 import warning from 'fbjs/lib/warning';
@@ -297,7 +298,8 @@ export function createElementWithValidation(type, props, children) {
     (typeof type === 'object' &&
       type !== null &&
       (type.$$typeof === REACT_PROVIDER_TYPE ||
-        type.$$typeof === REACT_CONTEXT_TYPE));
+        type.$$typeof === REACT_CONTEXT_TYPE ||
+        type.$$typeof === REACT_FORWARD_REF_TYPE));
 
   // We warn in this case but don't throw. We expect the element creation to
   // succeed and there will likely be errors in render.
diff --git a/packages/react/src/__tests__/ReactCreateRef-test.js b/packages/react/src/__tests__/ReactCreateRef-test.js
new file mode 100644
index 0000000000000..683d3ddf01027
--- /dev/null
+++ b/packages/react/src/__tests__/ReactCreateRef-test.js
@@ -0,0 +1,60 @@
+/**
+ * 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 React;
+let ReactTestRenderer;
+
+describe('ReactCreateRef', () => {
+  beforeEach(() => {
+    jest.resetModules();
+
+    React = require('react');
+    ReactTestRenderer = require('react-test-renderer');
+  });
+
+  it('should warn in dev if an invalid ref object is provided', () => {
+    function Wrapper({children}) {
+      return children;
+    }
+
+    class ExampleComponent extends React.Component {
+      render() {
+        return null;
+      }
+    }
+
+    expect(() =>
+      ReactTestRenderer.create(
+        <Wrapper>
+          <div ref={{}} />
+        </Wrapper>,
+      ),
+    ).toWarnDev(
+      'Unexpected ref object provided for div. ' +
+        'Use either a ref-setter function or Reacte.createRef().\n' +
+        '    in div (at **)\n' +
+        '    in Wrapper (at **)',
+    );
+
+    expect(() =>
+      ReactTestRenderer.create(
+        <Wrapper>
+          <ExampleComponent ref={{}} />
+        </Wrapper>,
+      ),
+    ).toWarnDev(
+      'Unexpected ref object provided for ExampleComponent. ' +
+        'Use either a ref-setter function or Reacte.createRef().\n' +
+        '    in ExampleComponent (at **)\n' +
+        '    in Wrapper (at **)',
+    );
+  });
+});
diff --git a/packages/react/src/__tests__/forwardRef-test.internal.js b/packages/react/src/__tests__/forwardRef-test.internal.js
new file mode 100644
index 0000000000000..068448c437400
--- /dev/null
+++ b/packages/react/src/__tests__/forwardRef-test.internal.js
@@ -0,0 +1,252 @@
+/**
+ * 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';
+
+describe('forwardRef', () => {
+  let React;
+  let ReactFeatureFlags;
+  let ReactNoop;
+
+  beforeEach(() => {
+    jest.resetModules();
+    ReactFeatureFlags = require('shared/ReactFeatureFlags');
+    ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
+    React = require('react');
+    ReactNoop = require('react-noop-renderer');
+  });
+
+  it('should work without a ref to be forwarded', () => {
+    class Child extends React.Component {
+      render() {
+        ReactNoop.yield(this.props.value);
+        return null;
+      }
+    }
+
+    function Wrapper(props) {
+      return <Child {...props} ref={props.forwardedRef} />;
+    }
+
+    const RefForwardingComponent = React.forwardRef((props, ref) => (
+      <Wrapper {...props} forwardedRef={ref} />
+    ));
+
+    ReactNoop.render(<RefForwardingComponent value={123} />);
+    expect(ReactNoop.flush()).toEqual([123]);
+  });
+
+  it('should forward a ref for a single child', () => {
+    class Child extends React.Component {
+      render() {
+        ReactNoop.yield(this.props.value);
+        return null;
+      }
+    }
+
+    function Wrapper(props) {
+      return <Child {...props} ref={props.forwardedRef} />;
+    }
+
+    const RefForwardingComponent = React.forwardRef((props, ref) => (
+      <Wrapper {...props} forwardedRef={ref} />
+    ));
+
+    const ref = React.createRef();
+
+    ReactNoop.render(<RefForwardingComponent ref={ref} value={123} />);
+    expect(ReactNoop.flush()).toEqual([123]);
+    expect(ref.current instanceof Child).toBe(true);
+  });
+
+  it('should forward a ref for multiple children', () => {
+    class Child extends React.Component {
+      render() {
+        ReactNoop.yield(this.props.value);
+        return null;
+      }
+    }
+
+    function Wrapper(props) {
+      return <Child {...props} ref={props.forwardedRef} />;
+    }
+
+    const RefForwardingComponent = React.forwardRef((props, ref) => (
+      <Wrapper {...props} forwardedRef={ref} />
+    ));
+
+    const ref = React.createRef();
+
+    ReactNoop.render(
+      <div>
+        <div />
+        <RefForwardingComponent ref={ref} value={123} />
+        <div />
+      </div>,
+    );
+    expect(ReactNoop.flush()).toEqual([123]);
+    expect(ref.current instanceof Child).toBe(true);
+  });
+
+  it('should update refs when switching between children', () => {
+    function FunctionalComponent({forwardedRef, setRefOnDiv}) {
+      return (
+        <section>
+          <div ref={setRefOnDiv ? forwardedRef : null}>First</div>
+          <span ref={setRefOnDiv ? null : forwardedRef}>Second</span>
+        </section>
+      );
+    }
+
+    const RefForwardingComponent = React.forwardRef((props, ref) => (
+      <FunctionalComponent {...props} forwardedRef={ref} />
+    ));
+
+    const ref = React.createRef();
+
+    ReactNoop.render(<RefForwardingComponent ref={ref} setRefOnDiv={true} />);
+    ReactNoop.flush();
+    expect(ref.current.type).toBe('div');
+
+    ReactNoop.render(<RefForwardingComponent ref={ref} setRefOnDiv={false} />);
+    ReactNoop.flush();
+    expect(ref.current.type).toBe('span');
+  });
+
+  it('should maintain child instance and ref through updates', () => {
+    class Child extends React.Component {
+      constructor(props) {
+        super(props);
+      }
+      render() {
+        ReactNoop.yield(this.props.value);
+        return null;
+      }
+    }
+
+    function Wrapper(props) {
+      return <Child {...props} ref={props.forwardedRef} />;
+    }
+
+    const RefForwardingComponent = React.forwardRef((props, ref) => (
+      <Wrapper {...props} forwardedRef={ref} />
+    ));
+
+    let setRefCount = 0;
+    let ref;
+
+    const setRef = r => {
+      setRefCount++;
+      ref = r;
+    };
+
+    ReactNoop.render(<RefForwardingComponent ref={setRef} value={123} />);
+    expect(ReactNoop.flush()).toEqual([123]);
+    expect(ref instanceof Child).toBe(true);
+    expect(setRefCount).toBe(1);
+    ReactNoop.render(<RefForwardingComponent ref={setRef} value={456} />);
+    expect(ReactNoop.flush()).toEqual([456]);
+    expect(ref instanceof Child).toBe(true);
+    expect(setRefCount).toBe(1);
+  });
+
+  it('should not break lifecycle error handling', () => {
+    class ErrorBoundary extends React.Component {
+      state = {error: null};
+      componentDidCatch(error) {
+        ReactNoop.yield('ErrorBoundary.componentDidCatch');
+        this.setState({error});
+      }
+      render() {
+        if (this.state.error) {
+          ReactNoop.yield('ErrorBoundary.render: catch');
+          return null;
+        }
+        ReactNoop.yield('ErrorBoundary.render: try');
+        return this.props.children;
+      }
+    }
+
+    class BadRender extends React.Component {
+      render() {
+        ReactNoop.yield('BadRender throw');
+        throw new Error('oops!');
+      }
+    }
+
+    function Wrapper(props) {
+      ReactNoop.yield('Wrapper');
+      return <BadRender {...props} ref={props.forwardedRef} />;
+    }
+
+    const RefForwardingComponent = React.forwardRef((props, ref) => (
+      <Wrapper {...props} forwardedRef={ref} />
+    ));
+
+    const ref = React.createRef();
+
+    ReactNoop.render(
+      <ErrorBoundary>
+        <RefForwardingComponent ref={ref} />
+      </ErrorBoundary>,
+    );
+    expect(ReactNoop.flush()).toEqual([
+      'ErrorBoundary.render: try',
+      'Wrapper',
+      'BadRender throw',
+      'ErrorBoundary.componentDidCatch',
+      'ErrorBoundary.render: catch',
+    ]);
+    expect(ref.current).toBe(null);
+  });
+
+  it('should support rendering null', () => {
+    const RefForwardingComponent = React.forwardRef((props, ref) => null);
+
+    const ref = React.createRef();
+
+    ReactNoop.render(<RefForwardingComponent ref={ref} />);
+    ReactNoop.flush();
+    expect(ref.current).toBe(null);
+  });
+
+  it('should support rendering null for multiple children', () => {
+    const RefForwardingComponent = React.forwardRef((props, ref) => null);
+
+    const ref = React.createRef();
+
+    ReactNoop.render(
+      <div>
+        <div />
+        <RefForwardingComponent ref={ref} />
+        <div />
+      </div>,
+    );
+    ReactNoop.flush();
+    expect(ref.current).toBe(null);
+  });
+
+  it('should warn if not provided a callback during creation', () => {
+    expect(() => React.forwardRef(undefined)).toWarnDev(
+      'forwardRef requires a render function but was given undefined.',
+    );
+    expect(() => React.forwardRef(null)).toWarnDev(
+      'forwardRef requires a render function but was given null.',
+    );
+    expect(() => React.forwardRef('foo')).toWarnDev(
+      'forwardRef requires a render function but was given string.',
+    );
+  });
+
+  it('should warn if no render function is provided', () => {
+    expect(React.forwardRef).toWarnDev(
+      'forwardRef requires a render function but was given undefined.',
+    );
+  });
+});
diff --git a/packages/react/src/forwardRef.js b/packages/react/src/forwardRef.js
new file mode 100644
index 0000000000000..6a923be12795d
--- /dev/null
+++ b/packages/react/src/forwardRef.js
@@ -0,0 +1,27 @@
+/**
+ * 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.
+ */
+
+import {REACT_FORWARD_REF_TYPE} from 'shared/ReactSymbols';
+
+import warning from 'fbjs/lib/warning';
+
+export default function forwardRef<Props, ElementType: React$ElementType>(
+  render: (props: Props, ref: React$ElementRef<ElementType>) => React$Node,
+) {
+  if (__DEV__) {
+    warning(
+      typeof render === 'function',
+      'forwardRef requires a render function but was given %s.',
+      render === null ? 'null' : typeof render,
+    );
+  }
+
+  return {
+    $$typeof: REACT_FORWARD_REF_TYPE,
+    render,
+  };
+}
diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js
index cc60e34e4c5b1..12e0fdddad9b9 100644
--- a/packages/shared/ReactSymbols.js
+++ b/packages/shared/ReactSymbols.js
@@ -36,6 +36,9 @@ export const REACT_CONTEXT_TYPE = hasSymbol
 export const REACT_ASYNC_MODE_TYPE = hasSymbol
   ? Symbol.for('react.async_mode')
   : 0xeacf;
+export const REACT_FORWARD_REF_TYPE = hasSymbol
+  ? Symbol.for('react.forward_ref')
+  : 0xead0;
 
 const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
 const FAUX_ITERATOR_SYMBOL = '@@iterator';
diff --git a/packages/shared/ReactTypeOfWork.js b/packages/shared/ReactTypeOfWork.js
index 3d1bfc3f37584..573b75aabc0f0 100644
--- a/packages/shared/ReactTypeOfWork.js
+++ b/packages/shared/ReactTypeOfWork.js
@@ -21,7 +21,8 @@ export type TypeOfWork =
   | 10
   | 11
   | 12
-  | 13;
+  | 13
+  | 14;
 
 export const IndeterminateComponent = 0; // Before we know whether it is functional or class
 export const FunctionalComponent = 1;
@@ -37,3 +38,4 @@ export const Fragment = 10;
 export const Mode = 11;
 export const ContextConsumer = 12;
 export const ContextProvider = 13;
+export const ForwardRef = 14;
diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js
index bede9f00e3006..f6a56ccc96ed4 100644
--- a/packages/shared/ReactTypes.js
+++ b/packages/shared/ReactTypes.js
@@ -101,5 +101,5 @@ export type ReactPortal = {
 };
 
 export type RefObject = {|
-  value: any,
+  current: any,
 |};
diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js
index 0be2375a58b9a..b9c5f57558a56 100644
--- a/scripts/rollup/bundles.js
+++ b/scripts/rollup/bundles.js
@@ -254,6 +254,16 @@ const bundles = [
     global: 'SimpleCacheProvider',
     externals: ['react'],
   },
+
+  /******* createComponentWithSubscriptions (experimental) *******/
+  {
+    label: 'create-subscription',
+    bundleTypes: [NODE_DEV, NODE_PROD],
+    moduleType: ISOMORPHIC,
+    entry: 'create-subscription',
+    global: 'createSubscription',
+    externals: ['react'],
+  },
 ];
 
 // Based on deep-freeze by substack (public domain)
diff --git a/scripts/rollup/results.json b/scripts/rollup/results.json
index 77a21c1b12d68..cd88e85620eae 100644
--- a/scripts/rollup/results.json
+++ b/scripts/rollup/results.json
@@ -4,8 +4,8 @@
       "filename": "react.development.js",
       "bundleType": "UMD_DEV",
       "packageName": "react",
-      "size": 55674,
-      "gzip": 15255
+      "size": 55675,
+      "gzip": 15253
     },
     {
       "filename": "react.production.min.js",
@@ -18,8 +18,8 @@
       "filename": "react.development.js",
       "bundleType": "NODE_DEV",
       "packageName": "react",
-      "size": 46095,
-      "gzip": 12925
+      "size": 46096,
+      "gzip": 12924
     },
     {
       "filename": "react.production.min.js",
@@ -32,8 +32,8 @@
       "filename": "React-dev.js",
       "bundleType": "FB_DEV",
       "packageName": "react",
-      "size": 45476,
-      "gzip": 12448
+      "size": 45477,
+      "gzip": 12446
     },
     {
       "filename": "React-prod.js",
@@ -46,50 +46,50 @@
       "filename": "react-dom.development.js",
       "bundleType": "UMD_DEV",
       "packageName": "react-dom",
-      "size": 591513,
-      "gzip": 138743
+      "size": 600642,
+      "gzip": 139543
     },
     {
       "filename": "react-dom.production.min.js",
       "bundleType": "UMD_PROD",
       "packageName": "react-dom",
-      "size": 96778,
-      "gzip": 31445
+      "size": 100738,
+      "gzip": 32495
     },
     {
       "filename": "react-dom.development.js",
       "bundleType": "NODE_DEV",
       "packageName": "react-dom",
-      "size": 575526,
-      "gzip": 134516
+      "size": 584651,
+      "gzip": 135289
     },
     {
       "filename": "react-dom.production.min.js",
       "bundleType": "NODE_PROD",
       "packageName": "react-dom",
-      "size": 95503,
-      "gzip": 30619
+      "size": 99167,
+      "gzip": 31568
     },
     {
       "filename": "ReactDOM-dev.js",
       "bundleType": "FB_DEV",
       "packageName": "react-dom",
-      "size": 594783,
-      "gzip": 136782
+      "size": 604987,
+      "gzip": 137591
     },
     {
       "filename": "ReactDOM-prod.js",
       "bundleType": "FB_PROD",
       "packageName": "react-dom",
-      "size": 279046,
-      "gzip": 53062
+      "size": 290412,
+      "gzip": 54502
     },
     {
       "filename": "react-dom-test-utils.development.js",
       "bundleType": "UMD_DEV",
       "packageName": "react-dom",
-      "size": 41697,
-      "gzip": 11964
+      "size": 41803,
+      "gzip": 12011
     },
     {
       "filename": "react-dom-test-utils.production.min.js",
@@ -102,8 +102,8 @@
       "filename": "react-dom-test-utils.development.js",
       "bundleType": "NODE_DEV",
       "packageName": "react-dom",
-      "size": 36434,
-      "gzip": 10505
+      "size": 36540,
+      "gzip": 10554
     },
     {
       "filename": "react-dom-test-utils.production.min.js",
@@ -116,8 +116,8 @@
       "filename": "ReactTestUtils-dev.js",
       "bundleType": "FB_DEV",
       "packageName": "react-dom",
-      "size": 37155,
-      "gzip": 10582
+      "size": 37255,
+      "gzip": 10630
     },
     {
       "filename": "react-dom-unstable-native-dependencies.development.js",
@@ -165,141 +165,141 @@
       "filename": "react-dom-server.browser.development.js",
       "bundleType": "UMD_DEV",
       "packageName": "react-dom",
-      "size": 102991,
-      "gzip": 26927
+      "size": 103067,
+      "gzip": 27041
     },
     {
       "filename": "react-dom-server.browser.production.min.js",
       "bundleType": "UMD_PROD",
       "packageName": "react-dom",
-      "size": 15184,
-      "gzip": 5856
+      "size": 15133,
+      "gzip": 5835
     },
     {
       "filename": "react-dom-server.browser.development.js",
       "bundleType": "NODE_DEV",
       "packageName": "react-dom",
-      "size": 92035,
-      "gzip": 24618
+      "size": 92111,
+      "gzip": 24739
     },
     {
       "filename": "react-dom-server.browser.production.min.js",
       "bundleType": "NODE_PROD",
       "packageName": "react-dom",
-      "size": 14818,
-      "gzip": 5705
+      "size": 14771,
+      "gzip": 5680
     },
     {
       "filename": "ReactDOMServer-dev.js",
       "bundleType": "FB_DEV",
       "packageName": "react-dom",
-      "size": 95165,
-      "gzip": 24327
+      "size": 95191,
+      "gzip": 24410
     },
     {
       "filename": "ReactDOMServer-prod.js",
       "bundleType": "FB_PROD",
       "packageName": "react-dom",
-      "size": 33262,
-      "gzip": 8299
+      "size": 33064,
+      "gzip": 8279
     },
     {
       "filename": "react-dom-server.node.development.js",
       "bundleType": "NODE_DEV",
       "packageName": "react-dom",
-      "size": 94003,
-      "gzip": 25175
+      "size": 94079,
+      "gzip": 25295
     },
     {
       "filename": "react-dom-server.node.production.min.js",
       "bundleType": "NODE_PROD",
       "packageName": "react-dom",
-      "size": 15642,
-      "gzip": 6010
+      "size": 15595,
+      "gzip": 5990
     },
     {
       "filename": "react-art.development.js",
       "bundleType": "UMD_DEV",
       "packageName": "react-art",
-      "size": 389869,
-      "gzip": 86413
+      "size": 399001,
+      "gzip": 87190
     },
     {
       "filename": "react-art.production.min.js",
       "bundleType": "UMD_PROD",
       "packageName": "react-art",
-      "size": 86808,
-      "gzip": 26944
+      "size": 90690,
+      "gzip": 27874
     },
     {
       "filename": "react-art.development.js",
       "bundleType": "NODE_DEV",
       "packageName": "react-art",
-      "size": 313942,
-      "gzip": 67385
+      "size": 323070,
+      "gzip": 68147
     },
     {
       "filename": "react-art.production.min.js",
       "bundleType": "NODE_PROD",
       "packageName": "react-art",
-      "size": 50754,
-      "gzip": 16005
+      "size": 54355,
+      "gzip": 16860
     },
     {
       "filename": "ReactART-dev.js",
       "bundleType": "FB_DEV",
       "packageName": "react-art",
-      "size": 318024,
-      "gzip": 66603
+      "size": 328230,
+      "gzip": 67375
     },
     {
       "filename": "ReactART-prod.js",
       "bundleType": "FB_PROD",
       "packageName": "react-art",
-      "size": 157473,
-      "gzip": 27225
+      "size": 168749,
+      "gzip": 28640
     },
     {
       "filename": "ReactNativeRenderer-dev.js",
       "bundleType": "RN_DEV",
       "packageName": "react-native-renderer",
-      "size": 443941,
-      "gzip": 97414
+      "size": 454044,
+      "gzip": 98203
     },
     {
       "filename": "ReactNativeRenderer-prod.js",
       "bundleType": "RN_PROD",
       "packageName": "react-native-renderer",
-      "size": 209855,
-      "gzip": 36492
+      "size": 220436,
+      "gzip": 37780
     },
     {
       "filename": "react-test-renderer.development.js",
       "bundleType": "NODE_DEV",
       "packageName": "react-test-renderer",
-      "size": 310910,
-      "gzip": 66329
+      "size": 320190,
+      "gzip": 67115
     },
     {
       "filename": "react-test-renderer.production.min.js",
       "bundleType": "NODE_PROD",
       "packageName": "react-test-renderer",
-      "size": 49219,
-      "gzip": 15315
+      "size": 52870,
+      "gzip": 16241
     },
     {
       "filename": "ReactTestRenderer-dev.js",
       "bundleType": "FB_DEV",
       "packageName": "react-test-renderer",
-      "size": 315000,
-      "gzip": 65520
+      "size": 325364,
+      "gzip": 66316
     },
     {
       "filename": "react-test-renderer-shallow.development.js",
       "bundleType": "NODE_DEV",
       "packageName": "react-test-renderer",
-      "size": 21221,
-      "gzip": 5193
+      "size": 21475,
+      "gzip": 5309
     },
     {
       "filename": "react-test-renderer-shallow.production.min.js",
@@ -312,43 +312,43 @@
       "filename": "ReactShallowRenderer-dev.js",
       "bundleType": "FB_DEV",
       "packageName": "react-test-renderer",
-      "size": 20928,
-      "gzip": 4566
+      "size": 21120,
+      "gzip": 4625
     },
     {
       "filename": "react-noop-renderer.development.js",
       "bundleType": "NODE_DEV",
       "packageName": "react-noop-renderer",
-      "size": 18777,
-      "gzip": 5303
+      "size": 19408,
+      "gzip": 5482
     },
     {
       "filename": "react-noop-renderer.production.min.js",
       "bundleType": "NODE_PROD",
       "packageName": "react-noop-renderer",
-      "size": 6429,
-      "gzip": 2573
+      "size": 6643,
+      "gzip": 2618
     },
     {
       "filename": "react-reconciler.development.js",
       "bundleType": "NODE_DEV",
       "packageName": "react-reconciler",
-      "size": 292377,
-      "gzip": 61765
+      "size": 301505,
+      "gzip": 62567
     },
     {
       "filename": "react-reconciler.production.min.js",
       "bundleType": "NODE_PROD",
       "packageName": "react-reconciler",
-      "size": 42443,
-      "gzip": 13358
+      "size": 46055,
+      "gzip": 14278
     },
     {
       "filename": "react-reconciler-reflection.development.js",
       "bundleType": "NODE_DEV",
       "packageName": "react-reconciler",
-      "size": 10934,
-      "gzip": 3388
+      "size": 11040,
+      "gzip": 3435
     },
     {
       "filename": "react-reconciler-reflection.production.min.js",
@@ -375,29 +375,29 @@
       "filename": "ReactFabric-dev.js",
       "bundleType": "RN_DEV",
       "packageName": "react-native-renderer",
-      "size": 438218,
-      "gzip": 96267
+      "size": 438891,
+      "gzip": 94687
     },
     {
       "filename": "ReactFabric-prod.js",
       "bundleType": "RN_PROD",
       "packageName": "react-native-renderer",
-      "size": 201883,
-      "gzip": 35448
+      "size": 204481,
+      "gzip": 35139
     },
     {
       "filename": "react-reconciler-persistent.development.js",
       "bundleType": "NODE_DEV",
       "packageName": "react-reconciler",
-      "size": 291949,
-      "gzip": 61587
+      "size": 300825,
+      "gzip": 62279
     },
     {
       "filename": "react-reconciler-persistent.production.min.js",
       "bundleType": "NODE_PROD",
       "packageName": "react-reconciler",
-      "size": 41327,
-      "gzip": 13133
+      "size": 44927,
+      "gzip": 14054
     },
     {
       "filename": "react-is.development.js",
@@ -431,15 +431,43 @@
       "filename": "simple-cache-provider.development.js",
       "bundleType": "NODE_DEV",
       "packageName": "simple-cache-provider",
-      "size": 5830,
-      "gzip": 1904
+      "size": 5759,
+      "gzip": 1870
     },
     {
       "filename": "simple-cache-provider.production.min.js",
       "bundleType": "NODE_PROD",
       "packageName": "simple-cache-provider",
-      "size": 1313,
-      "gzip": 665
+      "size": 1295,
+      "gzip": 656
+    },
+    {
+      "filename": "create-component-with-subscriptions.development.js",
+      "bundleType": "NODE_DEV",
+      "packageName": "create-component-with-subscriptions",
+      "size": 9931,
+      "gzip": 3067
+    },
+    {
+      "filename": "create-component-with-subscriptions.production.min.js",
+      "bundleType": "NODE_PROD",
+      "packageName": "create-component-with-subscriptions",
+      "size": 3783,
+      "gzip": 1637
+    },
+    {
+      "filename": "create-subscription.development.js",
+      "bundleType": "NODE_DEV",
+      "packageName": "create-subscription",
+      "size": 5491,
+      "gzip": 1896
+    },
+    {
+      "filename": "create-subscription.production.min.js",
+      "bundleType": "NODE_PROD",
+      "packageName": "create-subscription",
+      "size": 2190,
+      "gzip": 1007
     }
   ]
 }
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 5f1e2ce632188..2846df0a248db 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4897,6 +4897,12 @@ rx-lite@*, rx-lite@^4.0.8:
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
 
+rxjs@^5.5.6:
+  version "5.5.6"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.6.tgz#e31fb96d6fd2ff1fd84bcea8ae9c02d007179c02"
+  dependencies:
+    symbol-observable "1.0.1"
+
 safe-buffer@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
@@ -5217,6 +5223,10 @@ supports-hyperlinks@^1.0.1:
     has-flag "^2.0.0"
     supports-color "^5.0.0"
 
+symbol-observable@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"
+
 symbol-tree@^3.2.1:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"