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 | 27x
27x
27x
27x
80x
80x
140x
70x
70x
108x
108x
64x
32x
32x
64x
64x
64x
10x
54x
32x
27x
27x
26x
26x
27x
70x
70x
32x
32x
32x
38x
27x
48x
27x
24x
24x
32x
32x
32x
24x
24x
27x
27x
27x
27x
49x
27x
49x
27x
27x
51x
26x
49x
48x
26x
| import * as React from 'react';
import * as PropTypes from 'prop-types';
import { renderToStaticMarkup } from 'react-dom/server';
import Query from './Query';
// Like a Set, but for tuples. In practice, this class is used to store
// (query, JSON.stringify(variables)) tuples.
class Trie {
private children: Map<any, Trie> | null = null;
private added = false;
has(...keys: any[]) {
let node: Trie = this;
return keys.every(key => {
const child = node.children && node.children.get(key);
return !!(child && (node = child));
}) && node.added;
}
add(...keys: any[]) {
let node: Trie = this;
keys.forEach(key => {
const map = node.children || (node.children = new Map);
const child = map.get(key);
if (child) {
node = child;
} else {
map.set(key, node = new Trie());
}
});
node.added = true;
}
}
export class RenderPromises {
// Map from Query component instances to pending fetchData promises.
private queryPromises = new Map<Query<any, any>, Promise<any>>();
// A way of remembering queries we've seen during previous renderings,
// so that we never attempt to fetch them again in future renderings.
private queryGraveyard = new Trie();
public addQueryPromise<TData, TVariables>(
queryInstance: Query<TData, TVariables>,
finish: () => React.ReactNode,
): React.ReactNode {
const { query, variables } = queryInstance.props;
if (!this.queryGraveyard.has(query, JSON.stringify(variables))) {
this.queryPromises.set(
queryInstance,
new Promise(resolve => {
resolve(queryInstance.fetchData());
}),
);
// Render null to abandon this subtree for this rendering, so that we
// can wait for the data to arrive.
return null;
}
return finish();
}
public hasPromises() {
return this.queryPromises.size > 0;
}
public consumeAndAwaitPromises() {
const promises: Promise<any>[] = [];
this.queryPromises.forEach((promise, queryInstance) => {
const { query, variables } = queryInstance.props;
// Make sure we never try to call fetchData for this query document and
// these variables again. Since the queryInstance objects change with
// every rendering, deduplicating them by query and variables is the
// best we can do. If a different Query component happens to have the
// same query document and variables, it will be immediately rendered
// by calling finish() in addQueryPromise, which could result in the
// rendering of an unwanted loading state, but that's not nearly as bad
// as getting stuck in an infinite rendering loop because we kept calling
// queryInstance.fetchData for the same Query component indefinitely.
this.queryGraveyard.add(query, JSON.stringify(variables));
promises.push(promise);
});
this.queryPromises.clear();
return Promise.all(promises);
}
}
class RenderPromisesProvider extends React.Component<{
renderPromises: RenderPromises;
}> {
static childContextTypes = {
renderPromises: PropTypes.object,
};
getChildContext() {
return {
renderPromises: this.props.renderPromises,
};
}
render() {
return this.props.children;
}
}
export default function getDataFromTree(
rootElement: React.ReactNode,
// The rendering function is configurable! We use renderToStaticMarkup as
// the default, because it's a little less expensive than renderToString,
// and legacy usage of getDataFromTree ignores the return value anyway.
renderFunction = renderToStaticMarkup,
): Promise<string> {
const renderPromises = new RenderPromises();
function process(): Promise<string> | string {
const html = renderFunction(
React.createElement(RenderPromisesProvider, {
renderPromises,
// Always re-render from the rootElement, even though it might seem
// better to render the children of the component responsible for the
// promise, because it is not possible to reconstruct the full context
// of the original rendering (including all unknown context provider
// elements) for a subtree of the orginal component tree.
children: rootElement,
})
);
return renderPromises.hasPromises()
? renderPromises.consumeAndAwaitPromises().then(process)
: html;
}
return Promise.resolve().then(process);
}
|