Skip to content

Commit 47af8a3

Browse files
committed
POC for create-component-with-subscriptions
1 parent 1d220ce commit 47af8a3

File tree

6 files changed

+465
-0
lines changed

6 files changed

+465
-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,163 @@
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 ReactNoop;
15+
let ReactTestRenderer;
16+
17+
describe('CreateComponentWithSubscriptions', () => {
18+
beforeEach(() => {
19+
jest.resetModules();
20+
createComponent = require('create-component-with-subscriptions')
21+
.createComponent;
22+
React = require('react');
23+
ReactNoop = require('react-noop-renderer');
24+
ReactTestRenderer = require('react-test-renderer');
25+
});
26+
27+
function createFauxObservable() {
28+
let currentValue;
29+
let subscribedCallback = null;
30+
return {
31+
getValue: () => currentValue,
32+
subscribe: callback => {
33+
expect(subscribedCallback).toBe(null);
34+
subscribedCallback = callback;
35+
return {
36+
unsubscribe: () => {
37+
expect(subscribedCallback).not.toBe(null);
38+
subscribedCallback = null;
39+
},
40+
};
41+
},
42+
update: value => {
43+
currentValue = value;
44+
if (typeof subscribedCallback === 'function') {
45+
subscribedCallback(value);
46+
}
47+
},
48+
};
49+
}
50+
51+
it('supports basic subscription pattern', () => {
52+
const renderedValues = [];
53+
let data = null;
54+
let changeCallback = null;
55+
56+
const Component = createComponent(
57+
{
58+
subscribablePropertiesMap: {observable: 'value'},
59+
getDataFor: (subscribable, propertyName) => {
60+
expect(propertyName).toBe('observable');
61+
return data;
62+
},
63+
subscribeTo: (valueChangedCallback, subscribable, propertyName) => {
64+
expect(propertyName).toBe('observable');
65+
expect(changeCallback).toBe(null);
66+
changeCallback = valueChangedCallback;
67+
},
68+
unsubscribeFrom: (subscribable, propertyName, subscription) => {
69+
expect(propertyName).toBe('observable');
70+
expect(typeof changeCallback).toBe('function');
71+
changeCallback = null;
72+
},
73+
},
74+
({value}) => {
75+
renderedValues.push(value);
76+
return null;
77+
},
78+
);
79+
80+
const render = ReactTestRenderer.create(<Component observable={{}} />);
81+
82+
expect(renderedValues).toEqual([null]);
83+
expect(typeof changeCallback).toBe('function');
84+
changeCallback(123);
85+
expect(renderedValues).toEqual([null, 123]);
86+
changeCallback('abc');
87+
expect(renderedValues).toEqual([null, 123, 'abc']);
88+
89+
render.update(<Component observable={null} />);
90+
expect(changeCallback).toBe(null);
91+
expect(renderedValues).toEqual([null, 123, 'abc', undefined]);
92+
});
93+
94+
it('supports multiple subscriptions', () => {
95+
const renderedValues = [];
96+
97+
const Component = createComponent(
98+
{
99+
subscribablePropertiesMap: {
100+
foo: 'foo',
101+
bar: 'bar',
102+
},
103+
getDataFor: (subscribable, propertyName) => {
104+
switch (propertyName) {
105+
case 'foo':
106+
return foo.getValue();
107+
case 'bar':
108+
return bar.getValue();
109+
default:
110+
throw Error('Unexpected propertyName ' + propertyName);
111+
}
112+
},
113+
subscribeTo: (valueChangedCallback, subscribable, propertyName) => {
114+
switch (propertyName) {
115+
case 'foo':
116+
return foo.subscribe(valueChangedCallback);
117+
case 'bar':
118+
return bar.subscribe(valueChangedCallback);
119+
default:
120+
throw Error('Unexpected propertyName ' + propertyName);
121+
}
122+
},
123+
unsubscribeFrom: (subscribable, propertyName, subscription) => {
124+
switch (propertyName) {
125+
case 'foo':
126+
case 'bar':
127+
subscription.unsubscribe();
128+
break;
129+
default:
130+
throw Error('Unexpected propertyName ' + propertyName);
131+
}
132+
},
133+
},
134+
({foo, bar}) => {
135+
renderedValues.push({foo, bar});
136+
return null;
137+
},
138+
);
139+
140+
const foo = createFauxObservable();
141+
const bar = createFauxObservable();
142+
const render = ReactTestRenderer.create(<Component foo={foo} bar={bar} />);
143+
144+
expect(renderedValues).toEqual([{bar: undefined, foo: undefined}]);
145+
renderedValues.length = 0;
146+
foo.update(123);
147+
expect(renderedValues).toEqual([{bar: undefined, foo: 123}]);
148+
renderedValues.length = 0;
149+
bar.update('abc');
150+
expect(renderedValues).toEqual([{bar: 'abc', foo: 123}]);
151+
renderedValues.length = 0;
152+
foo.update(456);
153+
expect(renderedValues).toEqual([{bar: 'abc', foo: 456}]);
154+
155+
renderedValues.length = 0;
156+
render.update(<Component />);
157+
expect(renderedValues).toEqual([{bar: undefined, foo: undefined}]);
158+
159+
renderedValues.length = 0;
160+
foo.update(789);
161+
expect(renderedValues).toEqual([]);
162+
});
163+
});

0 commit comments

Comments
 (0)