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.
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.React on Rails needs to identify which functions are render-functions (as opposed to regular React components). There are two ways to mark a function as a render function:
(props, railsContext) - React on Rails will detect this signature (the parameter names don't matter).renderFunction = true property to your function - This is useful when your function doesn't need the railsContext.// Method 1: Use signature with two parameters
const MyComponent = (props, railsContext) => {
return () => (
<div>
Hello {props.name} from {railsContext.pathname}
</div>
);
};
// Method 2: Use renderFunction property
const MyOtherComponent = (props) => {
return () => <div>Hello {props.name}</div>;
};
MyOtherComponent.renderFunction = true;
ReactOnRails.register({ MyComponent, MyOtherComponent });
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>;
};
[!NOTE] Ensure to return a React component (a function or class) and not a React element (the result of calling
React.createElementor JSX).
renderedHtml string propertyconst MyComponent = (props, _railsContext) => {
return {
renderedHtml: `<div>Hello ${props.name}</div>`,
};
};
renderedHtml as a React elementReact 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.
const MyComponent = async (props, _railsContext) => {
const data = await fetchData();
return `<div>Hello ${data.name}</div>`;
};
const MyComponent = async (props, _railsContext) => {
const data = await fetchData();
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 = await fetchData();
return () => <div>Hello {data.name}</div>;
};
[!NOTE] React on Rails does not perform actual page redirections. Instead, it returns an empty component and relies on the front end to handle the redirection when the router is rendered. The
redirectLocationproperty is logged in the console and ignored by the server renderer. If therouteErrorproperty is not null or undefined, it is logged and will cause Ruby to throw aReactOnRails::PrerenderErrorif theraise_on_prerender_errorconfiguration is enabled.
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.
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.
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.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.
componentHtml keyconst 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 = await fetchData();
return `<div>Hello ${data.name}</div>`;
};
AsyncStringComponent.renderFunction = true;
ReactOnRails.register({ AsyncStringComponent });
<%# Ruby %>
<%= react_component("AsyncStringComponent", props: { dataUrl: "/api/data" }) %>
const AsyncObjectComponent = async (props) => {
const data = await fetchData();
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: { dataUrl: "/api/data" }) %>
<% content_for :head do %>
<%= helmet_data["title"] %>
<%= helmet_data["metaTags"] %>
<% end %>
<div class="content">
<%= helmet_data["componentHtml"] %>
</div>
const AsyncReactComponent = async (props) => {
const data = await fetchData();
return () => <div>Hello {data.name}</div>;
};
AsyncReactComponent.renderFunction = true;
ReactOnRails.register({ AsyncReactComponent });
<%# Ruby %>
<%= react_component("AsyncReactComponent", props: { dataUrl: "/api/data" }) %>
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.