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

create-subscription #12325

Merged
merged 54 commits into from
Mar 13, 2018
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
d5d8bf6
POC for create-component-with-subscriptions
bvaughn Mar 5, 2018
7b1e8c2
Updated README
bvaughn Mar 5, 2018
f8743b3
Added Rollup bundle
bvaughn Mar 5, 2018
4304b55
Expanded tests
bvaughn Mar 5, 2018
30eb16a
Updated bundle comment
bvaughn Mar 5, 2018
eb1372c
Added a test for "cold" observable
bvaughn Mar 5, 2018
88e7e22
Updated inline comments
bvaughn Mar 5, 2018
c395051
Updated README examples
bvaughn Mar 5, 2018
78c9a4c
Added a test (and README docs) for Promises
bvaughn Mar 5, 2018
d1fc6e8
Added a caveat about Promises to README
bvaughn Mar 5, 2018
2e98ca9
Use a HOC for functional components and a mixin for ES6 components
bvaughn Mar 6, 2018
5093ab4
Added tests for create-react-class
bvaughn Mar 6, 2018
54468e8
Flow fix
bvaughn Mar 6, 2018
6b16b7c
Added a ref test
bvaughn Mar 6, 2018
d05ffa3
Added a test for react-lifecycles-compat
bvaughn Mar 6, 2018
bd36fb4
Updated README to show class component
bvaughn Mar 6, 2018
a11164e
Added docs for default values
bvaughn Mar 6, 2018
31edf59
Improved README examples
bvaughn Mar 6, 2018
256c5e5
Simplified Promise docs and added additional test
bvaughn Mar 6, 2018
6dcac15
Swapped functional/class component usage in examples
bvaughn Mar 6, 2018
39d7ba8
Split internal and public API tests
bvaughn Mar 6, 2018
b5571c1
Tweaks
bvaughn Mar 6, 2018
2192fd5
Changed impl to only support one subscription per component
bvaughn Mar 6, 2018
fdfa22b
Docs tweak
bvaughn Mar 6, 2018
afeb6cd
Docs tweaks
bvaughn Mar 6, 2018
7532184
Refactored create-subscription to more closely mimic context API
bvaughn Mar 7, 2018
0f936ba
Renamed create-component-with-subscriptions => create-subscription
bvaughn Mar 7, 2018
2d824c2
Renamed references to create-subscription
bvaughn Mar 7, 2018
3edff49
Replaced .toThrow with .toWarnDev
bvaughn Mar 7, 2018
e056172
Disable render-phase side effects
bvaughn Mar 7, 2018
9bdc6d6
Updated docs
bvaughn Mar 7, 2018
9ffe079
README and naming tweaks
bvaughn Mar 7, 2018
629f145
README tweaks
bvaughn Mar 7, 2018
48b4a1b
Wording tweak
bvaughn Mar 7, 2018
ee2ae93
Inline comments tweak
bvaughn Mar 7, 2018
64d80b8
Minor test tidying up
bvaughn Mar 7, 2018
ad190fb
Added more context to README intro
bvaughn Mar 7, 2018
3288726
Wordsmith nit picking
bvaughn Mar 7, 2018
81f2695
Wordsmith nit picking
bvaughn Mar 7, 2018
267a76b
Replaced Value with Value | void type
bvaughn Mar 7, 2018
db7b84f
Tweaks in response to Flarnie's feedback
bvaughn Mar 7, 2018
32d6d40
Added RxJS for tests instead of fake impls
bvaughn Mar 7, 2018
5557120
Improved children Flow type slightly
bvaughn Mar 7, 2018
ee3dfcc
Added Flow <> around config
bvaughn Mar 7, 2018
a2f43a5
Fixed example imports in README
bvaughn Mar 8, 2018
4e57ed7
Replaced createComponent() references with createSubscription() in RE…
bvaughn Mar 8, 2018
f0c68b8
Changed subscribe() to return an unsubscribe method (or false)
bvaughn Mar 8, 2018
63a65e6
Flow type tweak
bvaughn Mar 12, 2018
e6740aa
Merge branch 'master' into create-component-with-subscriptions
bvaughn Mar 13, 2018
c116528
Responded to Andrew's PR feedback
bvaughn Mar 13, 2018
c1dd9a7
Docs updatE
bvaughn Mar 13, 2018
e10e2fc
Flow tweak
bvaughn Mar 13, 2018
f03dfa9
Addressed PR feedback from Flarnie
bvaughn Mar 13, 2018
6f740d9
Removed contradictory references to Flux stores in README
bvaughn Mar 13, 2018
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
294 changes: 294 additions & 0 deletions packages/create-component-with-subscriptions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
# create-component-with-subscriptions

[Async-safe subscriptions are hard to get right.](https://gist.github.com/bvaughn/d569177d70b50b58bff69c3c4a5353f3)

This complexity is acceptible for libraries like Redux/Relay/MobX, but it's not ideal to have mixed in with application code. `create-component-with-subscriptions` provides an interface to easily manage subscriptions in an async-safe way.

## Installation

```sh
# Yarn
yarn add create-component-with-subscriptions

# NPM
npm install create-component-with-subscriptions --save
```

# API

Creating a subscription component requires a configuration object and a React component. The configuration object must have four properties:

#### `property: string`

Property name of the subscribable sources (e.g. "hasLoaded").

#### `getValue: (props: Props) => Value`

Synchronously returns the value of the subscribable property.

You should return `undefined` if the subscribable type does not support this operation (e.g. native Promises).

For example:
```js
function getValue(props: Props) {
return props.scrollContainer.scrollTop;
}
```

#### `subscribe(props: Props, valueChangedCallback: (value: any) => void) => Subscription`

Setup a subscription for the subscribable value in `props`. This subscription should call the `valueChangedCallback` parameter whenever a subscription changes.

For example:
```js
function subscribe(props: Props, valueChangedCallback: (value: any) => void) {
const {scrollContainer} = props;
const onScroll = event => valueChangedCallback(scrollContainer.scrollTop);
scrollContainer.addEventListener("scroll", onScroll);
return onScroll;
}
```

#### `unsubscribe: (props: Props, subscription: Subscription) => void`

Unsubsribe from the subscribable value in `props`. The value returned by `subscribe()` is the second, `subscription` parameter.

For example:
```js
function unsubscribe(props, subscription) {
props.scrollContainer.removeEventListener("scroll", subscription);
}
```

# How it works

Depending on the type of React component specified, `create-component-with-subscriptions` will either create a wrapper component or use a mixin technique.

If a stateless functional component is specified, a high-order component will be wrapped around it. The wrapper will pass through all `props`. The subscribed value will be passed in place of the "subscribable" prop though.

Given the above example, a stateless functional component would look something like this:
```js
function ExampleComponent({ scrollTop, ...rest }) {
// Render ...
}
```

If a class (or `create-react-class`) component is specified, the library uses an ["ES6 mixin"](https://gist.github.com/sebmarkbage/fac0830dbb13ccbff596) technique in order to preserve compatibility with refs and to avoid the overhead of an additional fiber. In this case, the subscription value will be stored in `state` (using the same `property` name) and be accessed from within the `render` method.

Given the above example, a class component would look something like this:
```js
class ExampleComponent extends React.Component {
render() {
const { scrollTop } = this.state;
// Render ...
}
}
```

Examples of both [functional](#subscribing-to-event-dispatchers) and [class](#subscribing-to-a-promise) components are provided below.

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

## Subscribing to event dispatchers

Below is an example showing how `create-component-with-subscriptions` can be used to subscribe to event dispatchers such as DOM elements or Flux stores.

```js
import React from "react";
import createComponent from "create-component-with-subscriptions";

// Start with a simple component.
// In this case, it's a functional component, but it could have been a class.
function InnerComponent({ followers, username }) {
return (
<div>
{username} has {followers} follower
</div>
);
}

// 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 FollowerCountComponent = createComponent(
{
property: "followers",
getValue: props => props.followers.value,
subscribe: (props, valueChangedCallback) => {
const { followers } = props;
const onChange = event => valueChangedCallback(followers.value);
followers.addEventListener("change", onChange);
return onChange;
},
unsubscribe: (props, subscription) => {
// `subscription` is the value returned from subscribe, our event handler.
props.followers.removeEventListener("change", subscription);
}
},
InnerComponent
);

// Your component can now be used as shown below.
// In this example, `followerStore` represents a generic event dispatcher.
<FollowerCountComponent followers={followersStore} username="Brian" />;
```

## Subscribing to observables

Below are examples showing how `create-component-with-subscriptions` 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 SubscribedComponent = createComponent(
{
property: "behaviorSubject",
getValue: props => props.behaviorSubject.getValue(),
subscribe: (props, valueChangedCallback) =>
props.behaviorSubject.subscribe(valueChangedCallback),
unsubscribe: (props, subscription) => subscription.unsubscribe()
},
({ behaviorSubject }) => {
// Render ...
}
);
```

### `ReplaySubject`
```js
const SubscribedComponent = createComponent(
{
property: "replaySubject",
getValue: props => {
let currentValue;
// ReplaySubject does not have a sync data getter,
// So we need to temporarily subscribe to retrieve the most recent value.
const temporarySubscription = props.replaySubject.subscribe(value => {
currentValue = value;
});
temporarySubscription.unsubscribe();
return currentValue;
},
subscribe: (props, valueChangedCallback) =>
props.replaySubject.subscribe(valueChangedCallback),
unsubscribe: (props, subscription) => subscription.unsubscribe()
},
({ replaySubject }) => {
// Render ...
}
);
```

## Subscribing to a Promise

Below is an example showing how `create-component-with-subscriptions` 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 createComponent from "create-component-with-subscriptions";

// Start with a simple component.
function InnerComponent({ loadingStatus }) {
if (loadingStatus === undefined) {
// Loading
} else if (loadingStatus) {
// Success
} else {
// Error
}
}

// 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 LoadingComponent = createComponent(
{
property: "loadingStatus",
getValue: (props, subscription) => {
// There is no way to synchronously read a Promise's value,
// So this method should return undefined.
return undefined;
},
subscribe: (props, valueChangedCallback) => {
props.loadingStatus.then(
// Success
() => valueChangedCallback(true),
// Failure
() => valueChangedCallback(false)
);
},
unsubscribe: (props, subscription) => {
// There is no way to "unsubscribe" from a Promise.
// In this case, create-component-with-subscriptions will block stale values from rendering.
}
},
InnerComponent
);

// Your component can now be used as shown below.
<LoadingComponent loadingStatus={loadingPromise} />;
```

## Optional parameters and default values

Subscribable properties are treated as optional by `create-component-with-subscriptions`. In the event that a subscribable `prop` is missing, a value of `undefined` will be passed to the decorated component (using `props` for a functional component or `state` for a class component).

If you would like to set default values for missing subscriptions, you can do this as shown below.

For functional components, declare a default value while destructuring the `props` parameter:
```js
function InnerComponent({ followers = 0 }) {
return <div>You have {followers} followers.</div>;
}
```

For class components, declare a default value while destructuring `state`:
```js
class InnerComponent extends React.Component {
state = {};
render() {
const { followers = 0 } = this.state;
return <div>You have {followers} followers.</div>;
}
}
```

## Subscribing to multiple sources

It is possible for a single component to subscribe to multiple data sources. To do this, compose the return value of `create-component-with-subscriptions` as shown below:

```js
function InnerComponent({ bar, foo }) {
// Render ...
}

const MultiSubscriptionComponent = createComponent(
{
property: "promiseTwo",
getValue: props => props.promiseTwo.getValue(),
subscribe: (props, valueChangedCallback) =>
props.promiseTwo.subscribe(valueChangedCallback),
unsubscribe: (props, subscription) => subscription.unsubscribe()
},
createComponent(
{
property: "promiseTwo",
getValue: props => props.promiseTwo.getValue(),
subscribe: (props, valueChangedCallback) =>
props.promiseTwo.subscribe(valueChangedCallback),
unsubscribe: (props, subscription) => subscription.unsubscribe()
},
InnerComponent
)
);

// Your component can now be used as shown below.
<MultiSubscriptionComponent promiseOne={promiseOne} promiseTwo={promiseTwo} />;
```
12 changes: 12 additions & 0 deletions packages/create-component-with-subscriptions/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/createComponentWithSubscriptions';
7 changes: 7 additions & 0 deletions packages/create-component-with-subscriptions/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-component-with-subscriptions.production.min.js');
} else {
module.exports = require('./cjs/create-component-with-subscriptions.development.js');
}
22 changes: 22 additions & 0 deletions packages/create-component-with-subscriptions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "create-component-with-subscriptions",
"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": {
"create-react-class": "^15.6.3",
"react-lifecycles-compat": "^1.0.2"
}
}
Loading