diff --git a/packages/create-subscription/README.md b/packages/create-subscription/README.md deleted file mode 100644 index 281683786adc9..0000000000000 --- a/packages/create-subscription/README.md +++ /dev/null @@ -1,169 +0,0 @@ -# create-subscription - -`create-subscription` is a utility for subscribing to external data sources inside React components. - -**It is no longer maintained and will not be updated. Use the built-in [`React.useSyncExternalStore`](https://reactjs.org/docs/hooks-reference.html#usesyncexternalstore) instead.** - -# Installation - -```sh -# Yarn -yarn add create-subscription - -# NPM -npm install create-subscription -``` - -# 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, 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 function 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 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 consideration 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 function 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 deleted file mode 100644 index 314587a1351b1..0000000000000 --- a/packages/create-subscription/index.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * 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 deleted file mode 100644 index 6b7a5b017457d..0000000000000 --- a/packages/create-subscription/npm/index.js +++ /dev/null @@ -1,7 +0,0 @@ -'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 deleted file mode 100644 index a7b3334461133..0000000000000 --- a/packages/create-subscription/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "create-subscription", - "description": "utility for subscribing to external data sources inside React components", - "version": "18.0.0", - "repository": { - "type": "git", - "url": "https://github.com/facebook/react.git", - "directory": "packages/create-subscription" - }, - "files": [ - "LICENSE", - "README.md", - "index.js", - "cjs/" - ], - "peerDependencies": { - "react": "^18.0.0" - }, - "devDependencies": { - "rxjs": "^5.5.6" - } -} diff --git a/packages/create-subscription/src/__tests__/createSubscription-test.js b/packages/create-subscription/src/__tests__/createSubscription-test.js deleted file mode 100644 index c596bdb954c07..0000000000000 --- a/packages/create-subscription/src/__tests__/createSubscription-test.js +++ /dev/null @@ -1,505 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * 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 React; -let ReactNoop; -let Scheduler; -let ReplaySubject; - -describe('createSubscription', () => { - beforeEach(() => { - jest.resetModules(); - createSubscription = require('create-subscription').createSubscription; - - React = require('react'); - ReactNoop = require('react-noop-renderer'); - Scheduler = require('scheduler'); - - 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') => { - Scheduler.unstable_yieldValue(value); - return null; - }} - , - ); - - // Updates while subscribed should re-render the child component - expect(Scheduler).toFlushAndYield(['default']); - observable.next(123); - expect(Scheduler).toFlushAndYield([123]); - observable.next('abc'); - expect(Scheduler).toFlushAndYield(['abc']); - - // Unmounting the subscriber should remove listeners - ReactNoop.render(
); - observable.next(456); - expect(Scheduler).toFlushAndYield([]); - }); - - 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') { - Scheduler.unstable_yieldValue(value); - return null; - } - - const observable = createReplaySubject('initial'); - - ReactNoop.render({render}); - expect(Scheduler).toFlushAndYield(['initial']); - observable.next('updated'); - expect(Scheduler).toFlushAndYield(['updated']); - - // Unsetting the subscriber prop should reset subscribed values - ReactNoop.render({render}); - expect(Scheduler).toFlushAndYield(['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) { - Scheduler.unstable_yieldValue('loading'); - } else { - Scheduler.unstable_yieldValue(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(Scheduler).toFlushAndYield(['loading']); - resolveA(true); - await promiseA; - expect(Scheduler).toFlushAndYield(['finished']); - - // Test a promise that resolves before render - // Note that this will require an extra render anyway, - // Because there is no way to synchronously get a Promise's value - rejectB(false); - ReactNoop.render({render}); - expect(Scheduler).toFlushAndYield(['loading']); - await promiseB.catch(() => true); - expect(Scheduler).toFlushAndYield(['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') { - Scheduler.unstable_yieldValue(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(Scheduler).toFlushAndYield(['default']); - ReactNoop.render({render}); - expect(Scheduler).toFlushAndYield(['default']); - - // Resolve both Promises - resolveB(123); - resolveA('abc'); - await Promise.all([promiseA, promiseB]); - - // Ensure that only Promise B causes an update - expect(Scheduler).toFlushAndYield([123]); - }); - - it('should not call setState for a Promise that resolves after unmount', 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) { - Scheduler.unstable_yieldValue('rendered'); - return null; - } - - let resolvePromise; - const promise = new Promise((resolve, reject) => { - resolvePromise = resolve; - }); - - ReactNoop.render({render}); - expect(Scheduler).toFlushAndYield(['rendered']); - - // Unmount - ReactNoop.render(null); - expect(Scheduler).toFlushWithoutYielding(); - - // Resolve Promise should not trigger a setState warning - resolvePromise(true); - await promise; - }); - }); - - 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') { - Scheduler.unstable_yieldValue(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(Scheduler).toFlushAndYield(['a-0']); - - // Unsetting the subscriber prop should reset subscribed values - ReactNoop.render( - {render}, - ); - expect(Scheduler).toFlushAndYield(['b-0']); - - // Updates to the old subscribable should not re-render the child component - observableA.next('a-1'); - expect(Scheduler).toFlushAndYield([]); - - // Updates to the bew subscribable should re-render the child component - observableB.next('b-1'); - expect(Scheduler).toFlushAndYield(['b-1']); - }); - - it('should ignore values emitted by a new subscribable until the commit phase', () => { - const log = []; - - function Child({value}) { - Scheduler.unstable_yieldValue('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() { - return ( - - {(value = 'default') => { - Scheduler.unstable_yieldValue('Subscriber: ' + value); - return ; - }} - - ); - } - } - - const observableA = createBehaviorSubject('a-0'); - const observableB = createBehaviorSubject('b-0'); - - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Subscriber: a-0', 'Child: a-0']); - expect(log).toEqual(['Parent.componentDidMount']); - - // Start React update, but don't finish - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - ReactNoop.render(); - }); - } else { - ReactNoop.render(); - } - expect(Scheduler).toFlushAndYieldThrough(['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'); - - // Update again - ReactNoop.render(); - - // 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(Scheduler).toFlushAndYield([ - '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 = []; - - function Child({value}) { - Scheduler.unstable_yieldValue('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() { - return ( - - {(value = 'default') => { - Scheduler.unstable_yieldValue('Subscriber: ' + value); - return ; - }} - - ); - } - } - - const observableA = createBehaviorSubject('a-0'); - const observableB = createBehaviorSubject('b-0'); - - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Subscriber: a-0', 'Child: a-0']); - expect(log).toEqual(['Parent.componentDidMount']); - - // Start React update, but don't finish - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - ReactNoop.render(); - }); - } else { - ReactNoop.render(); - } - expect(Scheduler).toFlushAndYieldThrough(['Subscriber: b-0']); - expect(log).toEqual(['Parent.componentDidMount']); - - // Emit some updates from the old subscribable - observableA.next('a-1'); - observableA.next('a-2'); - - // Update again - ReactNoop.render(); - - // 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(Scheduler).toFlushAndYield([ - 'Child: b-0', - 'Subscriber: a-2', - 'Child: a-2', - ]); - expect(log).toEqual([ - 'Parent.componentDidMount', - 'Parent.componentDidUpdate', - 'Parent.componentDidUpdate', - ]); - - // Updates from the new subscribable should be ignored. - observableB.next('b-1'); - expect(Scheduler).toFlushAndYield([]); - expect(log).toEqual([ - 'Parent.componentDidMount', - 'Parent.componentDidUpdate', - 'Parent.componentDidUpdate', - ]); - }); - - describe('warnings', () => { - it('should warn for invalid missing getCurrentValue', () => { - expect(() => { - createSubscription( - { - subscribe: () => () => {}, - }, - () => null, - ); - }).toErrorDev('Subscription must specify a getCurrentValue function', { - withoutStack: true, - }); - }); - - it('should warn for invalid missing subscribe', () => { - expect(() => { - createSubscription( - { - getCurrentValue: () => () => {}, - }, - () => null, - ); - }).toErrorDev('Subscription must specify a subscribe function', { - withoutStack: true, - }); - }); - - 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(Scheduler).toFlushAndThrow( - 'A subscription must return an unsubscribe function.', - ); - }); - }); -}); diff --git a/packages/create-subscription/src/createSubscription.js b/packages/create-subscription/src/createSubscription.js deleted file mode 100644 index 329a238fc238a..0000000000000 --- a/packages/create-subscription/src/createSubscription.js +++ /dev/null @@ -1,159 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import * as React from 'react'; - -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 empty function if 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; - - if (__DEV__) { - if (typeof getCurrentValue !== 'function') { - console.error('Subscription must specify a getCurrentValue function'); - } - if (typeof subscribe !== 'function') { - console.error('Subscription must specify a subscribe function'); - } - } - - type Props = {| - children: (value: Value) => React$Element, - source: Property, - |}; - type State = {| - source: Property, - value: Value | void, - |}; - - // Reference: https://gist.github.com/bvaughn/d569177d70b50b58bff69c3c4a5353f3 - class Subscription extends React.Component { - state: State = { - source: this.props.source, - value: - this.props.source != null - ? getCurrentValue(this.props.source) - : undefined, - }; - - _hasUnmounted: boolean = false; - _unsubscribe: Unsubscribe | null = null; - - static getDerivedStateFromProps(nextProps, prevState) { - if (nextProps.source !== prevState.source) { - return { - source: nextProps.source, - value: - nextProps.source != null - ? getCurrentValue(nextProps.source) - : undefined, - }; - } - - return null; - } - - componentDidMount() { - this.subscribe(); - } - - componentDidUpdate(prevProps, prevState) { - if (this.state.source !== prevState.source) { - this.unsubscribe(); - this.subscribe(); - } - } - - componentWillUnmount() { - this.unsubscribe(); - - // Track mounted to avoid calling setState after unmounting - // For source like Promises that can't be unsubscribed from. - this._hasUnmounted = true; - } - - render() { - return this.props.children(this.state.value); - } - - subscribe() { - const {source} = this.state; - if (source != null) { - const callback = (value: Value | void) => { - if (this._hasUnmounted) { - return; - } - - 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 the unsubscribe method for later (in case the subscribable prop changes). - const unsubscribe = subscribe(source, callback); - - if (typeof unsubscribe !== 'function') { - throw new Error( - 'A subscription must return an unsubscribe function.', - ); - } - - // It's safe to store unsubscribe on the instance because - // We only read or write that property during the "commit" phase. - this._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() { - if (typeof this._unsubscribe === 'function') { - this._unsubscribe(); - } - this._unsubscribe = null; - } - } - - return Subscription; -} diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index dffa9055d8454..4819ecb2a47a8 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -772,23 +772,6 @@ const bundles = [ externals: ['react', 'scheduler'], }, - /******* createComponentWithSubscriptions *******/ - { - bundleTypes: [NODE_DEV, NODE_PROD], - moduleType: ISOMORPHIC, - entry: 'create-subscription', - global: 'createSubscription', - externals: ['react'], - minifyWithProdErrorCodes: true, - wrapWithModuleBoundaries: true, - babel: opts => - Object.assign({}, opts, { - plugins: opts.plugins.concat([ - [require.resolve('@babel/plugin-transform-classes'), {loose: true}], - ]), - }), - }, - /******* Hook for managing subscriptions safely *******/ { bundleTypes: [NODE_DEV, NODE_PROD],