import React from 'react'; import Helmet from 'react-helmet'; import { StaticRouter } from 'react-router-dom'; import { renderToString } from 'react-dom/server'; import { matchRoutes } from 'react-router-config'; import { Provider } from 'react-redux'; import clientEnv from 'environments/client'; import routes from 'src/configs/routes'; import { createNewStore } from 'src/store'; import { createNewContext } from 'src/services/initial-ssr-data'; import { initSSRContextValue } from 'src/services/initial-ssr-data/utils'; import { getEnv, isProd } from 'src/utils/env'; import AppRouter from 'src/AppRouter'; import { findFinalComponent, getComponent } from '../utils'; import pageTemplate from '../templates/page-template.hbs'; // eslint-disable-next-line import/no-dynamic-require const assets = require(process.env.RAZZLE_ASSETS_MANIFEST); function renderPage({ appHtml, preloadedData, preloadedState }) { const head = Helmet.renderStatic(); const title = head ? head.title.toString() : ''; const meta = head ? head.meta.toString() : ''; const link = head ? head.link.toString() : ''; const script = head ? head.script.toString() : ''; const html = pageTemplate({ appHtml: appHtml.toString(), PRELOADED_DATA_KEY: getEnv('PRELOADED_DATA_KEY'), PRELOADED_STATE_KEY: getEnv('PRELOADED_STATE_KEY'), preloadedData: JSON.stringify(preloadedData), preloadedState: JSON.stringify(preloadedState), assets, meta, link, script, title, isProd, env: JSON.stringify(clientEnv), }); return html; } function getInitialPropsList({ renderBranch, store: { getState, dispatch }, req }) { return renderBranch.reduce( ( initialPropsList, { route: { component: routeComponent, render: routeRenderer, path, }, }, ) => { const component = getComponent(routeComponent, routeRenderer); // Due to usage of HOCs, we have to traverse the component tree // to find the final wrapped component which contains the static server-side methods const { component: { serverSideInitial }, hasSSRData: wrappedInWithSSRData, } = findFinalComponent(component); if (!serverSideInitial) { return initialPropsList; } const dataPromise = serverSideInitial({ getState, dispatch, req }); const serverSideInitialReturns = dataPromise !== undefined; const hasPreloadedData = serverSideInitialReturns || wrappedInWithSSRData; return initialPropsList.concat({ path, hasPreloadedData, dataPromise, }); }, [], ); } function getPreloadedData({ initialDataList, initialPropsList }) { return initialDataList.reduce((preloadedData, data, index) => { const { path, hasPreloadedData } = initialPropsList[index]; if (!hasPreloadedData) { return preloadedData; } return { ...preloadedData, [path]: data, }; }, {}); } function getPageMarkup({ store, context, preloadedData, location, }) { const { Provider: InitialSSRDataProvider } = createNewContext(); return renderToString( <Provider store={store}> <InitialSSRDataProvider value={initSSRContextValue(preloadedData)}> <StaticRouter context={context} location={location}> <AppRouter /> </StaticRouter> </InitialSSRDataProvider> </Provider>, ); } function handleSSR(req, res, next) { const context = {}; const store = createNewStore(); const renderBranch = matchRoutes(routes, req.url); const initialPropsList = getInitialPropsList({ renderBranch, store, req, }); const initialDataPromises = initialPropsList.map( ({ dataPromise }) => dataPromise, ); Promise.all(initialDataPromises) .then((initialDataList) => { const preloadedData = getPreloadedData({ initialDataList, initialPropsList, }); // note that calling `getPageMarkup` might cause `context` value to update! const markup = getPageMarkup({ store, context, preloadedData, location: req.url, }); if (context.url) { res.redirect(context.url); } else { res.status(200).send( renderPage({ appHtml: markup, preloadedData, preloadedState: store.getState(), }), ); } }) .catch(next); } export default handleSSR;