UNPKG

react-on-rails

Version:

react-on-rails JavaScript for react_on_rails Ruby gem

156 lines 7.53 kB
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