Created
July 5, 2019 12:50
-
-
Save antoniopresto/81dc1d4d924b645916dc234cc7357002 to your computer and use it in GitHub Desktop.
next.js SSR
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 from 'react'; | |
import App, { Container } from 'next/app'; | |
import { MainProvider } from '../src/components/MainPage/MainProvider'; | |
import { AppReduxProvider } from '../src/components/ReduxProvider'; | |
import { SSRScriptElement } from '../src/lib/SSRPromisesHandler'; | |
import { captureError } from '../src/lib/capture'; | |
export default class MyApp extends App { | |
static async getInitialProps({ Component, ctx, router }: any) { | |
let pageProps = {}; | |
if (Component.getInitialProps) { | |
pageProps = await Component.getInitialProps(ctx); | |
} | |
return { pageProps: { ...pageProps, router } }; | |
} | |
componentDidCatch(error: any) { | |
captureError(error); | |
} | |
render() { | |
const { Component, pageProps } = this.props; | |
return ( | |
<AppReduxProvider> | |
<Container> | |
<MainProvider> | |
<SSRScriptElement /> | |
<Component {...pageProps} /> | |
</MainProvider> | |
</Container> | |
</AppReduxProvider> | |
); | |
} | |
} |
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 from 'react'; | |
import Document from 'next/document'; | |
import { ServerStyleSheet } from 'styled-components'; | |
import { SSRPromisesHandler, SSRRenderHelper } from '../src/lib/SSRPromisesHandler'; | |
import { captureError } from '../src/lib/capture'; | |
// @ts-ignore | |
export default class MyDocument extends Document { | |
componentDidCatch(error: any) { | |
captureError(error); | |
} | |
static async getInitialProps(ctx: any) { | |
const sheet = new ServerStyleSheet(); | |
const originalRenderPage = ctx.renderPage; | |
const renderApp: SSRRenderHelper = (helpers, isSecondRender) => | |
originalRenderPage({ | |
enhanceApp: (App: any) => (props: any) => { | |
const tree = helpers.withProvider(<App {...props} />); | |
return isSecondRender ? sheet.collectStyles(tree) : tree; | |
}, | |
}); | |
try { | |
const helpers = await SSRPromisesHandler({ | |
render: renderApp, | |
req: ctx.req, | |
}); | |
ctx.renderPage = () => renderApp(helpers, true); | |
const initialProps = await Document.getInitialProps(ctx); | |
return { | |
...initialProps, | |
styles: ( | |
<> | |
{initialProps.styles} | |
{sheet.getStyleElement()} | |
</> | |
), | |
}; | |
} catch (e) { | |
captureError(e); | |
// @ts-ignore | |
sheet.complete(); | |
throw e; | |
} | |
} | |
} |
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
/** | |
* Register and cache promises for SSR | |
*/ | |
import React from 'react'; | |
// @ts-ignore | |
import EventEmitter from 'events'; | |
// @ts-ignore | |
import * as http from 'http'; | |
import { captureError } from './capture'; | |
const PROMISES_TIMEOUT = 5000; | |
const browserSSRFallback = { | |
getSSRValue: function onBrowserGetSSRValue(requestID: string) { | |
// @ts-ignore | |
return typeof window !== 'undefined' && window.__PAPER_DATA__[requestID]; | |
}, | |
registerSSRPromise: function onBrowserRegisterSSRPromise(_: any, p: any) { | |
return p; | |
}, | |
getScriptElement: () => { | |
let results; | |
if (typeof window !== 'undefined') { | |
// @ts-ignore | |
results = window.__PAPER_DATA__; | |
} else { | |
results = null; | |
} | |
return ( | |
<script | |
dangerouslySetInnerHTML={{ | |
__html: `__PAPER_DATA__ = ${JSON.stringify(results)}`, | |
}} | |
/> | |
); | |
}, | |
}; | |
export const SSRPromisesHandlerContext = React.createContext(browserSSRFallback as SSRHelpers); | |
const { Provider } = SSRPromisesHandlerContext; | |
export const useSSRHelpers = () => { | |
return React.useContext(SSRPromisesHandlerContext); | |
}; | |
export const SSRScriptElement = () => { | |
const helpers = useSSRHelpers(); | |
return helpers.getScriptElement(); | |
}; | |
export const SSRPromisesHandler = async ({ render, req }: TProps): Promise<SSRHelpers> => { | |
const waiters = new Map<string, Promise<any>>(); | |
const results: SSRValues = new Map(); | |
const events = new EventEmitter(); | |
let isComplete = false; | |
let isFirstRender = true; | |
let isFetchComplete = false; | |
let onCompleteList: OnCompleteCb[] = []; | |
let registerSSRPromise: RegisterSSRPromise = function registerSSRPromise(requestID, promise) { | |
const existing = waiters.get(requestID); | |
if (existing) { | |
return existing; | |
} | |
waiters.set(requestID, promise); | |
return promise; | |
}; | |
let getSSRValue: GetSSRValue = function getValue(requestID) { | |
return results.get(requestID) || null; | |
}; | |
let getSSRValues = function getValue() { | |
let res: any = {}; | |
let keys: any[] = []; | |
try { | |
keys = results.keys() as any; | |
} catch (e) { | |
console.log({ results }); | |
captureError(e); | |
} | |
for (const i of keys) { | |
res[i] = results.get(i); | |
} | |
return res; | |
}; | |
let setSSRValue: SetSSRValue = function getValue(requestID, value) { | |
results.set(requestID, value); | |
}; | |
let onComplete = (fn: OnCompleteCb) => { | |
onCompleteList.push(fn); | |
}; | |
const partialHelpers = { | |
getSSRValue, | |
setSSRValue, | |
getSSRValues, | |
registerSSRPromise, | |
events, | |
isFetchComplete: () => isFetchComplete, | |
isComplete: () => isComplete, | |
isFirstRender: () => isFirstRender, | |
onComplete, | |
req, | |
}; | |
const helpers: SSRHelpers = { | |
...partialHelpers, | |
withProvider: (children, filledHelpers) => ( | |
<Provider value={(filledHelpers || helpers) as SSRHelpers}>{children}</Provider> | |
), | |
getScriptElement: () => { | |
let results; | |
if (typeof window !== 'undefined') { | |
// @ts-ignore | |
results = window.__PAPER_DATA__; | |
} else { | |
results = getSSRValues(); | |
} | |
return ( | |
<script | |
dangerouslySetInnerHTML={{ | |
__html: `__PAPER_DATA__ = ${JSON.stringify(results)}`, | |
}} | |
/> | |
); | |
}, | |
}; | |
// mounted only to render components then register promises of useQL hook, the resulting tree is ignored | |
render(helpers); | |
isFirstRender = false; | |
events.emit('firstRender'); | |
await new Promise(async resolve => { | |
setTimeout(resolve, PROMISES_TIMEOUT); | |
let keys = [] as any; | |
try { | |
keys = [...waiters.keys()]; | |
} catch (e) { | |
console.log({ waiters }); | |
captureError(e); | |
} | |
const values = await Promise.all([...waiters.values()]); | |
values.forEach((value, idx) => { | |
results.set(keys[idx], value); | |
}); | |
resolve(); | |
}); | |
isFetchComplete = true; | |
await Promise.all(onCompleteList.map(fn => fn(helpers))); | |
isComplete = true; | |
events.emit('complete'); | |
return helpers; | |
}; | |
export type SSRRenderHelper = (helpers: SSRHelpers, isSecondRender?: boolean) => any; | |
export type TProps = { | |
render: SSRRenderHelper; | |
req?: http.IncomingMessage; | |
}; | |
export type SSRHelpers = { | |
getSSRValue: GetSSRValue; | |
setSSRValue: SetSSRValue; | |
getSSRValues: () => { [key: string]: any }; | |
registerSSRPromise: RegisterSSRPromise; | |
events: EventEmitter; | |
isComplete: () => boolean; | |
isFirstRender: () => boolean; | |
isFetchComplete: () => boolean; | |
onComplete: (fn: OnCompleteCb) => any; | |
req?: http.IncomingMessage; | |
withProvider: (children: React.ReactNode, filledHelpers?: SSRHelpers) => React.ReactNode; | |
getScriptElement: () => JSX.Element; | |
}; | |
type OnCompleteCb = (state: SSRHelpers) => any; | |
export type SSRValues = Map<string, any>; | |
export type GetSSRValue = (requestID: string) => any | null; | |
export type SetSSRValue = (requestID: string, value: any) => void; | |
export type RegisterSSRPromise = (requestID: string, promise: Promise<any>) => void; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment