react-on-rails
Version:
react-on-rails JavaScript for react_on_rails Ruby gem
108 lines • 6.08 kB
JavaScript
import { createElement, isValidElement } from 'react';
import { isServerRenderHash, isPromise } from "./isServerRenderResult.js";
import { isRendererTeardownResult } from "./rendererTeardown.js";
const unsupportedManualRendererMessage = (name) => `ReactOnRails.render() does not support renderer functions ("${name}"). ` +
'Use normal React on Rails component rendering so renderer teardowns are captured on navigation.';
function isReactObjectComponentType(value) {
if (value == null || typeof value !== 'object') {
return false;
}
// React.memo, React.forwardRef, React.lazy, and related component types are non-callable
// objects tagged with React's element-type marker.
const typeMarker = value.$$typeof;
return typeof typeMarker === 'symbol' || typeof typeMarker === 'number';
}
function isReactComponentType(value) {
return typeof value === 'function' || typeof value === 'string' || isReactObjectComponentType(value);
}
function createReactElementFromRenderFunctionResult(renderFunctionResult, name, props) {
if (isValidElement(renderFunctionResult)) {
// If already a ReactElement, then just return it.
console.error(`Warning: ReactOnRails: Your registered render-function (ReactOnRails.register) for ${name}
incorrectly returned a React Element (JSX). Instead, return a React Function Component by
wrapping your JSX in a function. ReactOnRails v13 will throw error on this, as React Hooks do not
work if you return JSX. Update by wrapping the result JSX of ${name} in a fat arrow function.`);
return renderFunctionResult;
}
// If a component, then wrap in an element
return createElement(renderFunctionResult, props);
}
/**
* Logic to either call the renderFunction or call React.createElement to get the
* React.Component
* @param options
* @param options.componentObj
* @param options.props
* @param options.domNodeId
* @param options.trace
* @param options.location
* @returns {ReactElement}
*/
export default function createReactOutput({ componentObj, props, railsContext, domNodeId, trace, shouldHydrate, }) {
const { name, component, renderFunction, isRenderer } = componentObj;
if (trace) {
if (railsContext && railsContext.serverSide) {
console.log(`RENDERED ${name} to dom node with id: ${domNodeId}`);
}
else if (shouldHydrate) {
console.log(`HYDRATED ${name} in dom node with id: ${domNodeId} using props, railsContext:`, props, railsContext);
}
else {
console.log(`RENDERED ${name} to dom node with id: ${domNodeId} with props, railsContext:`, props, railsContext);
}
}
if (renderFunction) {
// Let's invoke the function to get the result
if (trace) {
console.log(`${name} is a renderFunction`);
}
// createReactOutput only handles render-functions that return a component or server-render hash.
// The 3-argument renderer form (which owns its own mount and may return a RendererTeardownResult)
// never reaches here on the supported paths: the client renderers delegate it earlier
// (`delegateToRenderer`) and return before this call, and server rendering rejects it upstream in
// validateComponent ("Detected a renderer while server rendering"). The only path that reaches
// here with a renderer is the manual public `ReactOnRails.render()` API, where renderers are
// unsupported because their teardown can't be tracked for cleanup. Reject it loudly and *before*
// invoking, rather than calling it with no domNodeId and rendering a half-wired, leak-prone result.
if (isRenderer) {
throw new Error(unsupportedManualRendererMessage(name));
}
if (typeof component !== 'function') {
throw new Error(`Registered render function "${name}" must be a function.`);
}
// The cast only narrows the broad RegisteredComponentValue union (which isn't structurally
// callable) to a concrete render-function role; it does not widen what may be returned.
const renderFunctionResult = component(props, railsContext);
// Defense-in-depth: RenderFunction (= ServerRenderFunction) returns RenderFunctionResult, which
// already excludes RendererTeardownResult at the type level, so a teardown can't arrive here from
// a typed caller. Reject it at runtime anyway for untyped JS callers that ignore the contract.
if (isRendererTeardownResult(renderFunctionResult)) {
throw new Error(unsupportedManualRendererMessage(name));
}
if (isServerRenderHash(renderFunctionResult)) {
// We just return at this point, because calling function knows how to handle this case and
// we can't call React.createElement with this type of Object.
return renderFunctionResult;
}
if (isPromise(renderFunctionResult)) {
// We just return at this point, because calling function knows how to handle this case and
// we can't call React.createElement with this type of Object.
return renderFunctionResult.then((result) => {
if (isRendererTeardownResult(result)) {
throw new Error(unsupportedManualRendererMessage(name));
}
// If the result is a function, then it returned a React Component (even class components are functions).
if (typeof result === 'function') {
return createReactElementFromRenderFunctionResult(result, name, props);
}
return result;
});
}
return createReactElementFromRenderFunctionResult(renderFunctionResult, name, props);
}
if (!isReactComponentType(component)) {
throw new Error(`Registered component "${name}" must be a function, string, or React object component type.`);
}
return createElement(component, props);
}
//# sourceMappingURL=createReactOutput.js.map