shakacode/react_on_rails

View on GitHub
node_package/src/serverRenderReactComponent.ts

Summary

Maintainability
C
1 day
Test Coverage
import ReactDOMServer from 'react-dom/server';
import type { ReactElement } from 'react';

import ComponentRegistry from './ComponentRegistry';
import createReactOutput from './createReactOutput';
import {isServerRenderHash, isPromise} from
    './isServerRenderResult';
import buildConsoleReplay from './buildConsoleReplay';
import handleError from './handleError';
import type { RenderParams, RenderResult, RenderingError } from './types/index';

/* eslint-disable @typescript-eslint/no-explicit-any */

function serverRenderReactComponentInternal(options: RenderParams): null | string | Promise<RenderResult> {
  const { name, domNodeId, trace, props, railsContext, renderingReturnsPromises, throwJsErrors } = options;

  let renderResult: null | string | Promise<string> = null;
  let hasErrors = false;
  let renderingError: null | RenderingError = null;

  try {
    const componentObj = ComponentRegistry.get(name);
    if (componentObj.isRenderer) {
      throw new Error(`\
Detected a renderer while server rendering component '${name}'. \
See https://github.com/shakacode/react_on_rails#renderer-functions`);
    }

    const reactRenderingResult = createReactOutput({
      componentObj,
      domNodeId,
      trace,
      props,
      railsContext,
    });

    const processServerRenderHash = () => {
        // We let the client side handle any redirect
        // Set hasErrors in case we want to throw a Rails exception
        hasErrors = !!(reactRenderingResult as {routeError: Error}).routeError;

        if (hasErrors) {
          console.error(
            `React Router ERROR: ${JSON.stringify((reactRenderingResult as {routeError: Error}).routeError)}`,
          );
        }

        if ((reactRenderingResult as {redirectLocation: {pathname: string; search: string}}).redirectLocation) {
          if (trace) {
            const { redirectLocation } = (reactRenderingResult as {redirectLocation: {pathname: string; search: string}});
            const redirectPath = redirectLocation.pathname + redirectLocation.search;
            console.log(`\
  ROUTER REDIRECT: ${name} to dom node with id: ${domNodeId}, redirect to ${redirectPath}`,
            );
          }
          // For redirects on server rendering, we can't stop Rails from returning the same result.
          // Possibly, someday, we could have the rails server redirect.
          return '';
        }
        return (reactRenderingResult as { renderedHtml: string }).renderedHtml;
    };

    const processPromise = () => {
      if (!renderingReturnsPromises) {
        console.error('Your render function returned a Promise, which is only supported by a node renderer, not ExecJS.')
      }
      return reactRenderingResult;
    }

    const processReactElement = () => {
      try {
        return ReactDOMServer.renderToString(reactRenderingResult as ReactElement);
      } catch (error) {
        console.error(`Invalid call to renderToString. Possibly you have a renderFunction, a function that already
calls renderToString, that takes one parameter. You need to add an extra unused parameter to identify this function
as a renderFunction and not a simple React Function Component.`);
        throw error;
      }
    };

    if (isServerRenderHash(reactRenderingResult)) {
      renderResult = processServerRenderHash();
    } else if (isPromise(reactRenderingResult)) {
      renderResult = processPromise() as Promise<string>;
    } else {
      renderResult = processReactElement();
    }
  } catch (e: any) {
    if (throwJsErrors) {
      throw e;
    }

    hasErrors = true;
    renderResult = handleError({
      e,
      name,
      serverSide: true,
    });
    renderingError = e;
  }

  const consoleReplayScript = buildConsoleReplay();
  const addRenderingErrors = (resultObject: RenderResult, renderError: RenderingError) => {
    resultObject.renderingError = { // eslint-disable-line no-param-reassign
      message: renderError.message,
      stack: renderError.stack,
    };
  }

  if(renderingReturnsPromises) {
    const resolveRenderResult = async () => {
      let promiseResult;

      try {
        promiseResult = {
          html: await renderResult,
          consoleReplayScript,
          hasErrors,
        };
      } catch (e: any) {
        if (throwJsErrors) {
          throw e;
        }
        promiseResult = {
          html: handleError({
            e,
            name,
            serverSide: true,
          }),
          consoleReplayScript,
          hasErrors: true,
        }
        renderingError = e;
      }

      if (renderingError !== null) {
        addRenderingErrors(promiseResult, renderingError);
      }

      return promiseResult;
    };

    return resolveRenderResult();
  }

  const result = {
    html: renderResult,
    consoleReplayScript,
    hasErrors,
  } as RenderResult;

  if (renderingError) {
    addRenderingErrors(result, renderingError);
  }

  return JSON.stringify(result);
}

const serverRenderReactComponent: typeof serverRenderReactComponentInternal = (options) => {
  try {
    return serverRenderReactComponentInternal(options);
  } finally {
    // Reset console history after each render.
    // See `RubyEmbeddedJavaScript.console_polyfill` for initialization.
    console.history = [];
  }
};
export default serverRenderReactComponent;