Skip to content
Merged
Show file tree
Hide file tree
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 Mar 30, 2025
f0837a6
convert serverRenderReactComponent test to TS
AbanoubGhadban Apr 3, 2025
22a017a
Add usage guide for React on Rails render functions and tests for ser…
AbanoubGhadban Mar 31, 2025
6f40195
update TS types to handle the case when render function return object
AbanoubGhadban Mar 31, 2025
151e636
add support for returning react component from async render function
AbanoubGhadban Mar 31, 2025
a8cdfef
add support for streaming react components returned from async render…
AbanoubGhadban Mar 31, 2025
5ddfe3f
on component streaming: report error at onError callback and return f…
AbanoubGhadban Mar 31, 2025
1824d2e
Enhance error handling and support for async render functions in stre…
AbanoubGhadban Mar 31, 2025
7cb666a
keep html in final render result as object instead of stringifying it
AbanoubGhadban Apr 2, 2025
c6136c0
Update ESLint configuration to allow default projects in test directo…
AbanoubGhadban Apr 3, 2025
67b76c2
fix linting in serverRenderReactComponent.test.ts
AbanoubGhadban Apr 3, 2025
e575b64
update docs
AbanoubGhadban Apr 3, 2025
4e8d41c
introduce StreamableComponentResult for improved type safety in strea…
AbanoubGhadban Apr 3, 2025
9caf8a5
Update documentation for render functions to clarify property names a…
AbanoubGhadban Apr 3, 2025
b659db6
Improve formatting of documentation note regarding React component re…
AbanoubGhadban Apr 3, 2025
4f33894
Refactor ESLint configuration to simplify default project settings an…
AbanoubGhadban Apr 4, 2025
0a47bfd
Update documentation to standardize terminology by changing "render f…
AbanoubGhadban Apr 6, 2025
2c3a53d
Empty commit to trigger new CI build
AbanoubGhadban Apr 6, 2025
b2ebbbf
update changelog
AbanoubGhadban Apr 7, 2025
86f8bfb
Enhance documentation to clarify that promise options are exclusive t…
AbanoubGhadban Apr 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Changes since the last non-beta release.

- Configuration option `generated_component_packs_loading_strategy` to control how generated component packs are loaded. It supports `sync`, `async`, and `defer` strategies. [PR 1712](https://github.com/shakacode/react_on_rails/pull/1712) by [AbanoubGhadban](https://github.com/AbanoubGhadban).

- Support for returning React component from async render-function. [PR 1720](https://github.com/shakacode/react_on_rails/pull/1720) by [AbanoubGhadban](https://github.com/AbanoubGhadban).

### Removed (Breaking Changes)

- Deprecated `defer_generated_component_packs` configuration option. You should use `generated_component_packs_loading_strategy` instead. [PR 1712](https://github.com/shakacode/react_on_rails/pull/1712) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
Expand Down
343 changes: 343 additions & 0 deletions docs/javascript/render-functions.md
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.

### 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).

### 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

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" }) %>
```

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.

```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:

- 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;
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) => {
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.
2 changes: 1 addition & 1 deletion eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ const config = tsEslint.config([
languageOptions: {
parserOptions: {
projectService: {
allowDefaultProject: ['eslint.config.ts', 'knip.ts'],
allowDefaultProject: ['eslint.config.ts', 'knip.ts', 'node_package/tests/*.test.ts'],
// Needed because `import * as ... from` instead of `import ... from` doesn't work in this file
// for some imports.
defaultProject: 'tsconfig.eslint.json',
Expand Down
12 changes: 7 additions & 5 deletions node_package/src/ReactOnRailsRSC.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { renderToPipeableStream } from 'react-on-rails-rsc/server.node';
import { PassThrough, Readable } from 'stream';
import type { ReactElement } from 'react';

import { RSCRenderParams, StreamRenderState } from './types';
import { RSCRenderParams, StreamRenderState, StreamableComponentResult } from './types';
import ReactOnRails from './ReactOnRails.full';
import buildConsoleReplay from './buildConsoleReplay';
import handleError from './handleError';
Expand All @@ -21,7 +20,10 @@ const stringToStream = (str: string) => {
return stream;
};

const streamRenderRSCComponent = (reactElement: ReactElement, options: RSCRenderParams): Readable => {
const streamRenderRSCComponent = (
reactRenderingResult: StreamableComponentResult,
options: RSCRenderParams,
): Readable => {
const { throwJsErrors, reactClientManifestFileName } = options;
const renderState: StreamRenderState = {
result: null,
Expand All @@ -31,8 +33,8 @@ const streamRenderRSCComponent = (reactElement: ReactElement, options: RSCRender

const { pipeToTransform, readableStream, emitError } =
transformRenderStreamChunksToResultObject(renderState);
loadReactClientManifest(reactClientManifestFileName)
.then((reactClientManifest) => {
Promise.all([loadReactClientManifest(reactClientManifestFileName), reactRenderingResult])
.then(([reactClientManifest, reactElement]) => {
const rscStream = renderToPipeableStream(reactElement, reactClientManifest, {
onError: (err) => {
const error = convertToError(err);
Expand Down
Loading
Loading