/
waterfallRender.mjs
133 lines (123 loc) · 4.7 KB
/
waterfallRender.mjs
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
// @ts-check
import React from "react";
import WaterfallRenderContext from "./WaterfallRenderContext.mjs";
/**
* Resolves a {@link React.ReactNode React node} rendered with all data loaded
* within cached; typically a HTML string.
*
* It repeatedly renders the {@link React.ReactNode React node} and awaits any
* loading cache promises declared within (using the
* {@link DeclareLoading declare loading function} via
* {@linkcode WaterfallRenderContext}, until no further loading is declared;
* implying all data has loaded and is rendered from cache.
*
* If server side rendering, afterwards the cache should be serialized for
* hydration on the client prior to the initial client side render.
*
* Intended for use in a server environment environment for server side
* rendering, but could potentially be used for preloading components in modern
* browser environments that support async functions, etc.
* @param {React.ReactNode} reactNode React node to render.
* @param {Function} render Synchronous React render function, e.g.
* [`ReactDOMServer.renderToStaticMarkup`](https://reactjs.org/docs/react-dom-server.html#rendertostaticmarkup)
* (faster), or
* [`ReactDOMServer.renderToString`](https://reactjs.org/docs/react-dom-server.html#rendertostring)
* (slower).
* @returns {Promise<unknown>} Resolves the final render result, typically a
* HTML string.
* @example
* Server side rendering a React app.
*
* ```jsx
* import ReactDOMServer from "react-dom/server";
* import waterfallRender from "react-waterfall-render/waterfallRender.mjs";
* import App from "./components/App.mjs";
*
* waterfallRender(<App />, ReactDOMServer.renderToStaticMarkup).then((html) => {
* // Do something with the HTML string…
* });
* ```
*/
export default async function waterfallRender(reactNode, render) {
// Check argument 1 exists, allowing an `undefined` value as that is a valid
// React node.
if (!arguments.length)
throw new TypeError("Argument 1 `reactNode` must be a React node.");
if (typeof render !== "function")
throw new TypeError("Argument 2 `render` must be a function.");
/**
* Repeatedly renders and awaits declared loading cache promises, until no
* further loading is declared.
* @returns {Promise<unknown>} Resolves the final rendered HTML string.
*/
async function recurseWaterfallRender() {
// Tracks loading data promises for this render pass.
/** @type {Array<Promise<any>>} */
const loadingPromises = [];
/**
* Declares loading cache promises.
* @type {DeclareLoading}
*/
function declareLoading(...promises) {
loadingPromises.push(...promises);
}
const renderResult = render(
React.createElement(
WaterfallRenderContext.Provider,
{ value: declareLoading },
reactNode
)
);
if (loadingPromises.length) {
await Promise.all(loadingPromises);
return recurseWaterfallRender();
} else return renderResult;
}
return recurseWaterfallRender();
}
/**
* Declares loading cache promises to {@linkcode waterfallRender}. Available
* within React components via {@linkcode WaterfallRenderContext}.
* @callback DeclareLoading
* @param {...Promise<any>} promises Promises that resolve once loading data has
* been cached. The values resolved don’t matter. Multiple arguments can be
* used, similar to how
* [`Array.push`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push)
* works.
* @returns {void}
* @example
* Loading data in a React component within a server and client side rendered app.
*
* ```jsx
* import React from "react";
* import WaterfallRenderContext from "react-waterfall-render/WaterfallRenderContext.mjs";
* import useUserProfileData from "../hooks/useUserProfileData.mjs";
* import UserProfile from "./UserProfile.mjs";
*
* export default function UserPage({ userId }) {
* const declareLoading = React.useContext(WaterfallRenderContext);
* const { load, loading, cache } = useUserProfileData(userId);
*
* // For this example, assume loading errors are cached.
* if (cache) return <UserProfile data={cache} />;
*
* if (!loading) {
* const userDataPromise = load();
*
* // Only present when the app is server side rendered using the function
* // `waterfallRender`.
* if (declareLoading) {
* declareLoading(userDataPromise);
*
* // This render is on the server and will be discarded anyway for a
* // re-render once the declared loading promises resolve, so it’s
* // slightly more efficient to render nothing; particularly if the
* // loading state is expensive to render.
* return null;
* }
* }
*
* return "Loading…";
* }
* ```
*/