Skip to content

Instantly share code, notes, and snippets.

@mizchi
Last active April 3, 2019 01:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mizchi/e2fd8c6193cef1b0e3eb27b0e3193373 to your computer and use it in GitHub Desktop.
Save mizchi/e2fd8c6193cef1b0e3eb27b0e3193373 to your computer and use it in GitHub Desktop.
import React, { Suspense } from "react";
import ReactDOMServer from "react-dom/server";
import { renderRoutes, matchRoutes, MatchedRoute } from "react-router-config";
import { StaticRouter } from "react-router-dom";
import ShallowRenderer from "react-test-renderer/shallow";
import { renderAsync, createResource } from "./renderAsync";
const shallowRenderer = ShallowRenderer.createRenderer();
// data fetcher
const resource = createResource(async (_key: string) => {
await new Promise(r => setTimeout(r, 1000));
return { message: "hello" };
});
function Home() {
return <div>Home</div>;
}
function Foo() {
const data = resource.read("/foo");
return (
<div>
Foo
<pre>
<code>{JSON.stringify(data)}</code>
</pre>
</div>
);
}
const routes = [
{
component: Home,
exact: true,
path: "/"
},
{
component: Foo,
exact: true,
path: "/foo"
}
];
function App() {
return <>{renderRoutes(routes)}</>;
}
function preloadRouteAction(matches: MatchedRoute<any>[]): Promise<any> | null {
let promises: Promise<any>[] = [];
matches.forEach(m => {
const C = m.route.component as React.ComponentType;
try {
shallowRenderer.render(<C {...m as any} />);
} catch (err) {
if (err instanceof Promise) {
promises.push(err);
}
}
});
if (promises.length > 0) {
return Promise.all(promises);
}
return null;
}
async function renderHtml(location: "/foo") {
let once = false;
const renderedHtml = await renderAsync({
renderFunction: ReactDOMServer.renderToString,
onBeforeRender() {
// load once
if (once) {
return;
}
once = true;
const matches = matchRoutes(routes, "/foo");
const promise = preloadRouteAction(matches);
if (promise) {
throw promise;
}
},
tree: (
<StaticRouter location={location}>
<App />
</StaticRouter>
)
});
return renderedHtml;
}
// Browser
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
const root = document.querySelector(".root") as HTMLDivElement;
async function main() {
// setup html
const html = await renderHtml("/foo");
root.innerHTML = html;
// hydrate
ReactDOM.hydrate(
<BrowserRouter>
<Suspense fallback="loading">
<App />
</Suspense>
</BrowserRouter>,
root
);
}
main();
// Extract from https://github.com/trojanowski/react-apollo-hooks
import React from "react";
const SSRContext = React.createContext<null | SSRManager>(null);
interface SSRManager {
hasPromises(): boolean;
register(promise: PromiseLike<any>): void;
consumeAndAwaitPromises(): Promise<any>;
}
function createSSRManager(): SSRManager {
const promiseSet = new Set<PromiseLike<any>>();
return {
hasPromises: () => promiseSet.size > 0,
register: promise => {
promiseSet.add(promise);
},
consumeAndAwaitPromises: () => {
const promises = Array.from(promiseSet);
promiseSet.clear();
return Promise.all(promises);
}
};
}
interface GetMarkupFromTreeOptions {
tree: React.ReactNode;
onBeforeRender?: () => any;
renderFunction: (tree: React.ReactElement<object>) => string;
}
export function renderAsync({
tree,
onBeforeRender,
renderFunction
}: GetMarkupFromTreeOptions): Promise<string> {
const ssrManager = createSSRManager();
function process(): string | Promise<string> {
try {
if (onBeforeRender) {
onBeforeRender();
}
const html = renderFunction(
<SSRContext.Provider value={ssrManager}>{tree}</SSRContext.Provider>
);
if (!ssrManager.hasPromises()) {
return html;
}
} catch (e) {
if (!(e instanceof Promise)) {
throw e;
}
ssrManager.register(e);
}
return ssrManager.consumeAndAwaitPromises().then(process);
}
return Promise.resolve().then(process);
}
export function createResource<T>(loader: (key: string) => T) {
const cache = new Map();
const load = (key: string) =>
new Promise(async (resolve, _reject) => {
const data = await loader(key);
cache.set(key, data);
resolve(data);
});
return {
async preload(key: string) {
if (cache.has(key)) {
return cache.get(key);
} else {
return load(key);
}
},
read(key: string) {
if (cache.has(key)) {
return cache.get(key);
} else {
throw load(key);
}
}
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment