Skip to content

Commit

Permalink
POC for create-component-with-subscriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
bvaughn committed Mar 5, 2018
1 parent 1d220ce commit d5d8bf6
Show file tree
Hide file tree
Showing 6 changed files with 472 additions and 0 deletions.
81 changes: 81 additions & 0 deletions packages/create-component-with-subscriptions/README.md
@@ -0,0 +1,81 @@
# create-component-with-subscriptions

Better docs coming soon...

```js
// Here is an example of using the subscribable HOC.
// It shows a couple of potentially common subscription types.
function ExampleComponent(props: Props) {
const {
observedValue,
relayData,
scrollTop,
} = props;

// The rendered output is not interesting.
// The interesting thing is the incoming props/values.
}

function getDataFor(subscribable, propertyName) {
switch (propertyName) {
case 'fragmentResolver':
return subscribable.resolve();
case 'observableStream':
// This only works for some observable types (e.g. BehaviorSubject)
// It's okay to just return null/undefined here for other types.
return subscribable.getValue();
case 'scrollTarget':
return subscribable.scrollTop;
default:
throw Error(`Invalid subscribable, "${propertyName}", specified.`);
}
}

function subscribeTo(valueChangedCallback, subscribable, propertyName) {
switch (propertyName) {
case 'fragmentResolver':
subscribable.setCallback(
() => valueChangedCallback(subscribable.resolve()
);
break;
case 'observableStream':
// Return the subscription; it's necessary to unsubscribe.
return subscribable.subscribe(valueChangedCallback);
case 'scrollTarget':
const onScroll = () => valueChangedCallback(subscribable.scrollTop);
subscribable.addEventListener(onScroll);
return onScroll;
default:
throw Error(`Invalid subscribable, "${propertyName}", specified.`);
}
}

function unsubscribeFrom(subscribable, propertyName, subscription) {
switch (propertyName) {
case 'fragmentResolver':
subscribable.dispose();
break;
case 'observableStream':
// Unsubscribe using the subscription rather than the subscribable.
subscription.unsubscribe();
case 'scrollTarget':
// In this case, 'subscription', is the event handler/function.
subscribable.removeEventListener(subscription);
break;
default:
throw Error(`Invalid subscribable, "${propertyName}", specified.`);
}
}

// 3: This is the component you would export.
createSubscribable({
subscribablePropertiesMap: {
fragmentResolver: 'relayData',
observableStream: 'observedValue',
scrollTarget: 'scrollTop',
},
getDataFor,
subscribeTo,
unsubscribeFrom,
}, ExampleComponent);
```
12 changes: 12 additions & 0 deletions packages/create-component-with-subscriptions/index.js
@@ -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
@@ -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');
}
13 changes: 13 additions & 0 deletions packages/create-component-with-subscriptions/package.json
@@ -0,0 +1,13 @@
{
"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"
}
}
@@ -0,0 +1,171 @@
/**
* 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.
*
* @emails react-core
*/

'use strict';

let createComponent;
let React;
let ReactTestRenderer;

describe('CreateComponentWithSubscriptions', () => {
beforeEach(() => {
jest.resetModules();
createComponent = require('create-component-with-subscriptions')
.createComponent;
React = require('react');
ReactTestRenderer = require('react-test-renderer');
});

function createFauxObservable() {
let currentValue;
let subscribedCallback = null;
return {
getValue: () => currentValue,
subscribe: callback => {
expect(subscribedCallback).toBe(null);
subscribedCallback = callback;
return {
unsubscribe: () => {
expect(subscribedCallback).not.toBe(null);
subscribedCallback = null;
},
};
},
update: value => {
currentValue = value;
if (typeof subscribedCallback === 'function') {
subscribedCallback(value);
}
},
};
}

it('supports basic subscription pattern', () => {
const renderedValues = [];

const Component = createComponent(
{
subscribablePropertiesMap: {observable: 'value'},
getDataFor: (subscribable, propertyName) => {
expect(propertyName).toBe('observable');
return observable.getValue();
},
subscribeTo: (valueChangedCallback, subscribable, propertyName) => {
expect(propertyName).toBe('observable');
return subscribable.subscribe(valueChangedCallback);
},
unsubscribeFrom: (subscribable, propertyName, subscription) => {
expect(propertyName).toBe('observable');
subscription.unsubscribe();
},
},
({value}) => {
renderedValues.push(value);
return null;
},
);

const observable = createFauxObservable();
const render = ReactTestRenderer.create(
<Component observable={observable} />,
);

// Updates while subscribed should re-render the child component
expect(renderedValues).toEqual([undefined]);
renderedValues.length = 0;
observable.update(123);
expect(renderedValues).toEqual([123]);
renderedValues.length = 0;
observable.update('abc');
expect(renderedValues).toEqual(['abc']);

// Unsetting the subscriber prop should reset subscribed values
renderedValues.length = 0;
render.update(<Component observable={null} />);
expect(renderedValues).toEqual([undefined]);

// Updates while unsubscribed should not re-render the child component
renderedValues.length = 0;
observable.update(789);
expect(renderedValues).toEqual([]);
});

it('supports multiple subscriptions', () => {
const renderedValues = [];

const Component = createComponent(
{
subscribablePropertiesMap: {
foo: 'foo',
bar: 'bar',
},
getDataFor: (subscribable, propertyName) => {
switch (propertyName) {
case 'foo':
return foo.getValue();
case 'bar':
return bar.getValue();
default:
throw Error('Unexpected propertyName ' + propertyName);
}
},
subscribeTo: (valueChangedCallback, subscribable, propertyName) => {
switch (propertyName) {
case 'foo':
return foo.subscribe(valueChangedCallback);
case 'bar':
return bar.subscribe(valueChangedCallback);
default:
throw Error('Unexpected propertyName ' + propertyName);
}
},
unsubscribeFrom: (subscribable, propertyName, subscription) => {
switch (propertyName) {
case 'foo':
case 'bar':
subscription.unsubscribe();
break;
default:
throw Error('Unexpected propertyName ' + propertyName);
}
},
},
({foo, bar}) => {
renderedValues.push({foo, bar});
return null;
},
);

const foo = createFauxObservable();
const bar = createFauxObservable();
const render = ReactTestRenderer.create(<Component foo={foo} bar={bar} />);

// Updates while subscribed should re-render the child component
expect(renderedValues).toEqual([{bar: undefined, foo: undefined}]);
renderedValues.length = 0;
foo.update(123);
expect(renderedValues).toEqual([{bar: undefined, foo: 123}]);
renderedValues.length = 0;
bar.update('abc');
expect(renderedValues).toEqual([{bar: 'abc', foo: 123}]);
renderedValues.length = 0;
foo.update(456);
expect(renderedValues).toEqual([{bar: 'abc', foo: 456}]);

// Unsetting the subscriber prop should reset subscribed values
renderedValues.length = 0;
render.update(<Component />);
expect(renderedValues).toEqual([{bar: undefined, foo: undefined}]);

// Updates while unsubscribed should not re-render the child component
renderedValues.length = 0;
foo.update(789);
expect(renderedValues).toEqual([]);
});
});

0 comments on commit d5d8bf6

Please sign in to comment.