|
| 1 | +# React on Rails Render Functions: Usage Guide |
| 2 | + |
| 3 | +Based on the test file `serverRenderReactComponent.test.ts` and the existing documentation, I'll clarify how render functions work in React on Rails and how to use them with each Ruby helper method. |
| 4 | + |
| 5 | +## Types of Render Functions and Their Return Values |
| 6 | + |
| 7 | +Looking at the test file, render functions can return several types of values: |
| 8 | + |
| 9 | +### 1. React Components (JSX) |
| 10 | + |
| 11 | +```jsx |
| 12 | +const MyComponent = (props, _railsContext) => { |
| 13 | + return <div>Hello {props.name}</div>; |
| 14 | +}; |
| 15 | +``` |
| 16 | + |
| 17 | +### 2. Objects with renderedHtml Property |
| 18 | + |
| 19 | +```jsx |
| 20 | +const MyComponent = (props, _railsContext) => { |
| 21 | + return { |
| 22 | + renderedHtml: `<div>Hello ${props.name}</div>`, |
| 23 | + }; |
| 24 | +}; |
| 25 | +``` |
| 26 | + |
| 27 | +### 3. Objects with Multiple HTML Properties |
| 28 | + |
| 29 | +```jsx |
| 30 | +const MyComponent = (props, _railsContext) => { |
| 31 | + return { |
| 32 | + componentHtml: <div>Hello {props.name}</div>, |
| 33 | + title: `<title>${props.title}</title>`, |
| 34 | + metaTags: `<meta name="description" content="${props.description}" />`, |
| 35 | + }; |
| 36 | +}; |
| 37 | +``` |
| 38 | + |
| 39 | +### 4. Promises of Strings |
| 40 | + |
| 41 | +```jsx |
| 42 | +const MyComponent = async (props, _railsContext) => { |
| 43 | + const data = await fetchData(); |
| 44 | + return `<div>Hello ${data.name}</div>`; |
| 45 | +}; |
| 46 | +``` |
| 47 | + |
| 48 | +### 5. Promises of Objects |
| 49 | + |
| 50 | +```jsx |
| 51 | +const MyComponent = async (props, _railsContext) => { |
| 52 | + const data = await fetchData(); |
| 53 | + return { |
| 54 | + componentHtml: `<div>Hello ${data.name}</div>`, |
| 55 | + }; |
| 56 | +}; |
| 57 | +``` |
| 58 | + |
| 59 | +### 6. Redirect Information |
| 60 | + |
| 61 | +```jsx |
| 62 | +const MyComponent = (props, _railsContext) => { |
| 63 | + return { |
| 64 | + redirectLocation: '/new-path', |
| 65 | + error: null, |
| 66 | + renderedHtml: null, |
| 67 | + }; |
| 68 | +}; |
| 69 | +``` |
| 70 | + |
| 71 | +## Important Limitations Observed in Tests |
| 72 | + |
| 73 | +The test file reveals some important limitations: |
| 74 | + |
| 75 | +1. **Direct String Returns Don't Work** - Returning a raw HTML string directly from a render function causes an error. The test `doesn't render html string returned directly from render function` demonstrates this. |
| 76 | + |
| 77 | +2. **Non-Promise Objects Need renderedHtml** - Non-promise objects must include a `renderedHtml` property to be valid, as shown in the test `doesn't render object without renderedHtml property`. |
| 78 | + |
| 79 | +3. **Async Functions Have Different Behavior** - Interestingly, the test `returns the object returned by async render function even if it doesn't have renderedHtml property` shows that Promise-returning functions can return objects without a `renderedHtml` property and they will still work. |
| 80 | + |
| 81 | +## Ruby Helper Functions with Render Functions |
| 82 | + |
| 83 | +### 1. react_component |
| 84 | + |
| 85 | +The `react_component` helper is used for rendering a single React component. It accepts various return types from render functions: |
| 86 | + |
| 87 | +```ruby |
| 88 | +# Basic usage with a component |
| 89 | +<%= react_component("MyComponent", props: { name: "John" }) %> |
| 90 | +
|
| 91 | +# With a render function that returns JSX |
| 92 | +<%= react_component("MyRenderFunction", props: { name: "John" }) %> |
| 93 | + |
| 94 | +# With a render function that returns an object with renderedHtml |
| 95 | +<%= react_component("MyRenderFunction", props: { name: "John" }) %> |
| 96 | +
|
| 97 | +# With a render function that returns a Promise |
| 98 | +<%= react_component("MyAsyncRenderFunction", props: { name: "John" }) %> |
| 99 | +``` |
| 100 | + |
| 101 | +#### When to use: |
| 102 | + |
| 103 | +- When you need to render a single component |
| 104 | +- When you're rendering client-side only |
| 105 | +- When your render function returns a single HTML string |
| 106 | + |
| 107 | +#### Not suitable for: |
| 108 | + |
| 109 | +- When your render function returns an object with multiple HTML strings |
| 110 | +- When you need to insert content in different parts of the page |
| 111 | + |
| 112 | +### 2. react_component_hash |
| 113 | + |
| 114 | +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. |
| 115 | + |
| 116 | +```ruby |
| 117 | +# With a render function that returns an object with multiple HTML properties |
| 118 | +<% helmet_data = react_component_hash("HelmetComponent", props: { |
| 119 | + title: "My Page", |
| 120 | + description: "Page description" |
| 121 | +}) %> |
| 122 | +
|
| 123 | +<% content_for :head do %> |
| 124 | + <%= helmet_data["title"] %> |
| 125 | + <%= helmet_data["metaTags"] %> |
| 126 | +<% end %> |
| 127 | +
|
| 128 | +<div class="main-content"> |
| 129 | + <%= helmet_data["componentHtml"] %> |
| 130 | +</div> |
| 131 | +``` |
| 132 | + |
| 133 | +#### When to use: |
| 134 | + |
| 135 | +- When your render function returns multiple HTML strings in an object |
| 136 | +- When you need to insert rendered content in different parts of your page |
| 137 | +- For SEO-related rendering like meta tags and title tags |
| 138 | +- When working with libraries like React Helmet |
| 139 | + |
| 140 | +#### Not suitable for: |
| 141 | + |
| 142 | +- Simple component rendering |
| 143 | +- Client-side only rendering (always uses server rendering) |
| 144 | + |
| 145 | +#### Requirements: |
| 146 | + |
| 147 | +- The render function MUST return an object |
| 148 | +- The object MUST include a `componentHtml` key |
| 149 | +- All other keys are optional and can be accessed in your Rails view |
| 150 | + |
| 151 | +## Best Practices Based on the Tests |
| 152 | + |
| 153 | +1. **Always Use Objects for Multiple HTML Parts**: If you need multiple HTML strings, return an object with named properties, and use `react_component_hash` to access them. |
| 154 | + |
| 155 | +2. **Don't Return Raw HTML Strings**: Tests show that returning a raw HTML string directly causes errors - either use JSX or wrap in a `{ renderedHtml: '...' }` object. |
| 156 | + |
| 157 | +3. **Async Functions Work with Objects**: For async render functions, you can return both strings and objects (with or without a `renderedHtml` property). |
| 158 | + |
| 159 | +4. **Use Redirect Object Format**: For redirects, return an object with `{ redirectLocation, error, renderedHtml: null }`. |
| 160 | + |
| 161 | +## Example: Different Return Types with Appropriate Helper Methods |
| 162 | + |
| 163 | +### Return Type 1: React Component |
| 164 | + |
| 165 | +```jsx |
| 166 | +// JavaScript |
| 167 | +const SimpleComponent = (props) => <div>Hello {props.name}</div>; |
| 168 | +ReactOnRails.register({ SimpleComponent }); |
| 169 | +``` |
| 170 | + |
| 171 | +```erb |
| 172 | +<%# Ruby - Either helper works %> |
| 173 | +<%= react_component("SimpleComponent", props: { name: "John" }) %> |
| 174 | +``` |
| 175 | + |
| 176 | +### Return Type 2: Object with renderedHtml |
| 177 | + |
| 178 | +```jsx |
| 179 | +// JavaScript |
| 180 | +const RenderedHtmlComponent = (props) => { |
| 181 | + return { renderedHtml: `<div>Hello ${props.name}</div>` }; |
| 182 | +}; |
| 183 | +ReactOnRails.register({ RenderedHtmlComponent }); |
| 184 | +``` |
| 185 | + |
| 186 | +```erb |
| 187 | +<%# Ruby - Either helper works %> |
| 188 | +<%= react_component("RenderedHtmlComponent", props: { name: "John" }) %> |
| 189 | +``` |
| 190 | + |
| 191 | +### Return Type 3: Object with Multiple HTML Properties |
| 192 | + |
| 193 | +```jsx |
| 194 | +// JavaScript |
| 195 | +const HelmetComponent = (props) => { |
| 196 | + return { |
| 197 | + componentHtml: <div>Hello {props.name}</div>, |
| 198 | + title: `<title>${props.title}</title>`, |
| 199 | + metaTags: `<meta name="description" content="${props.description}" />`, |
| 200 | + }; |
| 201 | +}; |
| 202 | +HelmetComponent.renderFunction = true; |
| 203 | +ReactOnRails.register({ HelmetComponent }); |
| 204 | +``` |
| 205 | + |
| 206 | +```erb |
| 207 | +<%# Ruby - MUST use react_component_hash %> |
| 208 | +<% helmet_data = react_component_hash("HelmetComponent", |
| 209 | + props: { name: "John", title: "My Page", description: "Page description" }) %> |
| 210 | +
|
| 211 | +<% content_for :head do %> |
| 212 | + <%= helmet_data["title"] %> |
| 213 | + <%= helmet_data["metaTags"] %> |
| 214 | +<% end %> |
| 215 | +
|
| 216 | +<div class="content"> |
| 217 | + <%= helmet_data["componentHtml"] %> |
| 218 | +</div> |
| 219 | +``` |
| 220 | + |
| 221 | +### Return Type 4: Promise of String |
| 222 | + |
| 223 | +```jsx |
| 224 | +// JavaScript |
| 225 | +const AsyncStringComponent = async (props) => { |
| 226 | + const data = await fetchData(); |
| 227 | + return `<div>Hello ${data.name}</div>`; |
| 228 | +}; |
| 229 | +AsyncStringComponent.renderFunction = true; |
| 230 | +ReactOnRails.register({ AsyncStringComponent }); |
| 231 | +``` |
| 232 | + |
| 233 | +```erb |
| 234 | +<%# Ruby %> |
| 235 | +<%= react_component("AsyncStringComponent", props: { dataUrl: "/api/data" }) %> |
| 236 | +``` |
| 237 | + |
| 238 | +### Return Type 5: Promise of Object |
| 239 | + |
| 240 | +```jsx |
| 241 | +// JavaScript |
| 242 | +const AsyncObjectComponent = async (props) => { |
| 243 | + const data = await fetchData(); |
| 244 | + return { |
| 245 | + componentHtml: <div>Hello {data.name}</div>, |
| 246 | + title: `<title>${data.title}</title>`, |
| 247 | + metaTags: `<meta name="description" content="${data.description}" />`, |
| 248 | + }; |
| 249 | +}; |
| 250 | +AsyncObjectComponent.renderFunction = true; |
| 251 | +ReactOnRails.register({ AsyncObjectComponent }); |
| 252 | +``` |
| 253 | + |
| 254 | +```erb |
| 255 | +<%# Ruby - MUST use react_component_hash %> |
| 256 | +<% helmet_data = react_component_hash("AsyncObjectComponent", props: { dataUrl: "/api/data" }) %> |
| 257 | +
|
| 258 | +<% content_for :head do %> |
| 259 | + <%= helmet_data["title"] %> |
| 260 | + <%= helmet_data["metaTags"] %> |
| 261 | +<% end %> |
| 262 | +
|
| 263 | +<div class="content"> |
| 264 | + <%= helmet_data["componentHtml"] %> |
| 265 | +</div> |
| 266 | +``` |
| 267 | + |
| 268 | +### Return Type 6: Redirect Object |
| 269 | + |
| 270 | +```jsx |
| 271 | +const RedirectComponent = (props, railsContext) => { |
| 272 | + if (!railsContext.currentUser) { |
| 273 | + return { |
| 274 | + redirectLocation: { pathname: '/login', search: '' }, // Object with pathname and search |
| 275 | + error: null, |
| 276 | + renderedHtml: null, // Use renderedHtml for consistency |
| 277 | + }; |
| 278 | + } |
| 279 | + return { |
| 280 | + renderedHtml: <div>Welcome {railsContext.currentUser.name}</div>, // Use renderedHtml for consistency |
| 281 | + }; |
| 282 | +}; |
| 283 | +RedirectComponent.renderFunction = true; |
| 284 | +ReactOnRails.register({ RedirectComponent }); |
| 285 | +``` |
| 286 | + |
| 287 | +```erb |
| 288 | +<%# Ruby - Either helper works %> |
| 289 | +<%= react_component("RedirectComponent") %> |
| 290 | +``` |
| 291 | + |
| 292 | +By understanding the different return types and which helper to use with each, you can create sophisticated server-rendered React components that fully integrate with your Rails views. |
0 commit comments