Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
POC for create-component-with-subscriptions
- Loading branch information
Showing
6 changed files
with
472 additions
and
0 deletions.
There are no files selected for viewing
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 |
---|---|---|
@@ -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); | ||
``` |
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 |
---|---|---|
@@ -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'; |
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 |
---|---|---|
@@ -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'); | ||
} |
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 |
---|---|---|
@@ -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" | ||
} | ||
} |
171 changes: 171 additions & 0 deletions
171
...reate-component-with-subscriptions/src/__tests__/createComponentWithSubscriptions-test.js
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 |
---|---|---|
@@ -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([]); | ||
}); | ||
}); |
Oops, something went wrong.