Skip to content

Instantly share code, notes, and snippets.

@antoniopresto
Created July 5, 2019 12:50
Show Gist options
  • Save antoniopresto/81dc1d4d924b645916dc234cc7357002 to your computer and use it in GitHub Desktop.
Save antoniopresto/81dc1d4d924b645916dc234cc7357002 to your computer and use it in GitHub Desktop.
next.js SSR
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>
);
}
}
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;
}
}
}
/**
* 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