-
Notifications
You must be signed in to change notification settings - Fork 47k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
create-subscription #12325
create-subscription #12325
Changes from 47 commits
d5d8bf6
7b1e8c2
f8743b3
4304b55
30eb16a
eb1372c
88e7e22
c395051
78c9a4c
d1fc6e8
2e98ca9
5093ab4
54468e8
6b16b7c
d05ffa3
bd36fb4
a11164e
31edf59
256c5e5
6dcac15
39d7ba8
b5571c1
2192fd5
fdfa22b
afeb6cd
7532184
0f936ba
2d824c2
3edff49
e056172
9bdc6d6
9ffe079
629f145
48b4a1b
ee2ae93
64d80b8
ad190fb
3288726
81f2695
267a76b
db7b84f
32d6d40
5557120
ee3dfcc
a2f43a5
4e57ed7
f0c68b8
63a65e6
e6740aa
c116528
c1dd9a7
e10e2fc
f03dfa9
6f740d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
# create-subscription | ||
|
||
[Async-safe subscriptions are hard to get right.](https://gist.github.com/bvaughn/d569177d70b50b58bff69c3c4a5353f3) | ||
|
||
`create-subscription` provides an simple, async-safe interface to manage a subscription. | ||
|
||
## Who should use this? | ||
|
||
This utility is 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). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit - 'This utility is should' -> 'This utility should' |
||
|
||
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 use this same technique (as referenced [here](https://gist.github.com/bvaughn/d569177d70b50b58bff69c3c4a5353f3)) in a way that is most optimized for their library usage. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit - this bullet could be reworded for clarity, something like this:
|
||
|
||
## 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: `getValue` and `subscribe`. | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. YES! I was going to suggest starting with a concrete example, glad you added this here. |
||
```js | ||
import { createSubscription } from "create-subscription"; | ||
|
||
const Subscription = createSubscription({ | ||
getValue(source) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For a while, I thought that this function was supposed to be
I'm worried people might do this, though I don't know what to do about it.
…where the source isn't something that offers a way to read the value synchronously. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any actionable suggestion to avoid this? Docs? Method names? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if we call it 'getCurrentValue'? We could also add a note in the comments saying that this value will be read repeatedly as the component re-renders. |
||
// 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 false 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 | ||
<Subscription source={eventDispatcher}> | ||
{value => <AnotherComponent value={value} />} | ||
</Subscription> | ||
``` | ||
|
||
# Examples | ||
|
||
This API can be used to subscribe to a variety of "subscribable" sources, from Flux stores to RxJS observables. Below are a few examples of how to subscribe to common types. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "from Flux stores" — this clashes with what you say earlier (this shouldn't be used for Flux stores) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, good call. |
||
|
||
## Subscribing to event dispatchers | ||
|
||
Below is an example showing how `create-subscription` can be used to subscribe to event dispatchers such as DOM elements or Flux stores. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same |
||
|
||
```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 <div>You have {followersCount} followers!</div>; | ||
} | ||
|
||
// Create a wrapper component to manage the subscription. | ||
const EventHandlerSubscription = createSubscription({ | ||
getValue: 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({ | ||
getValue: behaviorSubject => behaviorSubject.getValue(), | ||
subscribe: (behaviorSubject, callback) => { | ||
const subscription = behaviorSubject.subscribe(callback); | ||
return () => subscription.unsubscribe(); | ||
} | ||
}); | ||
``` | ||
|
||
### `ReplaySubject` | ||
```js | ||
const ReplaySubscription = createSubscription({ | ||
getValue: 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Firstly - What do you think of showing an example that passes through the value returned by the promise? Like so:
Secondly:
True; unless you're overly clever! :D
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I show true/false because the example shows a "loading" promise. Maybe I can compromise? Check out the commit I'll push shortly.
Unfortunately, that wouldn't prevent the initial render with |
||
|
||
**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({ | ||
getValue: 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 false; | ||
} | ||
}); | ||
|
||
// Your component can now be used as shown below. | ||
<PromiseSubscription source={loadingPromise}> | ||
{loadingStatus => <LoadingComponent loadingStatus={loadingStatus} />} | ||
</PromiseSubscription> | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one line summary is too appealing. It doesn't suggest any reasons for why I shouldn't use subscriptions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha. I tried to clarify who shouldn't use it below (in the "Who should use this?" section). Any specific wording suggestions?