diff --git a/docs/api/javascript-api.md b/docs/api/javascript-api.md index deb802f36..e17d9cfc3 100644 --- a/docs/api/javascript-api.md +++ b/docs/api/javascript-api.md @@ -18,7 +18,7 @@ If you are using [jquery-ujs](https://github.com/rails/jquery-ujs) for AJAX call ## API -The best source of docs is the main [ReactOnRails.ts](https://github.com/shakacode/react_on_rails/blob/master/node_package/src/ReactOnRails.ts) file. Here's a quick summary. No guarantees that this won't be outdated! +The best source of docs is the `interface ReactOnRails` in [types/index.ts](https://github.com/shakacode/react_on_rails/blob/master/node_package/src/types/index.ts). Here's a quick summary. No guarantees that this won't be outdated! ```js /** @@ -44,7 +44,7 @@ register(components); * the setStore API is different in that it's the actual store hydrated with props. * @param stores (key is store name, value is the store generator) */ -registerStore(stores); +registerStoreGenerators(storesGenerators); /** * Allows retrieval of the store by name. This store will be hydrated by any Rails form props. @@ -85,7 +85,6 @@ reactOnRailsPageLoaded(); * Returns CSRF authenticity token inserted by Rails csrf_meta_tags * @returns String or null */ - authenticityToken(); /** @@ -93,6 +92,5 @@ authenticityToken(); * @param {*} other headers * @returns {*} header */ - authenticityHeaders((otherHeaders = {})); ``` diff --git a/node_package/src/ReactOnRails.client.ts b/node_package/src/ReactOnRails.client.ts index 24ff1e709..fe7ee9623 100644 --- a/node_package/src/ReactOnRails.client.ts +++ b/node_package/src/ReactOnRails.client.ts @@ -15,6 +15,7 @@ import type { AuthenticityHeaders, Store, StoreGenerator, + ReactOnRailsOptions, } from './types'; import reactHydrateOrRender from './reactHydrateOrRender'; @@ -40,11 +41,7 @@ const DEFAULT_OPTIONS = { ctx.ReactOnRails = { options: {}, - /** - * 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 (key is component name, value is component) - */ + register(components: Record): void { ComponentRegistry.register(components); }, @@ -53,12 +50,6 @@ ctx.ReactOnRails = { this.registerStoreGenerators(stores); }, - /** - * Allows registration of store generators to be used by multiple React components on one Rails - * view. store generators are functions that take one arg, props, and 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): void { if (!storeGenerators) { throw new Error( @@ -70,55 +61,23 @@ ctx.ReactOnRails = { StoreRegistry.register(storeGenerators); }, - /** - * Allows retrieval of the store by name. This store will be hydrated by any Rails form props. - * Pass optional param throwIfMissing = false if you want to use this call to get back null if the - * store with name is not registered. - * @param name - * @param throwIfMissing Defaults to true. Set to false to have this call return undefined if - * there is no store with the given name. - * @returns Redux Store, possibly hydrated - */ getStore(name: string, throwIfMissing = true): Store | undefined { return StoreRegistry.getStore(name, throwIfMissing); }, - /** - * Get a store by name, or wait for it to be registered. - * @param name - * @returns Promise - */ getOrWaitForStore(name: string): Promise { return StoreRegistry.getOrWaitForStore(name); }, - /** - * Get a store generator by name, or wait for it to be registered. - * @param name - * @returns Promise - */ getOrWaitForStoreGenerator(name: string): Promise { return StoreRegistry.getOrWaitForStoreGenerator(name); }, - /** - * 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 { return reactHydrateOrRender(domNode, reactElement, hydrate); }, - /** - * Set options for ReactOnRails, typically before you call ReactOnRails.register - * Available Options: - * `traceTurbolinks: true|false Gives you debugging messages on Turbolinks events - * `turbo: true|false Turbo (the follower of Turbolinks) events will be registered, if set to true. - */ - setOptions(newOptions: { traceTurbolinks?: boolean; turbo?: boolean }): void { + setOptions(newOptions: Partial): void { if (typeof newOptions.traceTurbolinks !== 'undefined') { this.options.traceTurbolinks = newOptions.traceTurbolinks; @@ -138,12 +97,6 @@ ctx.ReactOnRails = { } }, - /** - * 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() { return ClientStartup.reactOnRailsPageLoaded(); }, @@ -156,21 +109,10 @@ ctx.ReactOnRails = { return hydrateStore(storeName); }, - /** - * Returns CSRF authenticity token inserted by Rails csrf_meta_tags - * @returns String or null - */ - authenticityToken(): string | null { return Authenticity.authenticityToken(); }, - /** - * Returns header with csrf authenticity token and XMLHttpRequest - * @param otherHeaders Other headers - * @returns {*} header - */ - authenticityHeaders(otherHeaders: Record = {}): AuthenticityHeaders { return Authenticity.authenticityHeaders(otherHeaders); }, @@ -179,66 +121,22 @@ ctx.ReactOnRails = { // INTERNALLY USED APIs // ///////////////////////////////////////////////////////////////////////////// - /** - * Retrieve an option by key. - * @param key - * @returns option value - */ - option(key: string): string | number | boolean | undefined { + option(key: K): ReactOnRailsOptions[K] | undefined { return this.options[key]; }, - /** - * 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 { return StoreRegistry.getStoreGenerator(name); }, - /** - * Allows saving the store populated by Rails form props. Used internally by ReactOnRails. - * @returns Redux Store, possibly hydrated - */ setStore(name: string, store: Store): void { StoreRegistry.setStore(name, store); }, - /** - * Clears hydratedStores to avoid accidental usage of wrong store hydrated in previous/parallel - * request. - */ clearHydratedStores(): void { StoreRegistry.clearHydratedStores(); }, - /** - * @example - * 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+. - * - * @param name Name of your registered component - * @param props Props to pass to your component - * @param domNodeId - * @param hydrate 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, domNodeId: string, hydrate: boolean): RenderReturnType { const componentObj = ComponentRegistry.get(name); const reactElement = createReactOutput({ componentObj, props, domNodeId }); @@ -250,88 +148,48 @@ ctx.ReactOnRails = { ); }, - /** - * Get the component that you registered - * @param name - * @returns {name, component, renderFunction, isRenderer} - */ getComponent(name: string): RegisteredComponent { return ComponentRegistry.get(name); }, - /** - * Get the component that you registered, or wait for it to be registered - * @param name - * @returns {name, component, renderFunction, isRenderer} - */ getOrWaitForComponent(name: string): Promise { return ComponentRegistry.getOrWaitForComponent(name); }, - /** - * Used by server rendering by Rails - * @param options - */ serverRenderReactComponent(): null | string | Promise { throw new Error( 'serverRenderReactComponent is not available in "react-on-rails/client". Import "react-on-rails" server-side.', ); }, - /** - * Used by server rendering by Rails - * @param options - */ streamServerRenderedReactComponent() { throw new Error( 'streamServerRenderedReactComponent is only supported when using a bundle built for Node.js environments', ); }, - /** - * Generates RSC payload, used by Rails - */ serverRenderRSCReactComponent() { throw new Error('serverRenderRSCReactComponent is supported in RSC bundle only.'); }, - /** - * Used by Rails to catch errors in rendering - * @param options - */ handleError(): string | undefined { throw new Error( 'handleError is not available in "react-on-rails/client". Import "react-on-rails" server-side.', ); }, - /** - * Used by Rails server rendering to replay console messages. - */ buildConsoleReplay(): string { return buildConsoleReplay(); }, - /** - * Get an Object containing all registered components. Useful for debugging. - * @returns {*} - */ registeredComponents(): Map { return ComponentRegistry.components(); }, - /** - * Get an Object containing all registered store generators. Useful for debugging. - * @returns {*} - */ storeGenerators(): Map { return StoreRegistry.storeGenerators(); }, - /** - * Get an Object containing all hydrated stores. Useful for debugging. - * @returns {*} - */ stores(): Map { return StoreRegistry.stores(); }, diff --git a/node_package/src/ReactOnRails.full.ts b/node_package/src/ReactOnRails.full.ts index 2a5f0e856..91ca57eb1 100644 --- a/node_package/src/ReactOnRails.full.ts +++ b/node_package/src/ReactOnRails.full.ts @@ -10,16 +10,8 @@ if (typeof window !== 'undefined') { ); } -/** - * Used by Rails to catch errors in rendering - * @param options - */ Client.handleError = (options: ErrorOptions): string | undefined => handleError(options); -/** - * Used by server rendering by Rails - * @param options - */ Client.serverRenderReactComponent = (options: RenderParams): null | string | Promise => serverRenderReactComponent(options); diff --git a/node_package/src/context.ts b/node_package/src/context.ts index e8e1bad52..1ce2f49a5 100644 --- a/node_package/src/context.ts +++ b/node_package/src/context.ts @@ -1,4 +1,4 @@ -import type { ReactOnRails as ReactOnRailsType, RailsContext } from './types'; +import type { ReactOnRailsInternal as ReactOnRailsType, RailsContext } from './types'; declare global { interface Window { diff --git a/node_package/src/types/index.ts b/node_package/src/types/index.ts index 8574069c4..4e19e8230 100644 --- a/node_package/src/types/index.ts +++ b/node_package/src/types/index.ts @@ -3,10 +3,13 @@ import type { ReactElement, ReactNode, Component, ComponentType } from 'react'; 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. /* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * 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; }; @@ -169,38 +172,183 @@ export interface Root { // eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- inherited from React 16/17, can't avoid here export type RenderReturnType = void | Element | Component | Root; +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; +} + 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): void; /** @deprecated Use registerStoreGenerators instead */ registerStore(stores: Record): void; + /** + * Allows registration of store generators to be used by multiple React components on one Rails + * view. Store generators are functions that take one arg, props, and 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): 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; + /** + * Get a store generator by name, or wait for it to be registered. + */ getOrWaitForStoreGenerator(name: string): Promise; - setOptions(newOptions: { traceTurbolinks: boolean }): void; + /** + * Set options for ReactOnRails, typically before you call `ReactOnRails.register`. + * @see {ReactOnRailsOptions} + */ + setOptions(newOptions: Partial): 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; reactOnRailsComponentLoaded(domId: string): Promise; reactOnRailsStoreLoaded(storeName: string): Promise; + /** + * 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): AuthenticityHeaders; - option(key: string): string | number | boolean | undefined; +} + +/** 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(key: K): ReactOnRailsOptions[K] | undefined; + /** + * 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; - render(name: string, props: Record, domNodeId: string, hydrate: boolean): RenderReturnType; + /** + * @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+. + * + * @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, domNodeId: string, hydrate?: boolean): RenderReturnType; + /** + * Get the component that you registered + * @returns {name, component, renderFunction, isRenderer} + */ getComponent(name: string): RegisteredComponent; + /** + * Get the component that you registered, or wait for it to be registered + * @returns {name, component, renderFunction, isRenderer} + */ getOrWaitForComponent(name: string): Promise; + /** + * Used by server rendering by Rails + */ serverRenderReactComponent(options: RenderParams): null | string | Promise; + /** + * 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; + /** + * Used by Rails server rendering to replay console messages. + */ buildConsoleReplay(): string; + /** + * Get a Map containing all registered components. Useful for debugging. + */ registeredComponents(): Map; + /** + * Get a Map containing all registered store generators. Useful for debugging. + */ storeGenerators(): Map; + /** + * Get a Map containing all hydrated stores. Useful for debugging. + */ stores(): Map; + /** + * Reset options to default. + */ resetOptions(): void; - options: Record; + /** + * Current options. + */ + options: ReactOnRailsOptions; } export type RenderState = {