Last active
April 3, 2019 01:12
-
-
Save mizchi/e2fd8c6193cef1b0e3eb27b0e3193373 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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