-
Notifications
You must be signed in to change notification settings - Fork 46.9k
/
createSubscription.js
159 lines (135 loc) · 4.56 KB
/
createSubscription.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
/**
* Copyright (c) 2014-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
*/
import React from 'react';
import invariant from 'shared/invariant';
import warningWithoutStack from 'shared/warningWithoutStack';
type Unsubscribe = () => void;
export function createSubscription<Property, Value>(
config: $ReadOnly<{|
// Synchronously gets the value for the subscribed property.
// Return undefined if the subscribable value is undefined,
// Or does not support synchronous reading (e.g. native Promise).
getCurrentValue: (source: Property) => Value | void,
// Setup a subscription for the subscribable value in props, and return an unsubscribe function.
// Return empty function if the property cannot be unsubscribed from (e.g. native Promises).
// Due to the variety of change event types, subscribers should provide their own handlers.
// Those handlers should not attempt to update state though;
// They should call the callback() instead when a subscription changes.
subscribe: (
source: Property,
callback: (value: Value | void) => void,
) => Unsubscribe,
|}>,
): React$ComponentType<{
children: (value: Value | void) => React$Node,
source: Property,
}> {
const {getCurrentValue, subscribe} = config;
warningWithoutStack(
typeof getCurrentValue === 'function',
'Subscription must specify a getCurrentValue function',
);
warningWithoutStack(
typeof subscribe === 'function',
'Subscription must specify a subscribe function',
);
type Props = {
children: (value: Value) => React$Element<any>,
source: Property,
};
type State = {
source: Property,
value: Value | void,
};
// Reference: https://gist.github.com/bvaughn/d569177d70b50b58bff69c3c4a5353f3
class Subscription extends React.Component<Props, State> {
state: State = {
source: this.props.source,
value:
this.props.source != null
? getCurrentValue(this.props.source)
: undefined,
};
_hasUnmounted: boolean = false;
_unsubscribe: Unsubscribe | null = null;
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.source !== prevState.source) {
return {
source: nextProps.source,
value:
nextProps.source != null
? getCurrentValue(nextProps.source)
: undefined,
};
}
return null;
}
componentDidMount() {
this.subscribe();
}
componentDidUpdate(prevProps, prevState) {
if (this.state.source !== prevState.source) {
this.unsubscribe(prevState);
this.subscribe();
}
}
componentWillUnmount() {
this.unsubscribe(this.state);
// Track mounted to avoid calling setState after unmounting
// For source like Promises that can't be unsubscribed from.
this._hasUnmounted = true;
}
render() {
return this.props.children(this.state.value);
}
subscribe() {
const {source} = this.state;
if (source != null) {
const callback = (value: Value | void) => {
if (this._hasUnmounted) {
return;
}
this.setState(state => {
// If the value is the same, skip the unnecessary state update.
if (value === state.value) {
return null;
}
// If this event belongs to an old or uncommitted data source, ignore it.
if (source !== state.source) {
return null;
}
return {value};
});
};
// Store the unsubscribe method for later (in case the subscribable prop changes).
const unsubscribe = subscribe(source, callback);
invariant(
typeof unsubscribe === 'function',
'A subscription must return an unsubscribe function.',
);
// It's safe to store unsubscribe on the instance because
// We only read or write that property during the "commit" phase.
this._unsubscribe = unsubscribe;
// External values could change between render and mount,
// In some cases it may be important to handle this case.
const value = getCurrentValue(this.props.source);
if (value !== this.state.value) {
this.setState({value});
}
}
}
unsubscribe(state: State) {
if (typeof this._unsubscribe === 'function') {
this._unsubscribe();
}
this._unsubscribe = null;
}
}
return Subscription;
}