UNPKG

provide-page

Version:

Provides automatic server-side rendering and actions (regardless of whether or not client has JavaScript enabled) to React components. Use in conjunction with `provide-router`.

266 lines (222 loc) 7.35 kB
import { pushWait, pushClear } from 'react-redux-provide'; import { pushReplication, unshiftMiddleware } from 'react-redux-provide'; import defaultRenderDocumentToString from './defaultRenderDocumentToString'; import { extractServerStates, extractClientStates } from './extractStates'; import getProviders from './getProviders'; const inDevelopment = process.env.NODE_ENV === 'development'; export default function createMiddleware({ defaultProps, renderToString, renderDocumentToString = defaultRenderDocumentToString, getStates, initStates, maxRenders = 20, maxResponseTime = 2000 }) { return (request, response, next) => { const bodyType = typeof request.body; if (bodyType === 'undefined') { console.warn('Server needs to use `body-parser` or something like it!'); } try { const providers = getProviders(defaultProps.providers, request); const providerInstances = { ...defaultProps.providerInstances }; const activeQueries = { ...defaultProps.activeQueries }; const queryResults = { ...defaultProps.queryResults }; const partialStates = { ...defaultProps.partialStates }; providers.page.state = { ...providers.page.state, requestMethod: request.method, requestHeaders: request.headers }; if (initStates) { initStates(providers, providerInstances); } let html = null; function renderState() { if (responded) { return; } wait(); if (inDevelopment) { console.log(`--- rendering (${renderCount}) ${request.url}`); } html = renderToString({ ...defaultProps, providers, providerInstances, activeQueries, queryResults, partialStates }); clear(false); } let waitCount = 0; function wait() { waitCount++; } function clear(doRerender) { if (doRerender) { rerender = true; } if (--waitCount === 0) { respondOrRerender(); } } let renderCount = 0; let rerender = false; let handledRequest = false; let shouldSubmitRequest = false; let preActionStates; if (Array.isArray(request.body)) { shouldSubmitRequest = true; } else if (bodyType === 'object') { shouldSubmitRequest = Object.keys(request.body).length > 0; } else if (bodyType === 'string') { shouldSubmitRequest = request.body.length > 0; } else { shouldSubmitRequest = Boolean(request.body); } function respondOrRerender() { if (!rerender && !handledRequest) { handledRequest = true; handleRequest(); } renderCount++; if (rerender && renderCount < maxRenders) { rerender = false; renderState(); } else if (waitCount === 0) { respond(); } } function handleRequest() { const { page } = providerInstances; const requestState = { requestMethod: request.method, requestBody: request.body, requestHeaders: request.headers }; if (serverSide && shouldSubmitRequest) { preActionStates = extractServerStates(providerInstances, getStates); } if (!page) { providers.page.state = { ...providers.page.state, ...requestState }; } else if (shouldSubmitRequest) { page.actionCreators.submitRequest(requestState); } } const { acceptJson } = providers.page.state; const serverSide = acceptJson && request.headers['x-server-side']; let actions = null; if (serverSide && shouldSubmitRequest) { Object.keys(providers).forEach(key => { pushReplication({ [key]: providers[key] }, { replicator: { postReduction({ store, state, nextState, action }) { const { key: providerKey } = store; if (actions) { if (preActionStates && !preActionStates[providerKey]) { preActionStates[providerKey] = state; } if (!action._noEffect) { actions.push({ providerKey, action }); } } else if (handledRequest) { actions = []; } } } }); }); } let responded = false; let responseTimeout = maxResponseTime ? setTimeout(send408Status, maxResponseTime) : null; function respond() { if (responseTimeout) { clearTimeout(responseTimeout); responseTimeout = null; } if (responded) { return; } const states = extractServerStates( providerInstances, getStates, preActionStates ); const clientStates = extractClientStates(providerInstances, { ...partialStates, ...states }); const { headers, statusCode } = states.page || {}; let documentString = null; if (headers && !response.headersSent) { response.set(headers); } if (acceptJson) { if (statusCode && !response.headersSent) { response.status(statusCode).send(clientStates); } else { response.send(clientStates); } } else if (!redirect() && html) { documentString = renderDocumentToString(html, states, clientStates); if (statusCode && !response.headersSent) { response.status(statusCode).send(documentString); } else { response.send(documentString); } } else if (statusCode && !response.headersSent) { response.sendStatus(statusCode); } responded = true; if (inDevelopment) { console.log(`--- sent response for ${request.url}`); } } function redirect() { const { router } = providerInstances; const { history, routing } = router.store.getState(); const location = routing && routing.locationBeforeTransitions || routing && routing.location || router.store.getState().location || history.location; const url = location.pathname + location.search; if (url !== request.originalUrl) { if (!response.headersSent) { response.redirect(303, location.pathname); } return true; } return false; } function send408Status() { responseTimeout = null; if (!response.headersSent) { response.sendStatus(408); } responded = true; } pushWait(providers, wait); pushClear(providers, clear); unshiftMiddleware(providers, ({ dispatch, getState }) => { return next => action => { if (typeof action !== 'function' && !action._noEffect) { rerender = true; } return next(action); }; }); renderState(); } catch (error) { console.error(error.stack); if (!response.headersSent) { response.sendStatus(500); } } }; }