docs/oss/core-concepts/render-functions.md
This guide explains how render-functions work in React on Rails and how to use them with Ruby helper methods.
Before diving into render-functions, it helps to know the three kinds of values you can register with ReactOnRails.register. React on Rails classifies each registered entry based on its shape, and the classification determines where it can run (server, client, or both) and which Ruby helpers can invoke it.
| Type | Signature | Server (SSR) | Client | Detection rule |
|---|---|---|---|---|
| React Component | (props) => JSX or class component | Yes | Yes | Function.length <= 1 and no renderFunction flag |
| Render Function | (props, railsContext) => ... | Yes | Yes | Function.length >= 2 or fn.renderFunction === true |
| Renderer Function | (props, railsContext, domNodeId) => void | { teardown } | No — throws | Yes | A render function (detected first) with exactly Function.length === 3 |
A few important points about the detection:
Function.length (the number of declared parameters). Destructured parameters count as 1 — ({ name }) => ... has length 1.{ teardown: () => void | Promise<void> }, or a promise resolving to one. They take control of mounting/hydration themselves by calling ReactDOM.hydrateRoot / createRoot against domNodeId. Because there is no DOM on the server, registering a renderer function and then server-rendering it throws a descriptive error. Renderer functions are strictly client-side.fn.renderFunction = true is an escape hatch for render functions that don't need railsContext but still want to be treated as render functions (e.g., so they can return a hash). Without the flag, a one-parameter function is classified as a regular React component.import ReactDOMClient from 'react-dom/client';
// Regular React Component — 0 or 1 params, renders normally
const HelloMessage = (props) => <div>Hello {props.name}</div>;
// Render Function — 2 params, returns a React component or a hash
const HelloWithContext = (props, railsContext) => {
return () => (
<div>
Hello {props.name} from {railsContext.pathname}
</div>
);
};
// Render Function via the renderFunction flag — 1 param but still a render function
const HelloHash = (props) => {
return { renderedHtml: { componentHtml: `<div>Hello ${props.name}</div>` } };
};
HelloHash.renderFunction = true;
// Renderer Function — 3 params, handles hydration itself, CLIENT ONLY.
// Optionally return a { teardown } wrapper, or a promise resolving to one.
// React on Rails runs it on Turbo/Turbolinks navigation or same-id replacement.
const LazyHydrate = (props, _railsContext, domNodeId) =>
whenVisible(domNodeId).then(() => {
const domNode = document.getElementById(domNodeId);
// Navigation may remove the node before visibility resolves, so there is no mounted root to clean up.
if (!domNode) return undefined;
const root = ReactDOMClient.hydrateRoot(domNode, <HelloMessage {...props} />);
return { teardown: () => root.unmount() };
});
ReactOnRails.register({ HelloMessage, HelloWithContext, HelloHash, LazyHydrate });
whenVisible is a hypothetical helper that resolves when the element scrolls into view. The LazyHydrate example uses a concise-body arrow, so it returns the whenVisible(...).then(...) promise. If navigation removes the node before hydration runs, the callback returns nothing because there is no mounted root to clean up. If you switch the renderer to a { } block body, add an explicit return or React on Rails will not receive the teardown wrapper.
The rest of this document focuses on render functions — the most flexible of the three types, with the richest set of return values. For renderer functions (client-side mounting control), see Renderer Functions in the view helpers reference.
The Ruby helper you use in your Rails view must be compatible with the component type you registered. Mismatches usually produce a clear server-side error, but it's faster to pick the right combination upfront:
| Component type | react_component | react_component_hash | stream_react_component (Pro) |
|---|---|---|---|
| React Component (plain function / class) | ✅ Works (client-side rendering or SSR) | ❌ Raises — the helper requires a hash return, not a component | ✅ Works (streaming SSR) |
| Render Function returning a React component | ✅ Works | ❌ Raises — must return a hash, not a component | ✅ Works |
Render Function returning { renderedHtml: string } | ✅ Works | ❌ Raises — string is not a hash with componentHtml | ❌ Raises — streaming does not support server render hashes |
Render Function returning { renderedHtml: ReactElement } | ✅ Works (calls renderToString on the element) | ❌ Raises — element is not a hash with componentHtml | ❌ Raises — streaming does not support server render hashes |
Render Function returning a server-render hash ({ renderedHtml: { componentHtml, ... } }) | ⚠️ Raises — tells you to use react_component_hash | ✅ Works (the designed use case) | ❌ Raises — streaming does not support server render hashes |
| Async Render Function (returns a Promise) | ✅ Works with Pro Node renderer. ❌ ExecJS silently returns empty output — see Async functions and ExecJS. | ✅ Only if the promise resolves to a server-render hash with componentHtml. Pro Node renderer only. | ✅ Only if the promise resolves to a React component. Promises resolving to strings or server-render hashes are rejected — streaming does not support server render hashes. |
| Renderer Function (3 params) | ✅ Works with prerender: false (client-only). ❌ Throws with prerender: true — renderer functions cannot run on the server. | ❌ Raises — react_component_hash forces prerender: true, which is incompatible with renderer functions | ❌ Raises — streaming requires server rendering |
Key takeaways:
react_component_hash is specifically for the "multiple HTML strings in one response" use case. If your render function returns a plain component, string, or React element, use react_component instead.prerender: true, react_component_hash, or stream_react_component) will throw when used with a renderer function.Render-functions take two parameters:
props: The props passed from the Ruby helper methods (via the props: parameter), which become available in your JavaScript.railsContext: Rails contextual information like current pathname, locale, etc. See the Render-Functions and the Rails Context documentation for more details.As shown in the component types table above, React on Rails marks a function as a render function in two ways:
(props, railsContext) — React on Rails will detect this signature (the parameter names don't matter).renderFunction = true property to your function — useful when your function doesn't need railsContext.Render-functions can return several types of values:
const MyComponent = (props, _railsContext) => {
// The `props` parameter here is identical to the `props` passed from the Ruby helper methods (via the `props:` parameter).
// Both `props` and `reactProps` refer to the same object.
return (reactProps) => <div>Hello {props.name}</div>;
};
[!IMPORTANT] Return a React component (a function or class), not a React element. That means
return MyComponent;orreturn () => <div>…</div>;, notreturn <MyComponent />;orreturn <div>…</div>;. Returning a React element directly is deprecated: React on Rails currently logs aconsole.errorand still renders the element, but hooks silently don't work, and the behavior may change in a future release. If you need to return JSX from a render function, wrap it in a server-render hash — see Return Type 3 below.
renderedHtml string propertyconst MyComponent = (props, _railsContext) => {
return {
renderedHtml: `<div>Hello ${props.name}</div>`,
};
};
renderedHtml as a React elementThis is the supported way to return JSX from a render function: wrap it in { renderedHtml: ... }. React on Rails will call renderToString on the element and use the result as the server-rendered HTML. Unlike returning a React element directly, this form correctly satisfies React's Rules of Hooks — the element is rendered in a normal component tree context, so hooks that are SSR-compatible (e.g., useState, useContext) work as expected during server rendering.
React 19 Alternative: For metadata use cases (titles, meta tags), consider using React 19 Native Metadata instead of this pattern. React 19 hoists
<title>,<meta>, and<link>to<head>automatically, eliminating the need for server-side hash render-functions.
const MyComponent = (props, _railsContext) => {
return {
renderedHtml: <div>Hello {props.name}</div>,
};
};
renderedHtml as a server-side hash (componentHtml + optional keys)const MyComponent = (props, _railsContext) => {
const componentHtml = renderToString(<div>Hello {props.name}</div>);
return {
renderedHtml: {
componentHtml,
title: `<title>${props.title}</title>`,
metaTags: `<meta name="description" content="${props.description}" />`,
},
};
};
This and other promise options below are only available in React on Rails Pro with the Node renderer.
React on Rails note: Async render functions should still receive application data from Rails as regular props. For streaming slow props behind Suspense boundaries, React on Rails Pro async props inject
getReactOnRailsAsyncPropwhen the Rails view usesstream_react_component_with_async_props. That streaming setup requires the controller toinclude ReactOnRailsPro::Stream, render viastream_view_containing_react_components, and setconfig.enable_rsc_support = true. Keep authorization, database access, and cache-aware loading in Rails rather than fetching inside the render function. See RSC data fetching.
The async keyword is intentional in these examples: it makes the render function return a Promise so the Pro Node renderer can await the resolved string, hash, or component return value. The data is still prepared by Rails and read from props.
const MyComponent = async (props, _railsContext) => {
const data = props.data;
return `<div>Hello ${data.name}</div>`;
};
[!WARNING] Async render functions only work with the React on Rails Pro Node renderer. When a promise-returning render function is used on ExecJS (the OSS default SSR runtime), React on Rails logs a
console.errorand returns an empty JSON object ('{}') as the server-rendered output. The Rails view ends up with empty content and no visible exception, which can be hard to diagnose. If you use async render functions, make sure your server runtime is the Pro Node renderer.The exact error message logged to
console.erroris:Your render function returned a Promise, which is only supported by the React on Rails Pro Node renderer, not ExecJS.
const MyComponent = async (props, _railsContext) => {
const data = props.data;
return {
componentHtml: `<div>Hello ${data.name}</div>`,
title: `<title>${data.title}</title>`,
metaTags: `<meta name="description" content="${data.description}" />`,
};
};
const MyComponent = async (props, _railsContext) => {
const data = props.data;
return () => <div>Hello {data.name}</div>;
};
[!WARNING] These fields have significant limitations. They originated from React Router v3/v4 integrations but are still supported at the runtime level:
redirectLocationdoes not trigger an actual server-side HTTP redirect — Rails still returns the full response with an empty<div>. The redirect only takes effect once the client-side router renders.routeErroronly triggersraise_on_prerender_errorbehavior (if enabled) — it does not produce a user-facing error page.- Modern React Router v6 Declarative Mode (
StaticRouter) has no mechanism to produce these values.- React Router v6 Data Mode (
createStaticHandler) handles redirects viaResponseobjects, not these fields.Modern alternatives:
- For redirects during SSR, handle them in your Rails controller (e.g., check auth before rendering and call
redirect_to).- For client-side redirects, use React Router's
<Navigate to="/path" />(note: this is a no-op during SSR).- For route errors, use React Router's
errorElementor anErrorBoundary.
// Legacy pattern — prefer modern alternatives above
const MyComponent = (props, _railsContext) => {
return {
redirectLocation: { pathname: '/new-path', search: '' },
routeError: null,
};
};
Take a look at serverRenderReactComponent.test.ts:
Direct String Returns Don't Work - Returning a raw HTML string directly from a render function causes an error. Always wrap HTML strings in { renderedHtml: '...' }.
Objects Require Specific Properties - Non-promise objects must include a renderedHtml property to be valid when used with react_component.
Which object keys trigger "server render hash" processing — React on Rails treats a returned object as a server render hash if it contains any of these keys: renderedHtml, redirectLocation, routeError, or error. If none of those keys are present, the object is passed through unchanged (which typically fails validation elsewhere).
[!WARNING] The
errorkey is a landmine. If your render function accidentally returns{ error: someError }— for example from atry/catchblock — the framework routes it through server-render-hash handling, which produces empty HTML output (becauserenderedHtmlis missing). Note thathasErrorsis not set — onlyrouteErrorsets the error flag, so noPrerenderErroris raised regardless ofraise_on_prerender_error. If you want to signal failure, throw an error instead of returning one in a plain object.
Async Functions Support Server Render Hashes - When using the React on Rails Pro Node renderer, async render-functions can return React components, strings, or full server render hashes, including clientProps, redirectLocation, and routeError. See 8. Redirect Information (Legacy).
clientProps are merged back into hydration props - If a server render result includes clientProps, React on Rails merges those keys into the client hydration props generated by react_component.
original_props.merge(clientProps), so keys from clientProps override matching original keys.props: to be a Ruby Hash or a JSON string representing an object. If you pass any other type (including nil), the helper raises an error with a message pointing to this requirement.:locale) and clientProps returns the same name as a string ("locale"), the merge writes to the existing symbol key to preserve its type. If your original props contain both forms of the same key (:locale and "locale"), the merge raises an error rather than guessing which one you meant.The react_component helper renders a single React component in your view.
<%= react_component("MyComponent", props: { name: "John" }) %>
This helper accepts render-functions that return React components, objects with a renderedHtml property, or promises that resolve to React components, strings, or server-side hash objects.
If your render-function returns clientProps, this helper also injects those values into the generated client hydration payload.
The react_component_hash helper is used when your render function returns an object with multiple HTML strings. It allows you to place different parts of the rendered output in different parts of your layout.
# With a render function that returns an object with multiple HTML properties
<% helmet_data = react_component_hash("HelmetComponent", props: {
title: "My Page",
description: "Page description"
}) %>
<% content_for :head do %>
<%= helmet_data["title"] %>
<%= helmet_data["metaTags"] %>
<% end %>
<div class="main-content">
<%= helmet_data["componentHtml"] %>
</div>
This helper accepts render-functions that return objects with a renderedHtml property containing componentHtml and any other necessary properties. It also supports promises that resolve to a server-side hash.
{ renderedHtml: { componentHtml, ...otherKeys } }renderedHtml object MUST include a componentHtml key — missing it raises ReactOnRails::ErrorrenderedHtml are optional and can be accessed in your Rails view as result["keyName"]const SimpleComponent = (props, _railsContext) => () => <div>Hello {props.name}</div>;
ReactOnRails.register({ SimpleComponent });
<%# Ruby %>
<%= react_component("SimpleComponent", props: { name: "John" }) %>
const RenderedHtmlComponent = (props, _railsContext) => {
return { renderedHtml: `<div>Hello ${props.name}</div>` };
};
ReactOnRails.register({ RenderedHtmlComponent });
<%# Ruby %>
<%= react_component("RenderedHtmlComponent", props: { name: "John" }) %>
renderedHtml React elementconst ElementHtmlComponent = (props, _railsContext) => {
return {
renderedHtml: <div>Hello {props.name}</div>,
};
};
ElementHtmlComponent.renderFunction = true;
ReactOnRails.register({ ElementHtmlComponent });
<%# Ruby %>
<%= react_component("ElementHtmlComponent", props: { name: "John" }, prerender: true) %>
const HelmetComponent = (props) => {
const componentHtml = renderToString(<div>Hello {props.name}</div>);
return {
renderedHtml: {
componentHtml,
title: `<title>${props.title}</title>`,
metaTags: `<meta name="description" content="${props.description}" />`,
},
};
};
// The render function should either:
// 1. Accept two arguments: (props, railsContext)
// 2. Have a property `renderFunction` set to true
HelmetComponent.renderFunction = true;
ReactOnRails.register({ HelmetComponent });
<%# Ruby - MUST use react_component_hash %>
<% helmet_data = react_component_hash("HelmetComponent",
props: { name: "John", title: "My Page", description: "Page description" }) %>
<% content_for :head do %>
<%= helmet_data["title"] %>
<%= helmet_data["metaTags"] %>
<% end %>
<div class="content">
<%= helmet_data["componentHtml"] %>
</div>
const AsyncStringComponent = async (props) => {
const data = props.data;
return `<div>Hello ${data.name}</div>`;
};
AsyncStringComponent.renderFunction = true;
ReactOnRails.register({ AsyncStringComponent });
<%# Ruby %>
<%= react_component("AsyncStringComponent", props: { data: { name: @user.name } }) %>
const AsyncObjectComponent = async (props) => {
const data = props.data;
return {
componentHtml: `<div>Hello ${data.name}</div>`,
title: `<title>${data.title}</title>`,
metaTags: `<meta name="description" content="${data.description}" />`,
};
};
AsyncObjectComponent.renderFunction = true;
ReactOnRails.register({ AsyncObjectComponent });
<%# Ruby - MUST use react_component_hash %>
<% helmet_data = react_component_hash("AsyncObjectComponent",
props: {
data: {
name: @user.name,
title: "#{@user.name}'s Profile",
description: @user.bio
}
}) %>
<% content_for :head do %>
<%= helmet_data["title"] %>
<%= helmet_data["metaTags"] %>
<% end %>
<div class="content">
<%= helmet_data["componentHtml"] %>
</div>
const AsyncReactComponent = async (props) => {
const data = props.data;
return () => <div>Hello {data.name}</div>;
};
AsyncReactComponent.renderFunction = true;
ReactOnRails.register({ AsyncReactComponent });
<%# Ruby %>
<%= react_component("AsyncReactComponent", props: { data: { name: @user.name } }) %>
const RedirectComponent = (props, railsContext) => {
if (!railsContext.currentUser) {
return {
redirectLocation: { pathname: '/login', search: '' },
};
}
return {
renderedHtml: <div>Welcome {railsContext.currentUser.name}</div>,
};
};
RedirectComponent.renderFunction = true;
ReactOnRails.register({ RedirectComponent });
<%# Ruby %>
<%= react_component("RedirectComponent") %>
clientProps for hydrationconst RouterShell = (props, railsContext) => {
const componentHtml = renderToString(<App initialUrl={railsContext.location} />);
return {
renderedHtml: componentHtml,
clientProps: {
routerDehydratedState: { url: railsContext.location },
},
};
};
RouterShell.renderFunction = true;
ReactOnRails.register({ RouterShell });
<%# Ruby: pass a Hash or a JSON object string so clientProps can merge correctly %>
<%= react_component("RouterShell", props: { locale: I18n.locale }, prerender: true) %>
By understanding these return types and which helper to use with each, you can create sophisticated server-rendered React components that fully integrate with your Rails views.