From 287ee8199aba7380e5cb17fe73e3516aa4db95e9 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 13 Mar 2018 13:59:09 -0700 Subject: [PATCH] create-subscription (#12325) create-subscription provides an simple, async-safe interface to manage a subscription. --- packages/create-subscription/README.md | 184 +++++++ packages/create-subscription/index.js | 12 + packages/create-subscription/npm/index.js | 7 + packages/create-subscription/package.json | 21 + .../createSubscription-test.internal.js | 457 ++++++++++++++++++ .../src/createSubscription.js | 159 ++++++ scripts/rollup/bundles.js | 10 + scripts/rollup/results.json | 204 ++++---- yarn.lock | 10 + 9 files changed, 976 insertions(+), 88 deletions(-) create mode 100644 packages/create-subscription/README.md create mode 100644 packages/create-subscription/index.js create mode 100644 packages/create-subscription/npm/index.js create mode 100644 packages/create-subscription/package.json create mode 100644 packages/create-subscription/src/__tests__/createSubscription-test.internal.js create mode 100644 packages/create-subscription/src/createSubscription.js 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/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"