Delete create-subscription folder (#24288)

This commit is contained in:
dan
2022-04-11 20:07:22 +01:00
committed by GitHub
parent f993ffc514
commit d9a0f9e203
7 changed files with 0 additions and 891 deletions

View File

@@ -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
<Subscription source={eventDispatcher}>
{value => <AnotherComponent value={value} />}
</Subscription>
```
# Examples
This API can be used to subscribe to a variety of "subscribable" sources, from event dispatchers to RxJS observables. Below are a few examples of how to subscribe to common types.
## Subscribing to event dispatchers
Below is an example showing how `create-subscription` can be used to subscribe to event dispatchers such as DOM elements.
```js
import React from "react";
import { createSubscription } from "create-subscription";
// Start with a simple component.
// In this case, it's a function component, but it could have been a class.
function FollowerComponent({ followersCount }) {
return <div>You have {followersCount} followers!</div>;
}
// Create a wrapper component to manage the subscription.
const EventHandlerSubscription = createSubscription({
getCurrentValue: eventDispatcher => eventDispatcher.value,
subscribe: (eventDispatcher, callback) => {
const onChange = event => callback(eventDispatcher.value);
eventDispatcher.addEventListener("change", onChange);
return () => eventDispatcher.removeEventListener("change", onChange);
}
});
// Your component can now be used as shown below.
// In this example, 'eventDispatcher' represents a generic event dispatcher.
<EventHandlerSubscription source={eventDispatcher}>
{value => <FollowerComponent followersCount={value} />}
</EventHandlerSubscription>;
```
## Subscribing to observables
Below are examples showing how `create-subscription` can be used to subscribe to certain types of observables (e.g. RxJS `BehaviorSubject` and `ReplaySubject`).
**Note** that it is not possible to support all observable types (e.g. RxJS `Subject` or `Observable`) because some provide no way to read the "current" value after it has been emitted.
### `BehaviorSubject`
```js
const BehaviorSubscription = createSubscription({
getCurrentValue: behaviorSubject => behaviorSubject.getValue(),
subscribe: (behaviorSubject, callback) => {
const subscription = behaviorSubject.subscribe(callback);
return () => subscription.unsubscribe();
}
});
```
### `ReplaySubject`
```js
const ReplaySubscription = createSubscription({
getCurrentValue: replaySubject => {
let currentValue;
// ReplaySubject does not have a sync data getter,
// So we need to temporarily subscribe to retrieve the most recent value.
replaySubject
.subscribe(value => {
currentValue = value;
})
.unsubscribe();
return currentValue;
},
subscribe: (replaySubject, callback) => {
const subscription = replaySubject.subscribe(callback);
return () => subscription.unsubscribe();
}
});
```
## Subscribing to a Promise
Below is an example showing how `create-subscription` can be used with native Promises.
**Note** that 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.
<PromiseSubscription source={loadingPromise}>
{loadingStatus => <LoadingComponent loadingStatus={loadingStatus} />}
</PromiseSubscription>
```

View File

@@ -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';

View File

@@ -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');
}

View File

@@ -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"
}
}

View File

@@ -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(
<Subscription source={observable}>
{(value = 'default') => {
Scheduler.unstable_yieldValue(value);
return null;
}}
</Subscription>,
);
// 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(<div />);
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(<Subscription source={observable}>{render}</Subscription>);
expect(Scheduler).toFlushAndYield(['initial']);
observable.next('updated');
expect(Scheduler).toFlushAndYield(['updated']);
// Unsetting the subscriber prop should reset subscribed values
ReactNoop.render(<Subscription>{render}</Subscription>);
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(<Subscription source={promiseA}>{render}</Subscription>);
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(<Subscription source={promiseB}>{render}</Subscription>);
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(<Subscription source={promiseA}>{render}</Subscription>);
expect(Scheduler).toFlushAndYield(['default']);
ReactNoop.render(<Subscription source={promiseB}>{render}</Subscription>);
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(<Subscription source={promise}>{render}</Subscription>);
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(
<Subscription source={observableA}>{render}</Subscription>,
);
// Updates while subscribed should re-render the child component
expect(Scheduler).toFlushAndYield(['a-0']);
// Unsetting the subscriber prop should reset subscribed values
ReactNoop.render(
<Subscription source={observableB}>{render}</Subscription>,
);
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 (
<Subscription source={this.state.observed}>
{(value = 'default') => {
Scheduler.unstable_yieldValue('Subscriber: ' + value);
return <Child value={value} />;
}}
</Subscription>
);
}
}
const observableA = createBehaviorSubject('a-0');
const observableB = createBehaviorSubject('b-0');
ReactNoop.render(<Parent observed={observableA} />);
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(<Parent observed={observableB} />);
});
} else {
ReactNoop.render(<Parent observed={observableB} />);
}
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(<Parent 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(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 (
<Subscription source={this.state.observed}>
{(value = 'default') => {
Scheduler.unstable_yieldValue('Subscriber: ' + value);
return <Child value={value} />;
}}
</Subscription>
);
}
}
const observableA = createBehaviorSubject('a-0');
const observableB = createBehaviorSubject('b-0');
ReactNoop.render(<Parent observed={observableA} />);
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(<Parent observed={observableB} />);
});
} else {
ReactNoop.render(<Parent observed={observableB} />);
}
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(<Parent 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(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(
<Subscription source={observable}>{value => null}</Subscription>,
);
expect(Scheduler).toFlushAndThrow(
'A subscription must return an unsubscribe function.',
);
});
});
});

View File

@@ -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<Property, Value>(
config: $ReadOnly<{|
// Synchronously gets the value for the subscribed property.
// Return undefined if the subscribable value is undefined,
// Or does not support synchronous reading (e.g. native Promise).
getCurrentValue: (source: Property) => Value | void,
// Setup a subscription for the subscribable value in props, and return an unsubscribe function.
// Return 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<any>,
source: Property,
|};
type State = {|
source: Property,
value: Value | void,
|};
// Reference: https://gist.github.com/bvaughn/d569177d70b50b58bff69c3c4a5353f3
class Subscription extends React.Component<Props, State> {
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;
}

View File

@@ -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],