-
Notifications
You must be signed in to change notification settings - Fork 46.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
109 additions
and
64 deletions.
There are no files selected for viewing
173 changes: 109 additions & 64 deletions
173
packages/create-component-with-subscriptions/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,81 +1,126 @@ | ||
# create-component-with-subscriptions | ||
|
||
Below is an example showing how the container can be used: | ||
[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: | ||
* **subscribablePropertiesMap** `{[subscribableProperty: string]: string}` - Maps property names of incoming subscribable sources (e.g. "eventDispatcher") to property names for their values (e.g. "value"). | ||
* **getDataFor** `(subscribable: any, propertyName: string) => any` - Synchronously returns the value of the specified subscribable property. If your component has multiple subscriptions,the second 'propertyName' parameter can be used to distinguish between them. | ||
* **subscribeTo** `( | ||
valueChangedCallback: (value: any) => void, | ||
subscribable: any, | ||
propertyName: string, | ||
) => any` - Subscribes to the specified subscribable and call the `valueChangedCallback` parameter whenever a subscription changes. If your component has multiple subscriptions, the third 'propertyName' parameter can be used to distinguish between them. | ||
* **unsubscribeFrom** `( | ||
subscribable: any, | ||
propertyName: string, | ||
subscription: any, | ||
) => void` - Unsubscribes from the specified subscribable. If your component has multiple subscriptions, the second `propertyName` parameter can be used to distinguish between them. The value returned by `subscribeTo()` is the third `subscription` parameter. | ||
|
||
# Examples | ||
|
||
## 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 | ||
// This is an example functional component that subscribes to some values. | ||
function ExampleComponent({ | ||
examplePassThroughProperty, | ||
friendsList, | ||
userProfile | ||
}) { | ||
// The rendered output of this component is not very important. | ||
// It just exists to show how the observed values are provided. | ||
// Properties not related to subscriptions are passed through as-is, | ||
// (e.g. examplePassThroughProperty). | ||
} | ||
import React from "react"; | ||
import createComponent from "create-component-with-subscriptions"; | ||
|
||
// In the below example, "friendsList" mimics an RxJS BehaviorSubject, | ||
// and "userProfile" mimics an event dispatcher (like a DOM element). | ||
function getDataFor(subscribable, propertyName) { | ||
switch (propertyName) { | ||
case "friendsListSubject": | ||
return subscribable.getValue(); | ||
case "userProfile": | ||
return subscribable.value; | ||
default: | ||
throw Error(`Invalid subscribable, "${propertyName}", specified.`); | ||
} | ||
// Start with a simple functional (or class-based) component. | ||
function InnerComponent({ followerCount, username }) { | ||
return ( | ||
<div> | ||
{username} has {followerCount} follower | ||
</div> | ||
); | ||
} | ||
|
||
function subscribeTo(valueChangedCallback, subscribable, propertyName) { | ||
switch (propertyName) { | ||
case "friendsListSubject": | ||
// Return the subscription in this case; it's necessary to unsubscribe. | ||
return subscribable.subscribe(valueChangedCallback); | ||
case "userProfile": | ||
const onChange = () => valueChangedCallback(subscribable.value); | ||
// 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( | ||
{ | ||
subscribablePropertiesMap: { followerStore: "followerCount" }, | ||
getDataFor: (subscribable, propertyName) => subscribable.value, | ||
subscribeTo: (valueChangedCallback, subscribable, propertyName) => { | ||
const onChange = event => valueChangedCallback(subscribable.value); | ||
subscribable.addEventListener(onChange); | ||
// Return the event handling callback, since it's required to unsubscribe. | ||
return onChange; | ||
default: | ||
throw Error(`Invalid subscribable, "${propertyName}", specified.`); | ||
} | ||
} | ||
|
||
function unsubscribeFrom(subscribable, propertyName, subscription) { | ||
switch (propertyName) { | ||
case "friendsListSubject": | ||
// Unsubscribe using the subscription rather than the subscribable. | ||
subscription.unsubscribe(); | ||
case "userProfile": | ||
// In this case, 'subscription', is the event handler/function. | ||
}, | ||
unsubscribeFrom: (subscribable, propertyName, subscription) => { | ||
// `subscription` is the value returned from subscribeTo, our event handler. | ||
subscribable.removeEventListener(subscription); | ||
break; | ||
default: | ||
throw Error(`Invalid subscribable, "${propertyName}", specified.`); | ||
} | ||
} | ||
}, | ||
InnerComponent | ||
); | ||
|
||
// Your component can now be used as shown below. | ||
// (In this example, `followerStore` represents a generic event dispatcher.) | ||
<FollowerCountComponent followerStore={followerStore} username="Brian" />; | ||
``` | ||
|
||
## Subscribing to observables | ||
|
||
Below is an example 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. | ||
|
||
```js | ||
import React from "react"; | ||
import createComponent from "create-component-with-subscriptions"; | ||
|
||
function InnerComponent({ behaviorValue, replayValue }) { | ||
// Render ... | ||
} | ||
|
||
// Map incoming subscriptions property names (e.g. friendsListSubject) | ||
// to property names expected by our functional component (e.g. friendsList). | ||
const subscribablePropertiesMap = { | ||
friendsListSubject: "friendsList", | ||
userProfile: "userProfile" | ||
}; | ||
|
||
// Decorate our functional component with a subscriber component. | ||
// This HOC will automatically manage subscriptions to the incoming props, | ||
// and map them to subscribed values to be passed to the inner component. | ||
// All other props will be passed through as-is. | ||
export default createSubscribable( | ||
const SubscribedComponent = createComponent( | ||
{ | ||
getDataFor, | ||
subscribablePropertiesMap, | ||
subscribeTo, | ||
unsubscribeFrom | ||
subscribablePropertiesMap: { | ||
behaviorSubject: "behaviorValue", | ||
replaySubject: "replayValue" | ||
}, | ||
getDataFor: (subscribable, propertyName) => { | ||
switch (propertyName) { | ||
case "behaviorSubject": | ||
return subscribable.getValue(); | ||
case "replaySubject": | ||
let currentValue; | ||
// ReplaySubject does not have a sync data getter, | ||
// So we need to temporarily subscribe to retrieve the most recent value. | ||
const temporarySubscription = subscribable.subscribe(value => { | ||
currentValue = value; | ||
}); | ||
temporarySubscription.unsubscribe(); | ||
return currentValue; | ||
} | ||
}, | ||
subscribeTo: (valueChangedCallback, subscribable, propertyName) => | ||
subscribable.subscribe(valueChangedCallback), | ||
unsubscribeFrom: (subscribable, propertyName, subscription) => | ||
subscription.unsubscribe() | ||
}, | ||
ExampleComponent | ||
InnerComponent | ||
); | ||
|
||
// Your component can now be used as shown below. | ||
// In this example, both properties below represent RxJS types with the same name. | ||
<SubscribedComponent | ||
behaviorSubject={behaviorSubject} | ||
replaySubject={replaySubject} | ||
/>; | ||
``` |