-
-
Couldn't load subscription status.
- Fork 638
Add Support for Async Render Functions Returning React Components #1720
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
AbanoubGhadban
merged 20 commits into
master
from
abanoubghadban/ror1719/add-support-for-async-render-function-returns-component
Apr 7, 2025
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
03a7e08
rename serverRenderReactComponent to TS
AbanoubGhadban f0837a6
convert serverRenderReactComponent test to TS
AbanoubGhadban 22a017a
Add usage guide for React on Rails render functions and tests for ser…
AbanoubGhadban 6f40195
update TS types to handle the case when render function return object
AbanoubGhadban 151e636
add support for returning react component from async render function
AbanoubGhadban a8cdfef
add support for streaming react components returned from async render…
AbanoubGhadban 5ddfe3f
on component streaming: report error at onError callback and return f…
AbanoubGhadban 1824d2e
Enhance error handling and support for async render functions in stre…
AbanoubGhadban 7cb666a
keep html in final render result as object instead of stringifying it
AbanoubGhadban c6136c0
Update ESLint configuration to allow default projects in test directo…
AbanoubGhadban 67b76c2
fix linting in serverRenderReactComponent.test.ts
AbanoubGhadban e575b64
update docs
AbanoubGhadban 4e8d41c
introduce StreamableComponentResult for improved type safety in strea…
AbanoubGhadban 9caf8a5
Update documentation for render functions to clarify property names a…
AbanoubGhadban b659db6
Improve formatting of documentation note regarding React component re…
AbanoubGhadban 4f33894
Refactor ESLint configuration to simplify default project settings an…
AbanoubGhadban 0a47bfd
Update documentation to standardize terminology by changing "render f…
AbanoubGhadban 2c3a53d
Empty commit to trigger new CI build
AbanoubGhadban b2ebbbf
update changelog
AbanoubGhadban 86f8bfb
Enhance documentation to clarify that promise options are exclusive t…
AbanoubGhadban File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,343 @@ | ||
| # React on Rails Render-Functions: Usage Guide | ||
|
|
||
| This guide explains how render-functions work in React on Rails and how to use them with Ruby helper methods. | ||
|
|
||
| ## Types of Render-Functions and Their Return Values | ||
|
|
||
| Render-functions take two parameters: | ||
|
|
||
| 1. `props`: The props passed from the Ruby helper methods (via the `props:` parameter), which become available in your JavaScript. | ||
| 2. `railsContext`: Rails contextual information like current pathname, locale, etc. See the [Render-Functions and the Rails Context](https://www.shakacode.com/react-on-rails/docs/guides/render-functions-and-railscontext/) documentation for more details. | ||
AbanoubGhadban marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ### Identifying Render-Functions | ||
|
|
||
| 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: | ||
|
|
||
| 1. Accept two parameters in your function definition: `(props, railsContext)` - React on Rails will detect this signature (the parameter names don't matter). | ||
| 2. Add a `renderFunction = true` property to your function - This is useful when your function doesn't need the railsContext. | ||
|
|
||
| ```jsx | ||
| // 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: | ||
|
|
||
| ### 1. React Components | ||
|
|
||
| ```jsx | ||
| 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.createElement` or JSX). | ||
AbanoubGhadban marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ### 2. Objects with `renderedHtml` string property | ||
|
|
||
| ```jsx | ||
| const MyComponent = (props, _railsContext) => { | ||
| return { | ||
| renderedHtml: `<div>Hello ${props.name}</div>`, | ||
| }; | ||
| }; | ||
| ``` | ||
|
|
||
| ### 3. Objects with `renderedHtml` as object containing `componentHtml` and other properties if needed (server-side hash) | ||
|
|
||
| ```jsx | ||
| const MyComponent = (props, _railsContext) => { | ||
| return { | ||
| renderedHtml: { | ||
| componentHtml: <div>Hello {props.name}</div>, | ||
| title: `<title>${props.title}</title>`, | ||
| metaTags: `<meta name="description" content="${props.description}" />`, | ||
| }, | ||
| }; | ||
| }; | ||
| ``` | ||
|
|
||
| ### 4. Promises of Strings | ||
AbanoubGhadban marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| This and other promise options below are only available in React on Rails Pro with the Node renderer. | ||
|
|
||
| ```jsx | ||
| const MyComponent = async (props, _railsContext) => { | ||
| const data = await fetchData(); | ||
| return `<div>Hello ${data.name}</div>`; | ||
| }; | ||
| ``` | ||
|
|
||
| ### 5. Promises of server-side hash | ||
|
|
||
| ```jsx | ||
| 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}" />`, | ||
| }; | ||
| }; | ||
| ``` | ||
|
|
||
| ### 6. Promises of React Components | ||
|
|
||
| ```jsx | ||
| const MyComponent = async (props, _railsContext) => { | ||
| const data = await fetchData(); | ||
| return () => <div>Hello {data.name}</div>; | ||
| }; | ||
| ``` | ||
|
|
||
| ### 7. Redirect Information | ||
|
|
||
| > [!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 `redirectLocation` property is logged in the console and ignored by the server renderer. If the `routeError` property is not null or undefined, it is logged and will cause Ruby to throw a `ReactOnRails::PrerenderError` if the `raise_on_prerender_error` configuration is enabled. | ||
|
|
||
| ```jsx | ||
| const MyComponent = (props, _railsContext) => { | ||
| return { | ||
| redirectLocation: { pathname: '/new-path', search: '' }, | ||
| routeError: null, | ||
| }; | ||
| }; | ||
| ``` | ||
|
|
||
| ## Important Rendering Behavior | ||
|
|
||
| Take a look at [serverRenderReactComponent.test.ts](https://github.com/shakacode/react_on_rails/blob/master/node_package/tests/serverRenderReactComponent.test.ts): | ||
|
|
||
| 1. **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: '...' }`. | ||
|
|
||
| 2. **Objects Require Specific Properties** - Non-promise objects must include a `renderedHtml` property to be valid when used with `react_component`. | ||
|
|
||
| 3. **Async Functions Support All Return Types** - Async functions can return React components, strings, or objects with any property structure due to special handling in the server renderer, but it doesn't support properties like `redirectLocation` and `routeError` that can be returned by sync render function. See [7. Redirect Information](#7-redirect-information). | ||
|
|
||
| ## Ruby Helper Functions | ||
|
|
||
| ### 1. react_component | ||
|
|
||
| The `react_component` helper renders a single React component in your view. | ||
|
|
||
| ```ruby | ||
| <%= react_component("MyComponent", props: { name: "John" }) %> | ||
| ``` | ||
AbanoubGhadban marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| This helper accepts render-functions that return React components, objects with a `renderedHtml` property, or promises that resolve to React components, or strings. | ||
|
|
||
| #### When to use: | ||
|
|
||
| - When you need to render a single component | ||
| - When you're rendering client-side only | ||
| - When your render function returns a single HTML string | ||
|
|
||
| #### Not suitable for: | ||
|
|
||
| - When your render function returns an object with multiple HTML strings | ||
| - When you need to insert content in different parts of the page, such as meta tags & style tags | ||
|
|
||
| ### 2. react_component_hash | ||
|
|
||
| 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. | ||
AbanoubGhadban marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ```ruby | ||
| # 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. | ||
|
|
||
| #### When to use: | ||
|
|
||
| - When your render function returns multiple HTML strings in an object | ||
| - When you need to insert rendered content in different parts of your page | ||
| - For SEO-related rendering like meta tags and title tags | ||
| - When working with libraries like React Helmet | ||
|
|
||
| #### Not suitable for: | ||
|
|
||
| - Simple component rendering | ||
| - Client-side only rendering (always uses server rendering) | ||
|
|
||
| #### Requirements: | ||
AbanoubGhadban marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| - The render function MUST return an object | ||
| - The object MUST include a `componentHtml` key | ||
| - All other keys are optional and can be accessed in your Rails view | ||
|
|
||
| ## Examples with Appropriate Helper Methods | ||
|
|
||
| ### Return Type 1: React Component | ||
|
|
||
| ```jsx | ||
| const SimpleComponent = (props, _railsContext) => () => <div>Hello {props.name}</div>; | ||
| ReactOnRails.register({ SimpleComponent }); | ||
| ``` | ||
|
|
||
| ```erb | ||
| <%# Ruby %> | ||
| <%= react_component("SimpleComponent", props: { name: "John" }) %> | ||
| ``` | ||
|
|
||
| ### Return Type 2: Object with renderedHtml | ||
|
|
||
| ```jsx | ||
| const RenderedHtmlComponent = (props, _railsContext) => { | ||
| return { renderedHtml: `<div>Hello ${props.name}</div>` }; | ||
| }; | ||
| ReactOnRails.register({ RenderedHtmlComponent }); | ||
| ``` | ||
|
|
||
| ```erb | ||
| <%# Ruby %> | ||
| <%= react_component("RenderedHtmlComponent", props: { name: "John" }) %> | ||
| ``` | ||
|
|
||
| ### Return Type 3: Object with server-side hash | ||
|
|
||
| ```jsx | ||
| const HelmetComponent = (props) => { | ||
| return { | ||
| renderedHtml: { | ||
| componentHtml: <div>Hello {props.name}</div>, | ||
| 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; | ||
AbanoubGhadban marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ReactOnRails.register({ HelmetComponent }); | ||
| ``` | ||
|
|
||
| ```erb | ||
| <%# 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> | ||
| ``` | ||
|
|
||
| ### Return Type 4: Promise of String | ||
|
|
||
| ```jsx | ||
| const AsyncStringComponent = async (props) => { | ||
| const data = await fetchData(); | ||
| return `<div>Hello ${data.name}</div>`; | ||
| }; | ||
| AsyncStringComponent.renderFunction = true; | ||
| ReactOnRails.register({ AsyncStringComponent }); | ||
| ``` | ||
|
|
||
| ```erb | ||
| <%# Ruby %> | ||
| <%= react_component("AsyncStringComponent", props: { dataUrl: "/api/data" }) %> | ||
| ``` | ||
|
|
||
| ### Return Type 5: Promise of server-side hash | ||
|
|
||
| ```jsx | ||
| 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 }); | ||
| ``` | ||
|
|
||
| ```erb | ||
| <%# 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> | ||
| ``` | ||
|
|
||
| ### Return Type 6: Promise of React Component | ||
|
|
||
| ```jsx | ||
| const AsyncReactComponent = async (props) => { | ||
| const data = await fetchData(); | ||
| return () => <div>Hello {data.name}</div>; | ||
| }; | ||
| AsyncReactComponent.renderFunction = true; | ||
| ReactOnRails.register({ AsyncReactComponent }); | ||
| ``` | ||
|
|
||
| ```erb | ||
| <%# Ruby %> | ||
| <%= react_component("AsyncReactComponent", props: { dataUrl: "/api/data" }) %> | ||
| ``` | ||
|
|
||
| ### Return Type 7: Redirect Object | ||
|
|
||
| ```jsx | ||
| const RedirectComponent = (props, railsContext) => { | ||
AbanoubGhadban marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (!railsContext.currentUser) { | ||
| return { | ||
| redirectLocation: { pathname: '/login', search: '' }, | ||
| }; | ||
| } | ||
| return { | ||
| renderedHtml: <div>Welcome {railsContext.currentUser.name}</div>, | ||
| }; | ||
| }; | ||
| RedirectComponent.renderFunction = true; | ||
| ReactOnRails.register({ RedirectComponent }); | ||
| ``` | ||
|
|
||
| ```erb | ||
| <%# Ruby %> | ||
| <%= react_component("RedirectComponent") %> | ||
| ``` | ||
|
|
||
| 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. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.