Skip to content

Commit d5d8bf6

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

File tree

6 files changed

+472
-0
lines changed

6 files changed

+472
-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,171 @@
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+
52+
const Component = createComponent(
53+
{
54+
subscribablePropertiesMap: {observable: 'value'},
55+
getDataFor: (subscribable, propertyName) => {
56+
expect(propertyName).toBe('observable');
57+
return observable.getValue();
58+
},
59+
subscribeTo: (valueChangedCallback, subscribable, propertyName) => {
60+
expect(propertyName).toBe('observable');
61+
return subscribable.subscribe(valueChangedCallback);
62+
},
63+
unsubscribeFrom: (subscribable, propertyName, subscription) => {
64+
expect(propertyName).toBe('observable');
65+
subscription.unsubscribe();
66+
},
67+
},
68+
({value}) => {
69+
renderedValues.push(value);
70+
return null;
71+
},
72+
);
73+
74+
const observable = createFauxObservable();
75+
const render = ReactTestRenderer.create(
76+
<Component observable={observable} />,
77+
);
78+
79+
// Updates while subscribed should re-render the child component
80+
expect(renderedValues).toEqual([undefined]);
81+
renderedValues.length = 0;
82+
observable.update(123);
83+
expect(renderedValues).toEqual([123]);
84+
renderedValues.length = 0;
85+
observable.update('abc');
86+
expect(renderedValues).toEqual(['abc']);
87+
88+
// Unsetting the subscriber prop should reset subscribed values
89+
renderedValues.length = 0;
90+
render.update(<Component observable={null} />);
91+
expect(renderedValues).toEqual([undefined]);
92+
93+
// Updates while unsubscribed should not re-render the child component
94+
renderedValues.length = 0;
95+
observable.update(789);
96+
expect(renderedValues).toEqual([]);
97+
});
98+
99+
it('supports multiple subscriptions', () => {
100+
const renderedValues = [];
101+
102+
const Component = createComponent(
103+
{
104+
subscribablePropertiesMap: {
105+
foo: 'foo',
106+
bar: 'bar',
107+
},
108+
getDataFor: (subscribable, propertyName) => {
109+
switch (propertyName) {
110+
case 'foo':
111+
return foo.getValue();
112+
case 'bar':
113+
return bar.getValue();
114+
default:
115+
throw Error('Unexpected propertyName ' + propertyName);
116+
}
117+
},
118+
subscribeTo: (valueChangedCallback, subscribable, propertyName) => {
119+
switch (propertyName) {
120+
case 'foo':
121+
return foo.subscribe(valueChangedCallback);
122+
case 'bar':
123+
return bar.subscribe(valueChangedCallback);
124+
default:
125+
throw Error('Unexpected propertyName ' + propertyName);
126+
}
127+
},
128+
unsubscribeFrom: (subscribable, propertyName, subscription) => {
129+
switch (propertyName) {
130+
case 'foo':
131+
case 'bar':
132+
subscription.unsubscribe();
133+
break;
134+
default:
135+
throw Error('Unexpected propertyName ' + propertyName);
136+
}
137+
},
138+
},
139+
({foo, bar}) => {
140+
renderedValues.push({foo, bar});
141+
return null;
142+
},
143+
);
144+
145+
const foo = createFauxObservable();
146+
const bar = createFauxObservable();
147+
const render = ReactTestRenderer.create(<Component foo={foo} bar={bar} />);
148+
149+
// Updates while subscribed should re-render the child component
150+
expect(renderedValues).toEqual([{bar: undefined, foo: undefined}]);
151+
renderedValues.length = 0;
152+
foo.update(123);
153+
expect(renderedValues).toEqual([{bar: undefined, foo: 123}]);
154+
renderedValues.length = 0;
155+
bar.update('abc');
156+
expect(renderedValues).toEqual([{bar: 'abc', foo: 123}]);
157+
renderedValues.length = 0;
158+
foo.update(456);
159+
expect(renderedValues).toEqual([{bar: 'abc', foo: 456}]);
160+
161+
// Unsetting the subscriber prop should reset subscribed values
162+
renderedValues.length = 0;
163+
render.update(<Component />);
164+
expect(renderedValues).toEqual([{bar: undefined, foo: undefined}]);
165+
166+
// Updates while unsubscribed should not re-render the child component
167+
renderedValues.length = 0;
168+
foo.update(789);
169+
expect(renderedValues).toEqual([]);
170+
});
171+
});

0 commit comments

Comments
 (0)