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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194 | 27x
119x
268x
119x
95x
27x
276x
6x
2x
274x
6x
268x
226x
119x
119x
119x
119x
119x
95x
95x
95x
95x
95x
2x
1x
2x
95x
2x
95x
18x
95x
24x
71x
24x
24x
95x
91x
10x
86x
107x
107x
63x
113x
112x
42x
42x
88x
27x
84x
42x
42x
190x
27x
27x
24x
24x
42x
61x
42x
42x
24x
18x
24x
24x
23x
1x
18x
18x
1x
1x
1x
| import * as React from 'react';
export interface Context {
[key: string]: any;
}
interface PromiseTreeArgument {
rootElement: React.ReactNode;
rootContext?: Context;
}
interface FetchComponent extends React.Component<any> {
fetchData(): Promise<void>;
}
interface PromiseTreeResult {
promise: Promise<any>;
context: Context;
instance: FetchComponent;
}
interface PreactElement<P> {
attributes: P;
}
function getProps<P>(element: React.ReactElement<P> | PreactElement<P>): P {
return (element as React.ReactElement<P>).props || (element as PreactElement<P>).attributes;
}
function isReactElement(element: React.ReactNode): element is React.ReactElement<any> {
return !!(element as any).type;
}
function isComponentClass(Comp: React.ComponentType<any>): Comp is React.ComponentClass<any> {
return Comp.prototype && (Comp.prototype.render || Comp.prototype.isReactComponent);
}
function providesChildContext(instance: React.Component<any>): instance is React.Component<any> & React.ChildContextProvider<any> {
return !!(instance as any).getChildContext;
}
// Recurse a React Element tree, running visitor on each element.
// If visitor returns `false`, don't call the element's render function
// or recurse into its child elements
export function walkTree(element: React.ReactNode, context: Context, visitor: (
element: React.ReactNode,
instance: React.Component<any> | null,
context: Context,
childContext?: Context,
) => boolean | void,
) {
if (Array.isArray(element)) {
element.forEach(item => walkTree(item, context, visitor));
return;
}
if (!element) {
return;
}
// a stateless functional component or a class
if (isReactElement(element)) {
if (typeof element.type === 'function') {
const Comp = element.type;
const props = Object.assign({}, Comp.defaultProps, getProps(element));
let childContext = context;
let child;
// Are we are a react class?
// https://github.com/facebook/react/blob/master/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js#L66
if (isComponentClass(Comp)) {
const instance = new Comp(props, context);
// In case the user doesn't pass these to super in the constructor
instance.props = instance.props || props;
instance.context = instance.context || context;
// set the instance state to null (not undefined) if not set, to match React behaviour
instance.state = instance.state || null;
// Override setState to just change the state, not queue up an update.
// (we can't do the default React thing as we aren't mounted "properly"
// however, we don't need to re-render as well only support setState in
// componentWillMount, which happens *before* render).
instance.setState = newState => {
if (typeof newState === 'function') {
// React's TS type definitions don't contain context as a third parameter for
// setState's updater function.
// Remove this cast to `any` when that is fixed.
newState = (newState as any)(instance.state, instance.props, instance.context);
}
instance.state = Object.assign({}, instance.state, newState);
};
// this is a poor man's version of
// https://github.com/facebook/react/blob/master/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js#L181
if (instance.componentWillMount) {
instance.componentWillMount();
}
if (providesChildContext(instance)) {
childContext = Object.assign({}, context, instance.getChildContext());
}
if (visitor(element, instance, context, childContext) === false) {
return;
}
child = instance.render();
} else {
// just a stateless functional
Iif (visitor(element, null, context) === false) {
return;
}
child = Comp(props, context);
}
if (child) {
if (Array.isArray(child)) {
child.forEach(item => walkTree(item, childContext, visitor));
} else {
walkTree(child, childContext, visitor);
}
}
} else {
// a basic string or dom element, just get children
Iif (visitor(element, null, context) === false) {
return;
}
if (element.props && element.props.children) {
React.Children.forEach(element.props.children, (child: any) => {
if (child) {
walkTree(child, context, visitor);
}
});
}
}
} else Eif (typeof element === 'string' || typeof element === 'number') {
// Just visit these, they are leaves so we don't keep traversing.
visitor(element, null, context);
}
// TODO: Portals?
}
function hasFetchDataFunction(instance: React.Component<any>): instance is FetchComponent {
return typeof (instance as any).fetchData === 'function';
}
function isPromise<T>(promise: Object): promise is Promise<T> {
return typeof (promise as any).then === 'function';
}
function getPromisesFromTree({rootElement, rootContext = {}}: PromiseTreeArgument): PromiseTreeResult[] {
const promises: PromiseTreeResult[] = [];
walkTree(rootElement, rootContext, (_, instance, context, childContext) => {
if (instance && hasFetchDataFunction(instance)) {
const promise = instance.fetchData();
if (isPromise<Object>(promise)) {
promises.push({promise, context: childContext || context, instance});
return false;
}
}
});
return promises;
}
export default function getDataFromTree(rootElement: React.ReactNode, rootContext: any = {}): Promise<any> {
const promises = getPromisesFromTree({rootElement, rootContext});
if (!promises.length) {
return Promise.resolve();
}
const errors: any[] = [];
const mappedPromises = promises.map(({promise, context, instance}) => {
return promise
.then(_ => getDataFromTree(instance.render(), context))
.catch(e => errors.push(e));
});
return Promise.all(mappedPromises).then(_ => {
if (errors.length > 0) {
const error =
errors.length === 1
? errors[0]
: new Error(`${errors.length} errors were thrown when executing your fetchData functions.`);
error.queryErrors = errors;
throw error;
}
});
}
|