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
+
+ {value => }
+
+```
+
+# 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
You have {followersCount} followers!
;
+}
+
+// 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.
+
+ {value => }
+;
+```
+
+## 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.
+
+ {loadingStatus => }
+
+```
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(
+
+ {(value = 'default') => {
+ ReactNoop.yield(value);
+ return null;
+ }}
+ ,
+ );
+
+ // 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();
+ 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({render});
+ expect(ReactNoop.flush()).toEqual(['initial']);
+ observable.next('updated');
+ expect(ReactNoop.flush()).toEqual(['updated']);
+
+ // Unsetting the subscriber prop should reset subscribed values
+ ReactNoop.render({render});
+ 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({render});
+ 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({render});
+ 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({render});
+ expect(ReactNoop.flush()).toEqual(['default']);
+ ReactNoop.render({render});
+ 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(
+ {render},
+ );
+
+ // 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(
+ {render},
+ );
+ 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 (
+
+ {(value = 'default') => {
+ ReactNoop.yield('Subscriber: ' + value);
+ return ;
+ }}
+
+ );
+ }
+ }
+
+ const observableA = createBehaviorSubject('a-0');
+ const observableB = createBehaviorSubject('b-0');
+
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['Subscriber: a-0', 'Child: a-0']);
+ expect(log).toEqual(['Parent.componentDidMount']);
+
+ // Start React update, but don't finish
+ ReactNoop.render();
+ 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 (
+
+ {(value = 'default') => {
+ ReactNoop.yield('Subscriber: ' + value);
+ return ;
+ }}
+
+ );
+ }
+ }
+
+ const observableA = createBehaviorSubject('a-0');
+ const observableB = createBehaviorSubject('b-0');
+
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['Subscriber: a-0', 'Child: a-0']);
+ expect(log).toEqual(['Parent.componentDidMount']);
+
+ // Start React update, but don't finish
+ ReactNoop.render();
+ 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(
+ {value => null},
+ );
+
+ 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(
+ 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,
+ 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 {
+ 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 {this.props.value}
;
+ }
+ }
+
+ const OuterComponent = React.forwardRef((props, ref) => (
+
+ ));
+
+ await clientRenderOnServerString(
+ ,
+ );
+
+ 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 = (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()).toBe(true);
});
+ it('should identify ref forwarding component', () => {
+ const RefForwardingComponent = React.forwardRef((props, ref) => null);
+ expect(ReactIs.typeOf()).toBe(ReactIs.ForwardRef);
+ expect(ReactIs.isForwardRef()).toBe(true);
+ expect(ReactIs.isForwardRef({type: ReactIs.StrictMode})).toBe(false);
+ expect(ReactIs.isForwardRef()).toBe(false);
+ expect(ReactIs.isForwardRef()).toBe(false);
+ });
+
it('should identify fragments', () => {
expect(ReactIs.typeOf()).toBe(ReactIs.Fragment);
expect(ReactIs.isFragment()).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(
}
}
+ 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(
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(
}
}
} else {
- ref.value = null;
+ ref.current = null;
}
}
}
@@ -315,7 +316,19 @@ export default function(
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(
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(
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 ;
+ }
+ ReactNoop.yield('render');
+ return ;
+ }
+ }
+
+ ReactNoop.render();
+ 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(
+
+
+ ,
+ ),
+ ).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(
+
+
+ ,
+ ),
+ ).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 ;
+ }
+
+ const RefForwardingComponent = React.forwardRef((props, ref) => (
+
+ ));
+
+ ReactNoop.render();
+ 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 ;
+ }
+
+ const RefForwardingComponent = React.forwardRef((props, ref) => (
+
+ ));
+
+ const ref = React.createRef();
+
+ ReactNoop.render();
+ 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 ;
+ }
+
+ const RefForwardingComponent = React.forwardRef((props, ref) => (
+
+ ));
+
+ const ref = React.createRef();
+
+ ReactNoop.render(
+ ,
+ );
+ 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 (
+
+ );
+ }
+
+ const RefForwardingComponent = React.forwardRef((props, ref) => (
+
+ ));
+
+ const ref = React.createRef();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+ expect(ref.current.type).toBe('div');
+
+ ReactNoop.render();
+ 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 ;
+ }
+
+ const RefForwardingComponent = React.forwardRef((props, ref) => (
+
+ ));
+
+ let setRefCount = 0;
+ let ref;
+
+ const setRef = r => {
+ setRefCount++;
+ ref = r;
+ };
+
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual([123]);
+ expect(ref instanceof Child).toBe(true);
+ expect(setRefCount).toBe(1);
+ ReactNoop.render();
+ 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 ;
+ }
+
+ const RefForwardingComponent = React.forwardRef((props, ref) => (
+
+ ));
+
+ const ref = React.createRef();
+
+ ReactNoop.render(
+
+
+ ,
+ );
+ 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();
+ 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(
+ ,
+ );
+ 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(
+ render: (props: Props, ref: React$ElementRef) => 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"