UNPKG

react-on-rails

Version:

react-on-rails JavaScript for react_on_rails Ruby gem

568 lines 26.7 kB
import type { ReactElement, ReactNode, Component, ComponentType, ExoticComponent } from 'react'; import type { PipeableStream } from 'react-dom/server'; import type { Readable } from 'stream'; /** * Don't import Redux just for the type definitions * See https://github.com/shakacode/react_on_rails/issues/1321 * and https://redux.js.org/api/store for the actual API. * @see {import('redux').Store} */ type Store = { getState(): unknown; }; type ReactComponent = ComponentType<any> | ExoticComponent<any> | string; export type RailsContext = { componentRegistryTimeout: number; railsEnv: string; inMailer: boolean; i18nLocale: string; i18nDefaultLocale: string; rorVersion: string; rorPro: boolean; rorProVersion?: string; href: string; location: string; scheme: string; host: string; port: number | null; pathname: string; search: string | null; httpAcceptLanguage: string; rscPayloadGenerationUrlPath?: string; cspNonce?: string; /** Omit or leave undefined to disable; only true is a valid opt-in value. */ rscStreamObservability?: true; } & ({ serverSide: false; } | { serverSide: true; serverSideRSCPayloadParameters?: unknown; reactClientManifestFileName?: string; reactServerClientManifestFileName?: string; getRSCPayloadStream: (componentName: string, props: unknown) => Promise<NodeJS.ReadableStream>; }); export type RailsContextWithServerComponentMetadata = RailsContext & { serverSide: true; serverSideRSCPayloadParameters?: unknown; reactClientManifestFileName: string; reactServerClientManifestFileName: string; }; export type RailsContextWithServerStreamingCapabilities = RailsContextWithServerComponentMetadata & { getRSCPayloadStream: (componentName: string, props: unknown) => Promise<NodeJS.ReadableStream>; addPostSSRHook: (hook: () => void) => void; recordRSCDiagnostic?: (componentName: string, diagnosticError: Error) => void; }; export declare const assertRailsContextWithServerComponentMetadata: (context: RailsContext | undefined) => asserts context is RailsContextWithServerComponentMetadata; export declare const assertRailsContextWithServerStreamingCapabilities: (context: RailsContext | undefined) => asserts context is RailsContextWithServerStreamingCapabilities; type AuthenticityHeaders = Record<string, string> & { 'X-CSRF-Token': string | null; 'X-Requested-With': string; }; type StoreGenerator = (props: Record<string, unknown>, railsContext: RailsContext) => Store; type ServerRenderHashRenderedHtml = { componentHtml: string; [key: string]: string; }; interface ServerRenderResult { renderedHtml?: string | ServerRenderHashRenderedHtml | ReactElement; clientProps?: Record<string, unknown>; redirectLocation?: { pathname: string; search: string; }; routeError?: Error; error?: Error; } type CreateReactOutputSyncResult = ServerRenderResult | ReactElement; type CreateReactOutputAsyncResult = Promise<string | ServerRenderHashRenderedHtml | ReactElement | ServerRenderResult>; type CreateReactOutputResult = CreateReactOutputSyncResult | CreateReactOutputAsyncResult; type RenderFunctionSyncResult = ReactComponent | ServerRenderResult; type RenderFunctionAsyncResult = Promise<string | ServerRenderHashRenderedHtml | ReactComponent | ServerRenderResult>; type RenderFunctionResult = RenderFunctionSyncResult | RenderFunctionAsyncResult; type ReactComponentRenderFunctionResult = ReactComponent | Promise<ReactComponent>; /** * Optional cleanup callback that a renderer function (the 3-argument form * `(props, railsContext, domNodeId) => …`) may return inside a {@link RendererTeardownResult}. * React on Rails invokes it when the mount is torn down — on Turbo/Turbolinks navigation (the * framework's soft-navigation page swap, not a native browser unload) and when the same `domNodeId` * node is replaced — so renderer-managed React roots, event listeners, and subscriptions are released * instead of leaked. May be synchronous or asynchronous. * * @see RenderFunction */ type RendererTeardownReturn = void | Promise<void>; type RendererTeardown = () => RendererTeardownReturn; /** * Object wrapper returned by a 3-argument renderer to opt into cleanup. The wrapper keeps teardown * detection unambiguous: legacy renderers may have returned function components before this contract * existed, so a bare function return is treated as no teardown. */ type RendererTeardownResult = { teardown: RendererTeardown; }; /** * What the 3-argument renderer form may return for cleanup: nothing, or a * {@link RendererTeardownResult}. Runtime cleanup only recognizes this explicit wrapper; legacy * component/server-result returns from 3-argument renderers are ignored. * * Consumers discriminate this union at runtime by an object with a `teardown` function vs. a thenable * (an async renderer, awaited/adopted before re-checking) vs. anything else (no teardown). The * `void` arm is therefore treated as "no teardown," same as `undefined`. */ type RendererResult = void | RendererTeardownResult | Promise<void | RendererTeardownResult>; type RendererFunctionResult = RendererResult | RenderFunctionResult; interface RenderFunctionMarker { renderFunction?: true; } /** * The precise call signature of the 3-argument "renderer" form `(props, railsContext, domNodeId) => * …`. A renderer owns its own mount and may return nothing or a {@link RendererTeardownResult} * (possibly async) to opt into cleanup. It also accepts the legacy {@link RenderFunctionResult} * return shapes because older 3-argument renderers sometimes returned a component only to satisfy * the old `RenderFunction` type; those non-teardown values are ignored at runtime. Shared by the * core and Pro client renderers so the two cannot drift. * * @returns New renderer code should return `void` or `{ teardown }`. The broader legacy return * shapes stay accepted only so existing 3-argument renderers remain type-compatible. */ interface RendererFunction extends RenderFunctionMarker { (props?: Record<string, unknown>, railsContext?: RailsContext, domNodeId?: string): RendererFunctionResult; } /** * A render function variant for APIs that require a React component result, such as * Pro server-component wrappers. Unlike {@link RenderFunction}, this does not allow * server-render hashes/HTML, and unlike {@link RendererFunction}, this does not allow * renderer teardown results. * * Runtime render-function detection still follows the regular React on Rails * convention: declare at least two parameters, or set `renderFunction = true` * on one-argument functions. */ interface ReactComponentRenderFunction<Props = any> extends RenderFunctionMarker { (props?: Props, railsContext?: RailsContext, domNodeId?: string): ReactComponentRenderFunctionResult; } type StreamableComponentResult = ReactElement | Promise<ReactElement | string>; type AsyncPropsManager = { getProp: (propName: string) => Promise<unknown>; setProp: (propName: string, propValue: unknown) => void; endStream: () => void; }; /** * Render-functions are used to create dynamic React components or server-rendered HTML with side effects. * They receive two arguments: props and railsContext. * * @param props - The component props passed to the render function * @param railsContext - The Rails context object containing environment information * @returns A string, React component, React element, or a Promise resolving to a string * * @remarks * To distinguish a render function from a React Function Component: * 1. Ensure it accepts two parameters (props and railsContext), even if railsContext is unused, or * 2. Set the `renderFunction` property to `true` on the function object. * * If neither condition is met, it will be treated as a React Function Component, * and ReactDOMServer will attempt to render it. * * @example * // Option 1: Two-parameter function * const renderFunction = (props, railsContext) => { ... }; * * // Option 2: Using renderFunction property * const anotherRenderFunction = (props) => { ... }; * anotherRenderFunction.renderFunction = true; * * @remarks * `RenderFunction` is exactly this 2-argument server/client render-function form. The 3-argument * "renderer" form `(props, railsContext, domNodeId)` owns its own DOM rendering/hydration and is a * distinct role: type those functions {@link RendererFunction} (they return nothing or an optional * `{ teardown }` wrapper for cleanup). For `RenderFunction`, this makes the illegal combination — * a server render-function "returning" a teardown — unrepresentable instead of merely discouraged. * (`RendererFunction` still accepts legacy {@link RenderFunctionResult} return shapes for backward * compatibility with old 3-argument renderers; those values are ignored at runtime.) * * The doc block above describes {@link RenderFunction}, the public alias for this interface. Prefer * `RenderFunction` in public-facing annotations; `ServerRenderFunction` is the concrete interface * behind it (exported mainly so call sites can narrow to the precise role after runtime guards). */ interface ServerRenderFunction extends RenderFunctionMarker { (props?: any, railsContext?: RailsContext): RenderFunctionResult; } /** * The public name for the 2-argument server/client render-function form * `(props, railsContext) => RenderFunctionResult`. Alias of {@link ServerRenderFunction}; prefer * `RenderFunction` in public-facing annotations. See {@link ServerRenderFunction} for the full * render-function vs. renderer role explanation. */ type RenderFunction = ServerRenderFunction; type ReactComponentOrRenderFunction = ReactComponent | RenderFunction | RendererFunction; type RegisteredComponentValue = ReactComponentOrRenderFunction | Record<string, unknown>; type PipeableOrReadableStream = PipeableStream | NodeJS.ReadableStream; export type { ReactComponentOrRenderFunction, RegisteredComponentValue, ReactComponent, ReactComponentRenderFunction, AuthenticityHeaders, RenderFunction, ServerRenderFunction, RendererTeardown, RendererTeardownResult, RendererFunction, RenderFunctionResult, RendererFunctionResult, Store, StoreGenerator, CreateReactOutputResult, ServerRenderResult, ServerRenderHashRenderedHtml, CreateReactOutputSyncResult, CreateReactOutputAsyncResult, RenderFunctionSyncResult, RenderFunctionAsyncResult, ReactComponentRenderFunctionResult, StreamableComponentResult, PipeableOrReadableStream, }; /** * The generic defaults to the pre-object-registration component type so existing consumers that * read `registeredComponent.component` stay source-compatible. Use * `RegisteredComponent<RegisteredComponentValue>` when handling plain-object server_render_js * registrations. */ export interface RegisteredComponent<ComponentValue extends RegisteredComponentValue = ReactComponentOrRenderFunction> { name: string; component: ComponentValue; /** * Indicates if the registered component is a RenderFunction * @see RenderFunction for more details on its behavior and usage. */ renderFunction: boolean; isRenderer: boolean; } export type ItemRegistrationCallback<T> = (component: T) => void; export type GenerateRSCPayloadFunction = (componentName: string, props: unknown, railsContext: RailsContextWithServerComponentMetadata) => Promise<NodeJS.ReadableStream>; interface Params { props?: Record<string, unknown>; railsContext?: RailsContext; domNodeId?: string; trace?: boolean; generateRSCPayload?: GenerateRSCPayloadFunction; } export interface RenderParams extends Params { name: string; throwJsErrors: boolean; renderingReturnsPromises: boolean; } export interface RSCRenderParams extends Omit<RenderParams, 'railsContext'> { railsContext: RailsContextWithServerStreamingCapabilities; } export interface CreateParams extends Params { componentObj: RegisteredComponent<RegisteredComponentValue>; shouldHydrate?: boolean; } export interface ErrorOptions { e: Error & { fileName?: string; lineNumber?: string; }; name?: string; jsCode?: string; serverSide: boolean; } export type RenderingError = Pick<Error, 'message' | 'stack'>; export type FinalHtmlResult = string | ServerRenderHashRenderedHtml; export interface RenderResult { html: FinalHtmlResult | null; clientProps?: Record<string, unknown>; consoleReplayScript: string; hasErrors: boolean; renderingError?: RenderingError; isShellReady?: boolean; } export interface RSCPayloadChunk extends RenderResult { html: string; } export interface Root { render(children: ReactNode): void; unmount(): void; } export type RenderReturnType = void | Element | Component | Root; /** Extra context React on Rails adds when invoking registered root error callbacks. */ export interface RootErrorContext { /** Name of the registered component rendered into the affected React root, when known. */ componentName?: string; /** DOM id of the element the affected React root was mounted in, when known. */ domNodeId?: string; } /** * A root error callback registered through `ReactOnRails.setOptions({ rootErrorHandlers })`. * Receives React's original `(error, errorInfo)` arguments plus React on Rails context about the * affected root. `errorInfo` typically contains `componentStack` (see the `react-dom/client` * `createRoot`/`hydrateRoot` option docs for the exact per-callback shape). */ export type RootErrorHandler = (error: unknown, errorInfo: unknown, context: RootErrorContext) => void; /** * User-registered React root error callbacks, applied to every React root that React on Rails * creates via `hydrateRoot`/`createRoot`. Register them before your components render (typically * in the same pack file where you call `ReactOnRails.register`); each root captures the callbacks * registered at the moment it is created. Partial updates merge per key: a later * `setOptions({ rootErrorHandlers })` call that sets only one callback keeps the others; pass an * explicit `undefined` for a key to clear just that callback. Passing `null` is invalid and * throws at runtime; use `undefined` to deregister. */ export interface RootErrorHandlers { /** * Called when React automatically recovers from an error, e.g. a hydration mismatch. * Requires React 18+. */ onRecoverableError?: RootErrorHandler; /** Called for errors caught by an error boundary. Requires React 19+. */ onCaughtError?: RootErrorHandler; /** Called for errors not caught by any error boundary. Requires React 19+. */ onUncaughtError?: RootErrorHandler; } export interface ReactOnRailsOptions { /** Gives you debugging messages on Turbolinks events. */ traceTurbolinks?: boolean; /** Turbo (the successor of Turbolinks) events will be registered, if set to true. */ turbo?: boolean; /** Enable debug mode for detailed logging of React on Rails operations. */ debugMode?: boolean; /** Log component registration details including timing and size information. */ logComponentRegistration?: boolean; /** * React root error callbacks (`onRecoverableError`, `onCaughtError`, `onUncaughtError`) * applied to every React root created by React on Rails. * @see {RootErrorHandlers} */ rootErrorHandlers?: RootErrorHandlers; } export interface ReactOnRails { /** * Main entry point to using the react-on-rails npm package. This is how Rails will be able to * find you components for rendering. * @param components keys are component names, values are components */ register(components: Record<string, RegisteredComponentValue>): void; /** @deprecated Use registerStoreGenerators instead */ registerStore(stores: Record<string, StoreGenerator>): void; /** * Allows registration of store generators for legacy or advanced pages where multiple React roots * on one Rails view share a Redux store. Store generators receive props and railsContext, then * return a store. Note that the `setStore` API is different in that it's the actual store hydrated * with props. * @param storeGenerators keys are store names, values are the store generators */ registerStoreGenerators(storeGenerators: Record<string, StoreGenerator>): void; /** * Allows retrieval of the store by name. This store will be hydrated by any Rails form props. * @param name * @param [throwIfMissing=true] When false, this function will return undefined if * there is no store with the given name. * @returns Redux Store, possibly hydrated */ getStore(name: string, throwIfMissing?: boolean): Store | undefined; /** * Get a store by name, or wait for it to be registered. */ getOrWaitForStore(name: string): Promise<Store>; /** * Get a store generator by name, or wait for it to be registered. */ getOrWaitForStoreGenerator(name: string): Promise<StoreGenerator>; /** * Set options for ReactOnRails, typically before you call `ReactOnRails.register`. * @see {ReactOnRailsOptions} */ setOptions(newOptions: Partial<ReactOnRailsOptions>): void; /** * Renders or hydrates the React element passed. In case React version is >=18 will use the root API. * @param domNode * @param reactElement * @param hydrate if true will perform hydration, if false will render * @returns {Root|ReactComponent|ReactElement|null} */ reactHydrateOrRender(domNode: Element, reactElement: ReactElement, hydrate: boolean): RenderReturnType; /** * Allow directly calling the page loaded script in case the default events that trigger React * rendering are not sufficient, such as when loading JavaScript asynchronously with TurboLinks. * More details can be found here: * https://github.com/shakacode/react_on_rails/blob/master/docs/additional-reading/turbolinks.md */ reactOnRailsPageLoaded(): Promise<void>; reactOnRailsComponentLoaded(domId: string): Promise<void>; reactOnRailsStoreLoaded(storeName: string): Promise<void>; /** * Returns CSRF authenticity token inserted by Rails csrf_meta_tags * @returns String or null */ authenticityToken(): string | null; /** * Returns headers with CSRF authenticity token and XMLHttpRequest * @param otherHeaders Other headers */ authenticityHeaders(otherHeaders: Record<string, string>): AuthenticityHeaders; } export type RSCPayloadStreamInfo = { stream: NodeJS.ReadableStream; props: unknown; componentName: string; }; export type RSCPayloadCallback = (streamInfo: RSCPayloadStreamInfo) => void; export type WithAsyncProps<AsyncPropsType extends Record<string, unknown>, PropsType extends Record<string, unknown>> = PropsType & { getReactOnRailsAsyncProp: <PropName extends keyof AsyncPropsType>(propName: PropName) => Promise<AsyncPropsType[PropName]>; }; /** Contains the parts of the `ReactOnRails` API intended for internal use only. */ export interface ReactOnRailsInternal extends ReactOnRails { /** * Retrieve an option by key. * @param key * @returns option value */ option<K extends keyof ReactOnRailsOptions>(key: K): ReactOnRailsOptions[K]; /** * Allows retrieval of the store generator by name. This is used internally by ReactOnRails after * a Rails form loads to prepare stores. * @param name * @returns Redux Store generator function */ getStoreGenerator(name: string): StoreGenerator; /** * Allows saving the store populated by Rails form props. Used internally by ReactOnRails. */ setStore(name: string, store: Store): void; /** * Clears `hydratedStores` to avoid accidental usage of wrong store hydrated in a previous/parallel * request. */ clearHydratedStores(): void; /** * Clears registered store generators. Used by internal tests and setup code that must reset global * registration state between runs. */ clearStoreGenerators(): void; /** * @example * ```js * ReactOnRails.render("HelloWorldApp", {name: "Stranger"}, "app"); * ``` * * Does this: * ```js * ReactDOM.render( * React.createElement(HelloWorldApp, {name: "Stranger"}), * document.getElementById("app") * ); * ``` * under React 16/17 and * ```js * const root = ReactDOMClient.createRoot(document.getElementById("app")); * root.render(React.createElement(HelloWorldApp, {name: "Stranger"})); * return root; * ``` * under React 18+. * * @remarks * **Cleanup is the caller's responsibility.** Unlike the components React on Rails mounts itself * (which are unmounted automatically on Turbo/Turbolinks navigation and same-id node replacement), * a root created by this imperative API is **not** tracked internally. The returned root is handed * back to you, and you must call `unmount()` on it yourself — e.g. on a Turbo `turbo:before-render` * / Turbolinks `turbolinks:before-render` event, or in your framework's teardown hook — to avoid * leaking the root (and any subscriptions or timers it holds) across navigations. If you want * automatic cleanup instead, register a renderer function (the 3-argument render-function form) and * return a {@link RendererTeardownResult}; React on Rails tracks those mounts and runs the teardown for * you. * * @param name Name of your registered component * @param props Props to pass to your component * @param domNodeId HTML ID of the node the component will be rendered at * @param [hydrate=false] Pass truthy to update server rendered HTML. Default is falsy * @returns {Root|ReactComponent|ReactElement} Under React 18+: the created React root * (see "What is a root?" in https://github.com/reactwg/react-18/discussions/5). * Under React 16/17: Reference to your component's backing instance or `null` for stateless components. */ render(name: string, props: Record<string, unknown>, domNodeId: string, hydrate?: boolean): RenderReturnType; /** * Get the component that you registered * @returns {name, component, renderFunction, isRenderer} */ getComponent(name: string): RegisteredComponent<RegisteredComponentValue>; /** * Get the component that you registered, or wait for it to be registered * @returns {name, component, renderFunction, isRenderer} */ getOrWaitForComponent(name: string): Promise<RegisteredComponent<RegisteredComponentValue>>; /** * Used by server rendering by Rails */ serverRenderReactComponent(options: RenderParams): null | string | Promise<string>; /** * Used by Rails to select progressive streaming only when the installed React DOM server supports it. */ isServerStreamingSupported(): boolean; /** * Used by server rendering by Rails */ streamServerRenderedReactComponent(options: RenderParams): Readable; /** * Generates RSC payload, used by Rails */ serverRenderRSCReactComponent(options: RSCRenderParams): Readable; /** * Used by Rails to catch errors in rendering */ handleError(options: ErrorOptions): string | undefined; /** * Prepares a rendering result in the length-prefixed wire format for transport to Ruby. * Used by the server_render_js Rails helper to format arbitrary JS evaluation results. */ prepareRenderResult(html: string, consoleReplayScript: string, hasErrors: boolean, renderingError: RenderingError | null): string; /** * Used by Rails server rendering to replay console messages. * Returns the console replay script wrapped in script tags. */ buildConsoleReplay(): string; /** * Returns the console replay JavaScript code without wrapping it in script tags. * Useful when you need to add CSP nonce or other attributes to the script tag. */ getConsoleReplayScript(): string; /** * Get a Map containing all registered components. Useful for debugging. */ registeredComponents(): Map<string, RegisteredComponent<RegisteredComponentValue>>; /** * Get a Map containing all registered store generators. Useful for debugging. */ storeGenerators(): Map<string, StoreGenerator>; /** * Get a Map containing all hydrated stores. Useful for debugging. */ stores(): Map<string, Store>; /** * Reset options to default. */ resetOptions(): void; /** * Current options. */ options: ReactOnRailsOptions; /** * Indicates if the RSC bundle is being used. */ isRSCBundle: boolean; /** * Adds the getAsyncProp function to the component props object. * Uses getOrCreateAsyncPropsManager internally to handle race conditions * between initial render and update chunks. * * @param props - The component props to enhance * @param sharedExecutionContext - Map scoped to the current HTTP request * @returns An object containing the component props with getReactOnRailsAsyncProp added */ addAsyncPropsCapabilityToComponentProps: <AsyncPropsType extends Record<string, unknown>, PropsType extends Record<string, unknown>>(props: PropsType, sharedExecutionContext: Map<string, unknown>) => { props: WithAsyncProps<AsyncPropsType, PropsType>; }; /** * Gets or creates an AsyncPropsManager from the shared execution context. * Implements lazy initialization to handle race conditions between * the initial render request and update chunks. * * @param sharedExecutionContext - Map scoped to the current HTTP request * @returns The AsyncPropsManager instance (existing or newly created) */ getOrCreateAsyncPropsManager: (sharedExecutionContext: Map<string, unknown>) => AsyncPropsManager; } export type RenderStateHtml = FinalHtmlResult | Promise<FinalHtmlResult | ServerRenderResult>; export type RenderState = { result: null | RenderStateHtml; clientProps?: Record<string, unknown>; hasErrors: boolean; error?: RenderingError; }; export type StreamRenderState = Omit<RenderState, 'result'> & { result: null | Readable; isShellReady: boolean; }; export type RenderOptions = { componentName: string; domNodeId?: string; trace?: boolean; renderingReturnsPromises: boolean; }; //# sourceMappingURL=index.d.ts.map