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

RFC #30: React.forwardRef implementation #12346

Merged
merged 22 commits into from
Mar 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


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

`create-subscription` provides an async-safe interface to manage a subscription.

## 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 [`simple-cache-provider`](https://github.com/facebook/react/blob/master/packages/simple-cache-provider/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.

## 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: `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, 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 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 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({
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 it 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 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({
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>
```
12 changes: 12 additions & 0 deletions packages/create-subscription/index.js
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';
7 changes: 7 additions & 0 deletions packages/create-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/create-subscription.production.min.js');
} else {
module.exports = require('./cjs/create-subscription.development.js');
}
21 changes: 21 additions & 0 deletions packages/create-subscription/package.json
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"
}
}
Loading