Skip to content

Commit ce883a1

Browse files
author
Brian Vaughn
authored
useSubscription hook (#15022)
* Added use-subscription package with README
1 parent c45c2c3 commit ce883a1

6 files changed

Lines changed: 861 additions & 0 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# use-subscription
2+
3+
React hook that safely manages subscriptions in concurrent mode.
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 a mechanism like [`react-cache`](https://github.com/facebook/react/blob/master/packages/react-cache/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+
## Limitations in concurrent mode
15+
16+
`use-subscription` is safe to use in concurrent mode. However, [it achieves correctness by sometimes de-opting to synchronous mode](https://github.com/facebook/react/issues/13186#issuecomment-403959161), obviating the benefits of concurrent rendering. This is an inherent limitation of storing state outside of React's managed state queue and rendering in response to a change event.
17+
18+
The effect of de-opting to sync mode is that the main thread may periodically be blocked (in the case of CPU-bound work), and placeholders may appear earlier than desired (in the case of IO-bound work).
19+
20+
For **full compatibility** with concurrent rendering, including both **time-slicing** and **React Suspense**, the suggested longer-term solution is to move to one of the patterns described in the previous section.
21+
22+
## What types of subscriptions can this support?
23+
24+
This abstraction can handle a variety of subscription types, including:
25+
* Event dispatchers like `HTMLInputElement`.
26+
* Custom pub/sub components like Relay's `FragmentSpecResolver`.
27+
* 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.)
28+
29+
Note that JavaScript promises are also **not supported** because they provide no way to synchronously read the "current" value.
30+
31+
# Installation
32+
33+
```sh
34+
# Yarn
35+
yarn add use-subscription
36+
37+
# NPM
38+
npm install use-subscription
39+
```
40+
41+
# Usage
42+
43+
To configure a subscription, you must provide two methods: `getCurrentValue` and `subscribe`.
44+
45+
In order to avoid removing and re-adding subscriptions each time this hook is called, the parameters passed to this hook should be memoized. This can be done by wrapping the entire subscription with `useMemo()`, or by wrapping the individual callbacks with `useCallback()`.
46+
47+
## Subscribing to event dispatchers
48+
49+
Below is an example showing how `use-subscription` can be used to subscribe to event dispatchers such as DOM elements.
50+
51+
```js
52+
import React, { useMemo } from "react";
53+
import { useSubscription } from "use-subscription";
54+
55+
// In this example, "input" is an event dispatcher (e.g. an HTMLInputElement)
56+
// but it could be anything that emits an event and has a readable current value.
57+
function Example({ input }) {
58+
59+
// Memoize to avoid removing and re-adding subscriptions each time this hook is called.
60+
const subscription = useMemo(
61+
() => ({
62+
getCurrentValue: () => input.value,
63+
subscribe: callback => {
64+
input.addEventListener("change", callback);
65+
return () => input.removeEventListener("change", callback);
66+
}
67+
}),
68+
69+
// Re-subscribe any time our input changes
70+
// (e.g. we get a new HTMLInputElement prop to subscribe to)
71+
[input]
72+
);
73+
74+
// The value returned by this hook reflects the input's current value.
75+
// Our component will automatically be re-rendered when that value changes.
76+
const value = useSubscription(subscription);
77+
78+
// Your rendered output goes here ...
79+
}
80+
```
81+
82+
## Subscribing to observables
83+
84+
Below are examples showing how `use-subscription` can be used to subscribe to certain types of observables (e.g. RxJS `BehaviorSubject` and `ReplaySubject`).
85+
86+
**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.
87+
88+
### `BehaviorSubject`
89+
```js
90+
const subscription = useMemo(
91+
() => ({
92+
getCurrentValue: () => behaviorSubject.getValue(),
93+
subscribe: callback => {
94+
const subscription = behaviorSubject.subscribe(callback);
95+
return () => subscription.unsubscribe();
96+
}
97+
}),
98+
99+
// Re-subscribe any time the behaviorSubject changes
100+
[behaviorSubject]
101+
);
102+
103+
const value = useSubscription(subscription);
104+
```
105+
106+
### `ReplaySubject`
107+
```js
108+
const subscription = useMemo(
109+
() => ({
110+
getCurrentValue: () => {
111+
let currentValue;
112+
// ReplaySubject does not have a sync data getter,
113+
// So we need to temporarily subscribe to retrieve the most recent value.
114+
replaySubject
115+
.subscribe(value => {
116+
currentValue = value;
117+
})
118+
.unsubscribe();
119+
return currentValue;
120+
},
121+
subscribe: callback => {
122+
const subscription = replaySubject.subscribe(callback);
123+
return () => subscription.unsubscribe();
124+
}
125+
}),
126+
127+
// Re-subscribe any time the replaySubject changes
128+
[replaySubject]
129+
);
130+
131+
const value = useSubscription(subscription);
132+
```

packages/use-subscription/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
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/useSubscription';
Lines changed: 7 additions & 0 deletions
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/use-subscription.production.min.js');
5+
} else {
6+
module.exports = require('./cjs/use-subscription.development.js');
7+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"private": true,
3+
"name": "use-subscription",
4+
"description": "Reusable hooks",
5+
"version": "0.0.0",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/facebook/react.git",
9+
"directory": "packages/use-subscription"
10+
},
11+
"files": [
12+
"LICENSE",
13+
"README.md",
14+
"build-info.json",
15+
"index.js",
16+
"cjs/"
17+
],
18+
"peerDependencies": {
19+
"react": "^16.8.0"
20+
},
21+
"devDependencies": {
22+
"rxjs": "^5.5.6"
23+
}
24+
}

0 commit comments

Comments
 (0)