Skip to content

Commit 11e741c

Browse files
committed
POC for create-component-with-subscriptions
1 parent 1d220ce commit 11e741c

File tree

6 files changed

+462
-0
lines changed

6 files changed

+462
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# create-component-with-subscriptions
2+
3+
Better docs coming soon...
4+
5+
```js
6+
// Here is an example of using the subscribable HOC.
7+
// It shows a couple of potentially common subscription types.
8+
function ExampleComponent(props: Props) {
9+
const {
10+
observedValue,
11+
relayData,
12+
scrollTop,
13+
} = props;
14+
15+
// The rendered output is not interesting.
16+
// The interesting thing is the incoming props/values.
17+
}
18+
19+
function getDataFor(subscribable, propertyName) {
20+
switch (propertyName) {
21+
case 'fragmentResolver':
22+
return subscribable.resolve();
23+
case 'observableStream':
24+
// This only works for some observable types (e.g. BehaviorSubject)
25+
// It's okay to just return null/undefined here for other types.
26+
return subscribable.getValue();
27+
case 'scrollTarget':
28+
return subscribable.scrollTop;
29+
default:
30+
throw Error(`Invalid subscribable, "${propertyName}", specified.`);
31+
}
32+
}
33+
34+
function subscribeTo(valueChangedCallback, subscribable, propertyName) {
35+
switch (propertyName) {
36+
case 'fragmentResolver':
37+
subscribable.setCallback(
38+
() => valueChangedCallback(subscribable.resolve()
39+
);
40+
break;
41+
case 'observableStream':
42+
// Return the subscription; it's necessary to unsubscribe.
43+
return subscribable.subscribe(valueChangedCallback);
44+
case 'scrollTarget':
45+
const onScroll = () => valueChangedCallback(subscribable.scrollTop);
46+
subscribable.addEventListener(onScroll);
47+
return onScroll;
48+
default:
49+
throw Error(`Invalid subscribable, "${propertyName}", specified.`);
50+
}
51+
}
52+
53+
function unsubscribeFrom(subscribable, propertyName, subscription) {
54+
switch (propertyName) {
55+
case 'fragmentResolver':
56+
subscribable.dispose();
57+
break;
58+
case 'observableStream':
59+
// Unsubscribe using the subscription rather than the subscribable.
60+
subscription.unsubscribe();
61+
case 'scrollTarget':
62+
// In this case, 'subscription', is the event handler/function.
63+
subscribable.removeEventListener(subscription);
64+
break;
65+
default:
66+
throw Error(`Invalid subscribable, "${propertyName}", specified.`);
67+
}
68+
}
69+
70+
// 3: This is the component you would export.
71+
createSubscribable({
72+
subscribablePropertiesMap: {
73+
fragmentResolver: 'relayData',
74+
observableStream: 'observedValue',
75+
scrollTarget: 'scrollTop',
76+
},
77+
getDataFor,
78+
subscribeTo,
79+
unsubscribeFrom,
80+
}, ExampleComponent);
81+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
'use strict';
11+
12+
export * from './src/createComponentWithSubscriptions';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
3+
if (process.env.NODE_ENV === 'production') {
4+
module.exports = require('./cjs/create-component-with-subscriptions.production.min.js');
5+
} else {
6+
module.exports = require('./cjs/create-component-with-subscriptions.development.js');
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "create-component-with-subscriptions",
3+
"description": "HOC for creating async-safe React components with subscriptions",
4+
"version": "0.0.1",
5+
"repository": "facebook/react",
6+
"files": ["LICENSE", "README.md", "index.js", "cjs/"],
7+
"dependencies": {
8+
"fbjs": "^0.8.16"
9+
},
10+
"peerDependencies": {
11+
"react": "16.3.0-alpha.1"
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
*/
9+
10+
'use strict';
11+
12+
let createComponent;
13+
let React;
14+
let ReactTestRenderer;
15+
16+
describe('CreateComponentWithSubscriptions', () => {
17+
beforeEach(() => {
18+
jest.resetModules();
19+
createComponent = require('create-component-with-subscriptions')
20+
.createComponent;
21+
React = require('react');
22+
ReactTestRenderer = require('react-test-renderer');
23+
});
24+
25+
function createFauxObservable() {
26+
let currentValue;
27+
let subscribedCallback = null;
28+
return {
29+
getValue: () => currentValue,
30+
subscribe: callback => {
31+
expect(subscribedCallback).toBe(null);
32+
subscribedCallback = callback;
33+
return {
34+
unsubscribe: () => {
35+
expect(subscribedCallback).not.toBe(null);
36+
subscribedCallback = null;
37+
},
38+
};
39+
},
40+
update: value => {
41+
currentValue = value;
42+
if (typeof subscribedCallback === 'function') {
43+
subscribedCallback(value);
44+
}
45+
},
46+
};
47+
}
48+
49+
it('supports basic subscription pattern', () => {
50+
const renderedValues = [];
51+
let data = null;
52+
let changeCallback = null;
53+
54+
const Component = createComponent(
55+
{
56+
subscribablePropertiesMap: {observable: 'value'},
57+
getDataFor: (subscribable, propertyName) => {
58+
expect(propertyName).toBe('observable');
59+
return data;
60+
},
61+
subscribeTo: (valueChangedCallback, subscribable, propertyName) => {
62+
expect(propertyName).toBe('observable');
63+
expect(changeCallback).toBe(null);
64+
changeCallback = valueChangedCallback;
65+
},
66+
unsubscribeFrom: (subscribable, propertyName, subscription) => {
67+
expect(propertyName).toBe('observable');
68+
expect(typeof changeCallback).toBe('function');
69+
changeCallback = null;
70+
},
71+
},
72+
({value}) => {
73+
renderedValues.push(value);
74+
return null;
75+
},
76+
);
77+
78+
const render = ReactTestRenderer.create(<Component observable={{}} />);
79+
80+
expect(renderedValues).toEqual([null]);
81+
expect(typeof changeCallback).toBe('function');
82+
changeCallback(123);
83+
expect(renderedValues).toEqual([null, 123]);
84+
changeCallback('abc');
85+
expect(renderedValues).toEqual([null, 123, 'abc']);
86+
87+
render.update(<Component observable={null} />);
88+
expect(changeCallback).toBe(null);
89+
expect(renderedValues).toEqual([null, 123, 'abc', undefined]);
90+
});
91+
92+
it('supports multiple subscriptions', () => {
93+
const renderedValues = [];
94+
95+
const Component = createComponent(
96+
{
97+
subscribablePropertiesMap: {
98+
foo: 'foo',
99+
bar: 'bar',
100+
},
101+
getDataFor: (subscribable, propertyName) => {
102+
switch (propertyName) {
103+
case 'foo':
104+
return foo.getValue();
105+
case 'bar':
106+
return bar.getValue();
107+
default:
108+
throw Error('Unexpected propertyName ' + propertyName);
109+
}
110+
},
111+
subscribeTo: (valueChangedCallback, subscribable, propertyName) => {
112+
switch (propertyName) {
113+
case 'foo':
114+
return foo.subscribe(valueChangedCallback);
115+
case 'bar':
116+
return bar.subscribe(valueChangedCallback);
117+
default:
118+
throw Error('Unexpected propertyName ' + propertyName);
119+
}
120+
},
121+
unsubscribeFrom: (subscribable, propertyName, subscription) => {
122+
switch (propertyName) {
123+
case 'foo':
124+
case 'bar':
125+
subscription.unsubscribe();
126+
break;
127+
default:
128+
throw Error('Unexpected propertyName ' + propertyName);
129+
}
130+
},
131+
},
132+
({foo, bar}) => {
133+
renderedValues.push({foo, bar});
134+
return null;
135+
},
136+
);
137+
138+
const foo = createFauxObservable();
139+
const bar = createFauxObservable();
140+
const render = ReactTestRenderer.create(<Component foo={foo} bar={bar} />);
141+
142+
expect(renderedValues).toEqual([{bar: undefined, foo: undefined}]);
143+
renderedValues.length = 0;
144+
foo.update(123);
145+
expect(renderedValues).toEqual([{bar: undefined, foo: 123}]);
146+
renderedValues.length = 0;
147+
bar.update('abc');
148+
expect(renderedValues).toEqual([{bar: 'abc', foo: 123}]);
149+
renderedValues.length = 0;
150+
foo.update(456);
151+
expect(renderedValues).toEqual([{bar: 'abc', foo: 456}]);
152+
153+
renderedValues.length = 0;
154+
render.update(<Component />);
155+
expect(renderedValues).toEqual([{bar: undefined, foo: undefined}]);
156+
157+
renderedValues.length = 0;
158+
foo.update(789);
159+
expect(renderedValues).toEqual([]);
160+
});
161+
});

0 commit comments

Comments
 (0)