react-on-rails
Version:
react-on-rails JavaScript for react_on_rails Ruby gem
156 lines • 7.53 kB
JavaScript
import { isValidElement } from 'react';
// ComponentRegistry is accessed via globalThis.ReactOnRails.getComponent for cross-bundle compatibility
import createReactOutput from "./createReactOutput.js";
import { isPromise, isServerRenderHash } from "./isServerRenderResult.js";
import buildConsoleReplay from "./buildConsoleReplay.js";
import handleError from "./handleError.js";
import { renderToString } from "./ReactDOMServer.cjs";
import { createResultObject, convertToError, validateComponent } from "./serverRenderUtils.js";
function processServerRenderHash(result, options) {
const { redirectLocation, routeError } = result;
const hasErrors = !!routeError;
if (hasErrors) {
console.error(`React Router ERROR: ${JSON.stringify(routeError)}`);
}
let htmlResult;
if (redirectLocation) {
if (options.trace) {
const redirectPath = redirectLocation.pathname + redirectLocation.search;
console.log(`ROUTER REDIRECT: ${options.componentName} to dom node with id: ${options.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.
htmlResult = '';
}
else {
htmlResult = result.renderedHtml;
}
return { result: htmlResult ?? null, hasErrors };
}
function processReactElement(result) {
try {
return renderToString(result);
}
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;
}
}
function processPromise(result, renderingReturnsPromises) {
if (!renderingReturnsPromises) {
console.error('Your render function returned a Promise, which is only supported by the React on Rails Pro Node renderer, not ExecJS.');
// If the app is using server rendering with ExecJS, then the promise will not be awaited.
// And when a promise is passed to JSON.stringify, it will be converted to '{}'.
return '{}';
}
return result.then((promiseResult) => {
if (isValidElement(promiseResult)) {
return processReactElement(promiseResult);
}
return promiseResult;
});
}
function processRenderingResult(result, options) {
if (isServerRenderHash(result)) {
return processServerRenderHash(result, options);
}
if (isPromise(result)) {
return { result: processPromise(result, options.renderingReturnsPromises), hasErrors: false };
}
return { result: processReactElement(result), hasErrors: false };
}
function handleRenderingError(e, options) {
if (options.throwJsErrors) {
throw e;
}
const error = convertToError(e);
return {
hasErrors: true,
result: handleError({ e: error, name: options.componentName, serverSide: true }),
error,
};
}
async function createPromiseResult(renderState, componentName, throwJsErrors) {
// Capture console history before awaiting the promise
// Node renderer will reset the global console.history after executing the synchronous part of the request.
// It resets it only if replayServerAsyncOperationLogs renderer config is set to false.
// In both cases, we need to keep a reference to console.history to avoid losing console logs in case of reset.
const consoleHistory = console.history;
try {
const html = await renderState.result;
const consoleReplayScript = buildConsoleReplay(consoleHistory);
return createResultObject(html, consoleReplayScript, renderState);
}
catch (e) {
const errorRenderState = handleRenderingError(e, { componentName, throwJsErrors });
const consoleReplayScript = buildConsoleReplay(consoleHistory);
return createResultObject(errorRenderState.result, consoleReplayScript, errorRenderState);
}
}
function createFinalResult(renderState, componentName, throwJsErrors) {
const { result } = renderState;
if (isPromise(result)) {
return createPromiseResult({ ...renderState, result }, componentName, throwJsErrors);
}
const consoleReplayScript = buildConsoleReplay();
return JSON.stringify(createResultObject(result, consoleReplayScript, renderState));
}
function serverRenderReactComponentInternal(options) {
const { name: componentName, domNodeId, trace, props, railsContext, renderingReturnsPromises, throwJsErrors, } = options;
let renderState;
try {
const reactOnRails = globalThis.ReactOnRails;
if (!reactOnRails) {
throw new Error('ReactOnRails is not defined');
}
const componentObj = reactOnRails.getComponent(componentName);
validateComponent(componentObj, componentName);
// Renders the component or executes the render function
// - If the registered component is a React element or component, it renders it
// - If it's a render function, it executes the function and processes the result:
// - For React elements or components, it renders them
// - For promises, it returns them without awaiting (for async rendering)
// - For other values (e.g., strings), it returns them directly
// Note: Only synchronous operations are performed at this stage
const reactRenderingResult = createReactOutput({ componentObj, domNodeId, trace, props, railsContext });
// Processes the result from createReactOutput:
// 1. Converts React elements to HTML strings
// 2. Returns rendered HTML from serverRenderHash
// 3. Handles promises for async rendering
renderState = processRenderingResult(reactRenderingResult, {
componentName,
domNodeId,
trace,
renderingReturnsPromises,
});
}
catch (e) {
renderState = handleRenderingError(e, { componentName, throwJsErrors });
}
// Finalize the rendering result and prepare it for server response
// 1. Builds the consoleReplayScript for client-side console replay
// 2. Extract the result from promise (if needed) by awaiting it
// 3. Constructs a JSON object with the following properties:
// - html: string | null (The rendered component HTML)
// - consoleReplayScript: string (Script to replay console outputs on the client)
// - hasErrors: boolean (Indicates if any errors occurred during rendering)
// - renderingError: Error | null (The error object if an error occurred, null otherwise)
// 4. For Promise results, it awaits resolution before creating the final JSON
return createFinalResult(renderState, componentName, throwJsErrors);
}
const serverRenderReactComponent = (options) => {
try {
return serverRenderReactComponentInternal(options);
}
finally {
// Reset console history after each render.
// See `RubyEmbeddedJavaScript.console_polyfill` for initialization.
// This is necessary when ExecJS and old versions of node renderer are used.
// New versions of node renderer reset the console history automatically.
console.history = [];
}
};
export default serverRenderReactComponent;
//# sourceMappingURL=serverRenderReactComponent.js.map