Skip to content
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

useSubscription hook #15022

Merged
merged 18 commits into from
Jul 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
132 changes: 132 additions & 0 deletions packages/use-subscription/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# use-subscription

React hook that safely manages subscriptions in concurrent mode.

## When should you NOT use this?

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).

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 a mechanism like [`react-cache`](https://github.com/facebook/react/blob/master/packages/react-cache/README.md) instead.
* 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.

## Limitations in concurrent mode

`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.

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).

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.

## 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.)

Note that JavaScript promises are also **not supported** because they provide no way to synchronously read the "current" value.

# Installation

```sh
# Yarn
yarn add use-subscription

# NPM
npm install use-subscription
```

# Usage

To configure a subscription, you must provide two methods: `getCurrentValue` and `subscribe`.

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()`.

## Subscribing to event dispatchers

Below is an example showing how `use-subscription` can be used to subscribe to event dispatchers such as DOM elements.

```js
import React, { useMemo } from "react";
import { useSubscription } from "use-subscription";

// In this example, "input" is an event dispatcher (e.g. an HTMLInputElement)
// but it could be anything that emits an event and has a readable current value.
function Example({ input }) {

// Memoize to avoid removing and re-adding subscriptions each time this hook is called.
const subscription = useMemo(
() => ({
getCurrentValue: () => input.value,
subscribe: callback => {
input.addEventListener("change", callback);
return () => input.removeEventListener("change", callback);
}
}),

// Re-subscribe any time our input changes
// (e.g. we get a new HTMLInputElement prop to subscribe to)
[input]
);

// The value returned by this hook reflects the input's current value.
// Our component will automatically be re-rendered when that value changes.
const value = useSubscription(subscription);

// Your rendered output goes here ...
}
```

## Subscribing to observables

Below are examples showing how `use-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 subscription = useMemo(
() => ({
getCurrentValue: () => behaviorSubject.getValue(),
subscribe: callback => {
const subscription = behaviorSubject.subscribe(callback);
return () => subscription.unsubscribe();
}
}),

// Re-subscribe any time the behaviorSubject changes
[behaviorSubject]
);

const value = useSubscription(subscription);
```

### `ReplaySubject`
```js
const subscription = useMemo(
() => ({
getCurrentValue: () => {
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: callback => {
const subscription = replaySubject.subscribe(callback);
return () => subscription.unsubscribe();
}
}),

// Re-subscribe any time the replaySubject changes
[replaySubject]
);

const value = useSubscription(subscription);
```
12 changes: 12 additions & 0 deletions packages/use-subscription/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* 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/useSubscription';
7 changes: 7 additions & 0 deletions packages/use-subscription/npm/index.js
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/use-subscription.production.min.js');
} else {
module.exports = require('./cjs/use-subscription.development.js');
}
24 changes: 24 additions & 0 deletions packages/use-subscription/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"private": true,
"name": "use-subscription",
"description": "Reusable hooks",
"version": "0.0.0",
"repository": {
"type": "git",
"url": "https://github.com/facebook/react.git",
"directory": "packages/use-subscription"
},
"files": [
"LICENSE",
"README.md",
"build-info.json",
"index.js",
"cjs/"
],
"peerDependencies": {
"react": "^16.8.0"
},
"devDependencies": {
"rxjs": "^5.5.6"
}
}