Skip to content

Commit 428531d

Browse files
committed
feat(createPubSubConnector): Refactored internal code and improved supported subscriptions methods
Normalized behaviour of mapSubscriptionsToProps when is object or function, improved performance and removed not necessary render invocations Breaking: API for mapSubscriptionsToProps is changed
1 parent ed82601 commit 428531d

File tree

5 files changed

+321
-324
lines changed

5 files changed

+321
-324
lines changed
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
import { Component, createElement } from 'react';
2+
import hoistStatics from 'hoist-non-react-statics';
3+
import isPlainObject from '../utils/isPlainObject';
4+
import shallowEqual from '../utils/shallowEqual';
5+
import pubSubShape from '../shapes/pubSubShape';
6+
7+
const defaultMapPublishToProps = publish => ({ publish });
8+
const defaultMapSubscriptionsToProps = () => ({});
9+
const defaultInitMapSubscriptionsToProps = () => defaultMapSubscriptionsToProps;
10+
11+
function getDisplayName(WrappedComponent) {
12+
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
13+
}
14+
15+
function cleanEmptyKeys(obj = {}) {
16+
Object.keys(obj)
17+
.filter(key => obj[key] === undefined || obj[key] === null)
18+
.forEach(key => delete obj[key]);
19+
return obj;
20+
}
21+
22+
function wrapSubscritionsMap(mapSubscriptionsToProps) {
23+
const validMappedSubscriptions = Object.keys(mapSubscriptionsToProps)
24+
.every(
25+
key => (typeof mapSubscriptionsToProps[key] === 'function' ||
26+
typeof mapSubscriptionsToProps[key] === 'string')
27+
);
28+
29+
if (!validMappedSubscriptions) {
30+
throw new Error(
31+
`Every mapped Subscription of "createPubSubConnector" must be a function`
32+
+ `returning the value to be passed as prop to the decorated component.`
33+
);
34+
}
35+
36+
return (pubSub, notifyChange, getProps) => {
37+
const { add } = pubSub;
38+
let map = {};
39+
40+
const updateStoredMapFromObject = (updatedValue) => {
41+
const updatedMap = {};
42+
const keysToUpdate = Object.keys(updatedValue)
43+
.filter(key => {
44+
if (map.hasOwnProperty(key)) {
45+
if (updatedValue[key] && shallowEqual(map[key], updatedValue[key])) {
46+
return false;
47+
}
48+
}
49+
updatedMap[key] = updatedValue[key];
50+
return true;
51+
});
52+
53+
if (keysToUpdate.length) {
54+
map = cleanEmptyKeys(Object.assign({}, map, updatedMap));
55+
return true;
56+
}
57+
return false;
58+
};
59+
60+
const updateStoredMapFromKey = (key, updatedValue) => {
61+
if (map.hasOwnProperty(key)) {
62+
if (updatedValue && shallowEqual(map[key], updatedValue)) {
63+
return false;
64+
}
65+
}
66+
map = cleanEmptyKeys(Object.assign({}, map, { [key]: updatedValue }));
67+
return true;
68+
};
69+
70+
Object
71+
.keys(mapSubscriptionsToProps)
72+
.forEach(key => {
73+
const transformerOrAlias = mapSubscriptionsToProps[key];
74+
if (typeof transformerOrAlias === 'function') {
75+
add(key, (...args) => {
76+
const updatedValue = transformerOrAlias(...args, getProps());
77+
if (updateStoredMapFromObject(updatedValue)) {
78+
notifyChange();
79+
}
80+
});
81+
} else {
82+
add(key, payload => {
83+
if (updateStoredMapFromKey(transformerOrAlias, payload)) {
84+
notifyChange();
85+
}
86+
});
87+
}
88+
});
89+
90+
return () => map;
91+
};
92+
}
93+
94+
function wrapPublishMethods(mapPublishToProps) {
95+
return publish => Object.keys(mapPublishToProps)
96+
.filter(key => typeof mapPublishToProps[key] === 'function')
97+
.reduce((acc, key) => {
98+
acc[key] = (...args) => mapPublishToProps[key](publish, ...args);
99+
return acc;
100+
}, {});
101+
}
102+
103+
export default function createPubSubConnector(mapSubscriptionsToProps, mapPublishToProps, options = {}) {
104+
if (
105+
mapSubscriptionsToProps &&
106+
(typeof mapSubscriptionsToProps !== 'function' && !isPlainObject(mapSubscriptionsToProps))
107+
) {
108+
throw new Error(
109+
`"createPubSubConnector" expected "mapSubscriptionsToProps" to be a function`
110+
+ ` or a plain object, instead received: ${mapSubscriptionsToProps}.`
111+
);
112+
}
113+
const shouldMapSubscriptions = isPlainObject(mapSubscriptionsToProps) ? Object.keys(mapSubscriptionsToProps).length : Boolean(mapSubscriptionsToProps);
114+
115+
let finalMapSubscriptionsToProps = defaultMapSubscriptionsToProps;
116+
let doSubscribedPropsDependOnOwnProps = false;
117+
118+
119+
const finalMapPublishToProps = isPlainObject(mapPublishToProps) ? wrapPublishMethods(mapPublishToProps) : mapPublishToProps || defaultMapPublishToProps;
120+
const doPublishPropsDependOnOwnProps = finalMapPublishToProps.length > 1;
121+
122+
const { withRef = false } = options || {};
123+
124+
function initComputeSubscribedProps(pubSub, notifier, getProps) {
125+
let subscriber;
126+
if (isPlainObject(mapSubscriptionsToProps)) {
127+
subscriber = wrapSubscritionsMap(mapSubscriptionsToProps);
128+
} else {
129+
subscriber = mapSubscriptionsToProps || defaultInitMapSubscriptionsToProps;
130+
}
131+
finalMapSubscriptionsToProps = subscriber(pubSub, notifier, getProps);
132+
doSubscribedPropsDependOnOwnProps = finalMapSubscriptionsToProps.length > 0;
133+
}
134+
135+
function computeSubscribedProps(props) {
136+
const subscribedProps = doSubscribedPropsDependOnOwnProps ? finalMapSubscriptionsToProps(props) : finalMapSubscriptionsToProps();
137+
138+
if (!isPlainObject(subscribedProps)) {
139+
throw new Error(
140+
`'mapSubscriptionsToProps' must return an object.`
141+
+ `Instead received ${subscribedProps}`
142+
);
143+
}
144+
return subscribedProps;
145+
}
146+
147+
function computePublishProps(pubSub, props) {
148+
const { publish } = pubSub;
149+
const publishProps = doPublishPropsDependOnOwnProps ? finalMapPublishToProps(publish, props) : finalMapPublishToProps(publish);
150+
151+
if (!isPlainObject(publishProps)) {
152+
throw new Error(
153+
`'mapPublishToProps' must return an object.`
154+
+ `Instead received ${publishProps}`
155+
);
156+
}
157+
return publishProps;
158+
}
159+
160+
return function wrapComponent(Composed) {
161+
class PubSubConnector extends Component {
162+
163+
constructor(props, context) {
164+
super(props, context);
165+
this.pubSubCore = props.pubSubCore || context.pubSubCore;
166+
167+
if (!this.pubSubCore) {
168+
throw new Error(
169+
`Could not find "pubSubCore" in either the context or `
170+
+ `props of "${this.constructor.displayName}". `
171+
+ `Either wrap the root component in a <PubSubProvider>, `
172+
+ `or explicitly pass "pubSubCore" as a prop to "${this.constructor.displayName}".`
173+
);
174+
}
175+
this.state = {};
176+
this.pubSub = this.pubSubCore.register(this);
177+
}
178+
179+
componentWillMount() {
180+
this.trySubscribe();
181+
}
182+
183+
componentWillReceiveProps(nextProps) {
184+
if (!shallowEqual(nextProps, this.props)) {
185+
this.haveOwnPropsChanged = true;
186+
}
187+
}
188+
189+
shouldComponentUpdate() {
190+
return this.haveOwnPropsChanged || this.hasSubscribedPropsChanged;
191+
}
192+
193+
componentWillUnmount() {
194+
this.pubSub.unsubscribe();
195+
}
196+
197+
getWrappedInstance() {
198+
if (!withRef) {
199+
throw new Error(
200+
`To access the wrapped instance, you need to specify explicitly`
201+
+ ` { withRef: true } in the options passed to the createPubSubConnector() call.`
202+
);
203+
}
204+
return this.refs.wrappedInstance;
205+
}
206+
207+
hasSubscriptions() {
208+
if (this.pubSub) {
209+
return this.pubSub.subscriptions.length ? true : false;
210+
}
211+
return false;
212+
}
213+
214+
notifyChange() {
215+
const prevSubscribedProps = this.state.subscribedProps;
216+
const subscribedProps = finalMapSubscriptionsToProps(this.props);
217+
218+
if (prevSubscribedProps !== subscribedProps) {
219+
this.hasSubscribedPropsChanged = true;
220+
this.setState({ subscribedProps });
221+
}
222+
}
223+
224+
trySubscribe() {
225+
if (shouldMapSubscriptions) {
226+
initComputeSubscribedProps(
227+
this.pubSub, () => this.notifyChange(), () => this.props
228+
);
229+
this.notifyChange();
230+
}
231+
}
232+
233+
updateSubscribedPropsIfNeeded() {
234+
const nextSubscribedProps = computeSubscribedProps(this.props);
235+
if (this.subscribedProps && shallowEqual(nextSubscribedProps, this.subscribedProps)) {
236+
return false;
237+
}
238+
239+
this.subscribedProps = nextSubscribedProps;
240+
return true;
241+
}
242+
243+
updatePublishPropsIfNeeded() {
244+
const nextPublishProps = computePublishProps(this.pubSub, this.props);
245+
if (this.publishProps && shallowEqual(nextPublishProps, this.publishProps)) {
246+
return false;
247+
}
248+
249+
this.publishProps = nextPublishProps;
250+
return true;
251+
}
252+
253+
render() {
254+
const {
255+
haveOwnPropsChanged,
256+
hasSubscribedPropsChanged,
257+
renderedElement,
258+
pubSub,
259+
} = this;
260+
261+
this.haveOwnPropsChanged = false;
262+
this.hasSubscribedPropsChanged = false;
263+
264+
let shouldUpdateSubscribedProps = true;
265+
let shouldUpdatePublishProps = true;
266+
if (renderedElement) {
267+
shouldUpdateSubscribedProps = hasSubscribedPropsChanged || (haveOwnPropsChanged && doSubscribedPropsDependOnOwnProps);
268+
shouldUpdatePublishProps = haveOwnPropsChanged && doPublishPropsDependOnOwnProps;
269+
}
270+
271+
let haveSubscribedPropsChanged = false;
272+
let havePublishPropsChanged = false;
273+
if (shouldUpdateSubscribedProps) {
274+
haveSubscribedPropsChanged = this.updateSubscribedPropsIfNeeded();
275+
}
276+
if (shouldUpdatePublishProps) {
277+
havePublishPropsChanged = this.updatePublishPropsIfNeeded();
278+
}
279+
280+
if (
281+
!haveSubscribedPropsChanged &&
282+
!havePublishPropsChanged &&
283+
!haveOwnPropsChanged &&
284+
renderedElement
285+
) {
286+
return renderedElement;
287+
}
288+
289+
const baseProps = { pubSub };
290+
if (withRef) {
291+
Object.assign(baseProps, { ref: 'wrappedInstance' });
292+
}
293+
294+
const mergedProps = Object.assign(
295+
{},
296+
this.props,
297+
this.subscribedProps,
298+
this.publishProps,
299+
baseProps
300+
);
301+
302+
this.renderedElement = createElement(Composed, mergedProps);
303+
304+
return this.renderedElement;
305+
}
306+
}
307+
308+
PubSubConnector.contextTypes = {
309+
pubSubCore: pubSubShape,
310+
};
311+
312+
PubSubConnector.propTypes = {
313+
pubSubCore: pubSubShape,
314+
};
315+
316+
PubSubConnector.displayName = `PubSubConnector(${getDisplayName(Composed)})`;
317+
PubSubConnector.WrappedComponent = Composed;
318+
319+
return hoistStatics(PubSubConnector, Composed);
320+
};
321+
}

0 commit comments

Comments
 (0)