-
Notifications
You must be signed in to change notification settings - Fork 47k
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
465 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" | ||
} | ||
} |
163 changes: 163 additions & 0 deletions
163
...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,163 @@ | ||
/** | ||
* 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 ReactNoop; | ||
let ReactTestRenderer; | ||
|
||
describe('CreateComponentWithSubscriptions', () => { | ||
beforeEach(() => { | ||
jest.resetModules(); | ||
createComponent = require('create-component-with-subscriptions') | ||
.createComponent; | ||
React = require('react'); | ||
ReactNoop = require('react-noop-renderer'); | ||
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 = []; | ||
let data = null; | ||
let changeCallback = null; | ||
|
||
const Component = createComponent( | ||
{ | ||
subscribablePropertiesMap: {observable: 'value'}, | ||
getDataFor: (subscribable, propertyName) => { | ||
expect(propertyName).toBe('observable'); | ||
return data; | ||
}, | ||
subscribeTo: (valueChangedCallback, subscribable, propertyName) => { | ||
expect(propertyName).toBe('observable'); | ||
expect(changeCallback).toBe(null); | ||
changeCallback = valueChangedCallback; | ||
}, | ||
unsubscribeFrom: (subscribable, propertyName, subscription) => { | ||
expect(propertyName).toBe('observable'); | ||
expect(typeof changeCallback).toBe('function'); | ||
changeCallback = null; | ||
}, | ||
}, | ||
({value}) => { | ||
renderedValues.push(value); | ||
return null; | ||
}, | ||
); | ||
|
||
const render = ReactTestRenderer.create(<Component observable={{}} />); | ||
|
||
expect(renderedValues).toEqual([null]); | ||
expect(typeof changeCallback).toBe('function'); | ||
changeCallback(123); | ||
expect(renderedValues).toEqual([null, 123]); | ||
changeCallback('abc'); | ||
expect(renderedValues).toEqual([null, 123, 'abc']); | ||
|
||
render.update(<Component observable={null} />); | ||
expect(changeCallback).toBe(null); | ||
expect(renderedValues).toEqual([null, 123, 'abc', undefined]); | ||
}); | ||
|
||
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} />); | ||
|
||
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}]); | ||
|
||
renderedValues.length = 0; | ||
render.update(<Component />); | ||
expect(renderedValues).toEqual([{bar: undefined, foo: undefined}]); | ||
|
||
renderedValues.length = 0; | ||
foo.update(789); | ||
expect(renderedValues).toEqual([]); | ||
}); | ||
}); |
Oops, something went wrong.