Skip to content

Commit 00a0e3c

Browse files
authored
create-subscription (#12325)
create-subscription provides an simple, async-safe interface to manage a subscription.
1 parent ad9544f commit 00a0e3c

File tree

9 files changed

+976
-88
lines changed

9 files changed

+976
-88
lines changed
+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# create-subscription
2+
3+
`create-subscription` provides an async-safe interface to manage a subscription.
4+
5+
## When should you NOT use this?
6+
7+
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).
8+
9+
Other cases have **better long-term solutions**:
10+
* Redux/Flux stores should use the [context API](https://reactjs.org/docs/context.html) instead.
11+
* 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.
12+
* 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.
13+
14+
## What types of subscriptions can this support?
15+
16+
This abstraction can handle a variety of subscription types, including:
17+
* Event dispatchers like `HTMLInputElement`.
18+
* Custom pub/sub components like Relay's `FragmentSpecResolver`.
19+
* 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.)
20+
* Native Promises.
21+
22+
# Installation
23+
24+
```sh
25+
# Yarn
26+
yarn add create-subscription
27+
28+
# NPM
29+
npm install create-subscription --save
30+
```
31+
32+
# Usage
33+
34+
To configure a subscription, you must provide two methods: `getCurrentValue` and `subscribe`.
35+
36+
```js
37+
import { createSubscription } from "create-subscription";
38+
39+
const Subscription = createSubscription({
40+
getCurrentValue(source) {
41+
// Return the current value of the subscription (source),
42+
// or `undefined` if the value can't be read synchronously (e.g. native Promises).
43+
},
44+
subscribe(source, callback) {
45+
// Subscribe (e.g. add an event listener) to the subscription (source).
46+
// Call callback(newValue) whenever a subscription changes.
47+
// Return an unsubscribe method,
48+
// Or a no-op if unsubscribe is not supported (e.g. native Promises).
49+
}
50+
});
51+
```
52+
53+
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:
54+
55+
```js
56+
<Subscription source={eventDispatcher}>
57+
{value => <AnotherComponent value={value} />}
58+
</Subscription>
59+
```
60+
61+
# Examples
62+
63+
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.
64+
65+
## Subscribing to event dispatchers
66+
67+
Below is an example showing how `create-subscription` can be used to subscribe to event dispatchers such as DOM elements.
68+
69+
```js
70+
import React from "react";
71+
import { createSubscription } from "create-subscription";
72+
73+
// Start with a simple component.
74+
// In this case, it's a functional component, but it could have been a class.
75+
function FollowerComponent({ followersCount }) {
76+
return <div>You have {followersCount} followers!</div>;
77+
}
78+
79+
// Create a wrapper component to manage the subscription.
80+
const EventHandlerSubscription = createSubscription({
81+
getCurrentValue: eventDispatcher => eventDispatcher.value,
82+
subscribe: (eventDispatcher, callback) => {
83+
const onChange = event => callback(eventDispatcher.value);
84+
eventDispatcher.addEventListener("change", onChange);
85+
return () => eventDispatcher.removeEventListener("change", onChange);
86+
}
87+
});
88+
89+
// Your component can now be used as shown below.
90+
// In this example, 'eventDispatcher' represents a generic event dispatcher.
91+
<EventHandlerSubscription source={eventDispatcher}>
92+
{value => <FollowerComponent followersCount={value} />}
93+
</EventHandlerSubscription>;
94+
```
95+
96+
## Subscribing to observables
97+
98+
Below are examples showing how `create-subscription` can be used to subscribe to certain types of observables (e.g. RxJS `BehaviorSubject` and `ReplaySubject`).
99+
100+
**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.
101+
102+
### `BehaviorSubject`
103+
```js
104+
const BehaviorSubscription = createSubscription({
105+
getCurrentValue: behaviorSubject => behaviorSubject.getValue(),
106+
subscribe: (behaviorSubject, callback) => {
107+
const subscription = behaviorSubject.subscribe(callback);
108+
return () => subscription.unsubscribe();
109+
}
110+
});
111+
```
112+
113+
### `ReplaySubject`
114+
```js
115+
const ReplaySubscription = createSubscription({
116+
getCurrentValue: replaySubject => {
117+
let currentValue;
118+
// ReplaySubject does not have a sync data getter,
119+
// So we need to temporarily subscribe to retrieve the most recent value.
120+
replaySubject
121+
.subscribe(value => {
122+
currentValue = value;
123+
})
124+
.unsubscribe();
125+
return currentValue;
126+
},
127+
subscribe: (replaySubject, callback) => {
128+
const subscription = replaySubject.subscribe(callback);
129+
return () => subscription.unsubscribe();
130+
}
131+
});
132+
```
133+
134+
## Subscribing to a Promise
135+
136+
Below is an example showing how `create-subscription` can be used with native Promises.
137+
138+
**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.
139+
140+
**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.
141+
142+
```js
143+
import React from "react";
144+
import { createSubscription } from "create-subscription";
145+
146+
// Start with a simple component.
147+
function LoadingComponent({ loadingStatus }) {
148+
if (loadingStatus === undefined) {
149+
// Loading
150+
} else if (loadingStatus === null) {
151+
// Error
152+
} else {
153+
// Success
154+
}
155+
}
156+
157+
// Wrap the functional component with a subscriber HOC.
158+
// This HOC will manage subscriptions and pass values to the decorated component.
159+
// It will add and remove subscriptions in an async-safe way when props change.
160+
const PromiseSubscription = createSubscription({
161+
getCurrentValue: promise => {
162+
// There is no way to synchronously read a Promise's value,
163+
// So this method should return undefined.
164+
return undefined;
165+
},
166+
subscribe: (promise, callback) => {
167+
promise.then(
168+
// Success
169+
value => callback(value),
170+
// Failure
171+
() => callback(null)
172+
);
173+
174+
// There is no way to "unsubscribe" from a Promise.
175+
// create-subscription will still prevent stale values from rendering.
176+
return () => {};
177+
}
178+
});
179+
180+
// Your component can now be used as shown below.
181+
<PromiseSubscription source={loadingPromise}>
182+
{loadingStatus => <LoadingComponent loadingStatus={loadingStatus} />}
183+
</PromiseSubscription>
184+
```

packages/create-subscription/index.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
'use strict';
11+
12+
export * from './src/createSubscription';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
3+
if (process.env.NODE_ENV === 'production') {
4+
module.exports = require('./cjs/create-subscription.production.min.js');
5+
} else {
6+
module.exports = require('./cjs/create-subscription.development.js');
7+
}
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "create-subscription",
3+
"description": "HOC for creating async-safe React components with subscriptions",
4+
"version": "0.0.1",
5+
"repository": "facebook/react",
6+
"files": [
7+
"LICENSE",
8+
"README.md",
9+
"index.js",
10+
"cjs/"
11+
],
12+
"dependencies": {
13+
"fbjs": "^0.8.16"
14+
},
15+
"peerDependencies": {
16+
"react": "16.3.0-alpha.1"
17+
},
18+
"devDependencies": {
19+
"rxjs": "^5.5.6"
20+
}
21+
}

0 commit comments

Comments
 (0)