Skip to content
4 changes: 4 additions & 0 deletions docs/api-reference/redux-store-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
>
> This Redux API is no longer recommended as it prevents dynamic code splitting for performance. Instead, you should use the standard `react_component` view helper passing in a "Render-Function."

> [!IMPORTANT]
>
> **Script Loading Requirement:** If you use Redux shared stores with inline component registration (registering components in view templates with `<script>ReactOnRails.register({ MyComponent })</script>`), you **must use `defer: true`** in your `javascript_pack_tag` instead of `async: true`. With async loading, the bundle may execute before inline scripts, causing component registration failures. See the [Streaming Server Rendering documentation](../building-features/streaming-server-rendering.md#important-redux-shared-store-caveat) for details and alternatives.

You don't need to use the `redux_store` api to use Redux. This API was set up to support multiple calls to `react_component` on one page that all talk to the same Redux store.

If you are only rendering one React component on a page, as is typical to do a "Single Page App" in React, then you should _probably_ pass the props to your React component in a "Render-Function."
Expand Down
126 changes: 126 additions & 0 deletions docs/building-features/streaming-server-rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,129 @@ Streaming SSR is particularly valuable in specific scenarios. Here's when to con
- Prioritize critical data that should be included in the initial HTML
- Use streaming for supplementary data that can load progressively
- Consider implementing a waterfall strategy for dependent data

### Script Loading Strategy for Streaming

**IMPORTANT**: When using streaming server rendering, you should NOT use `defer: true` for your JavaScript pack tags. Here's why:

#### Understanding the Problem with Defer

Deferred scripts (`defer: true`) only execute after the entire HTML document has finished parsing and streaming. This defeats the key benefit of React 18's Selective Hydration feature, which allows streamed components to hydrate as soon as they arrive—even while other parts of the page are still streaming.

**Example Problem:**

```erb
<!-- ❌ BAD: This delays hydration for ALL streamed components -->
<%= javascript_pack_tag('client-bundle', defer: true) %>
```

With `defer: true`, your streamed components will:

1. Arrive progressively in the HTML stream
2. Be visible to users immediately
3. But remain non-interactive until the ENTIRE page finishes streaming
4. Only then will they hydrate

#### Recommended Approaches

**For Pages WITH Streaming Components:**

```erb
<!-- ✅ GOOD: No defer - allows Selective Hydration to work -->
<%= javascript_pack_tag('client-bundle', 'data-turbo-track': 'reload', defer: false) %>

<!-- ✅ BEST: Use async for even faster hydration (requires Shakapacker ≥ 8.2.0) -->
<%= javascript_pack_tag('client-bundle', 'data-turbo-track': 'reload', async: true) %>
```

**For Pages WITHOUT Streaming Components:**

With Shakapacker ≥ 8.2.0, `async: true` is recommended even for non-streaming pages to improve Time to Interactive (TTI):

```erb
<!-- ✅ RECOMMENDED: Use async with immediate_hydration for optimal performance -->
<%= javascript_pack_tag('client-bundle', 'data-turbo-track': 'reload', async: true) %>
```

Note: `async: true` with the `immediate_hydration` feature allows components to hydrate during page load, improving TTI even without streaming. See the Immediate Hydration section below for configuration details.

**⚠️ Important: Redux Shared Store Caveat**

If you are using Redux shared stores with the `redux_store` helper and **inline script registration** (registering components in view templates with `<script>ReactOnRails.register({ MyComponent })</script>`), you must use `defer: true` instead of `async: true`:

```erb
<!-- ⚠️ REQUIRED for Redux shared stores with inline registration -->
<%= javascript_pack_tag('client-bundle', 'data-turbo-track': 'reload', defer: true) %>
```

**Why?** With `async: true`, the bundle executes immediately upon download, potentially **before** inline `<script>` tags in the HTML execute. This causes component registration failures when React on Rails tries to hydrate the component.

**Solutions:**

1. **Use `defer: true`** - Ensures proper execution order (inline scripts run before bundle)
2. **Move registration to bundle** - Register components in your JavaScript bundle instead of inline scripts (recommended)
3. **Use React on Rails Pro** - Pro's `getOrWaitForStore` and `getOrWaitForStoreGenerator` can handle async loading with inline registration

See the [Redux Store API documentation](../api-reference/redux-store-api.md) for more details on Redux shared stores.

#### Why Async is Better Than No Defer

With Shakapacker ≥ 8.2.0, using `async: true` provides the best performance:

- **No defer/async**: Scripts block HTML parsing and streaming
- **defer: true**: Scripts wait for complete page load (defeats Selective Hydration)
- **async: true**: Scripts load in parallel and execute ASAP, enabling:
- Selective Hydration to work immediately
- Components to become interactive as they stream in
- Optimal Time to Interactive (TTI)

#### Migration Timeline

1. **Before Shakapacker 8.2.0**: Use `defer: false` for streaming pages
2. **Shakapacker ≥ 8.2.0**: Migrate to `async: true` for all pages (streaming and non-streaming)
3. **Enable `immediate_hydration`**: Configure for optimal Time to Interactive (see section below)

#### Configuring Immediate Hydration

React on Rails Pro supports the `immediate_hydration` feature, which allows components to hydrate during the page loading state (before DOMContentLoaded). This works optimally with `async: true` scripts:

```ruby
# config/initializers/react_on_rails.rb
ReactOnRails.configure do |config|
config.immediate_hydration = true # Enable early hydration

# Optional: Configure pack loading strategy globally
config.generated_component_packs_loading_strategy = :async
end
```

**Benefits of `immediate_hydration` with `async: true`:**

- Components become interactive as soon as their JavaScript loads
- No need to wait for DOMContentLoaded or full page load
- Optimal Time to Interactive (TTI) for both streaming and non-streaming pages
- Works seamlessly with React 18's Selective Hydration

**Note:** The `immediate_hydration` feature requires a React on Rails Pro license.

**Component-Level Control:**

You can also enable immediate hydration on a per-component basis:

```erb
<%= react_component('MyComponent', props: {}, immediate_hydration: true) %>
```

**generated_component_packs_loading_strategy Option:**

This configuration option sets the default loading strategy for auto-generated component packs:

- `:async` (recommended for Shakapacker ≥ 8.2.0) - Scripts load asynchronously
- `:defer` - Scripts defer until page load completes
- `:sync` - Scripts load synchronously (blocks page rendering)

```ruby
ReactOnRails.configure do |config|
config.generated_component_packs_loading_strategy = :async
end
```
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@
media: 'all',
'data-turbo-track': 'reload') %>

<%# Used for testing purposes to simulate hydration failure %>
<%# async: true is the recommended approach for Shakapacker >= 8.2.0 (currently using 9.3.0).
It enables React 18's Selective Hydration and provides optimal Time to Interactive (TTI).
Use immediate_hydration feature to control hydration timing for Selective/Immediate Hydration.
See docs/building-features/streaming-server-rendering.md
skip_js_packs param is used for testing purposes to simulate hydration failure %>
<% unless params[:skip_js_packs] == 'true' %>
<%= javascript_pack_tag('client-bundle', 'data-turbo-track': 'reload', defer: false) %>
<%= javascript_pack_tag('client-bundle', 'data-turbo-track': 'reload', async: true) %>
<% end %>
<%= csrf_meta_tags %>
</head>
Expand Down
19 changes: 19 additions & 0 deletions spec/dummy/app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,23 @@ class ApplicationController < ActionController::Base
redirect_to server_side_log_throw_raise_invoker_path,
flash: { error: msg }
end

helper_method :uses_redux_shared_store?

# Returns true if the current page uses Redux shared stores with inline registration
# These pages require defer: true instead of async: true for proper script execution order
def uses_redux_shared_store?
# Pages that use redux_store helper with inline component registration
action_name.in?(%w[
index
server_side_redux_app
server_side_redux_app_cached
server_side_hello_world_shared_store
server_side_hello_world_shared_store_defer
server_side_hello_world_shared_store_controller
client_side_hello_world_shared_store
client_side_hello_world_shared_store_defer
client_side_hello_world_shared_store_controller
])
end
end
12 changes: 10 additions & 2 deletions spec/dummy/app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,16 @@

<%= yield :head %>

<!-- NOTE: Must use defer and not async to keep async scripts loading in correct order -->
<%= javascript_pack_tag('client-bundle', 'data-turbo-track': 'reload', defer: true) %>
<%# Conditionally use defer: true for pages with Redux shared stores (inline registration).
Modern apps should use async: true for optimal performance. See docs for details:
docs/building-features/streaming-server-rendering.md %>
<% if uses_redux_shared_store? %>
<%# defer: true required for Redux shared stores with inline component registration %>
<%= javascript_pack_tag('client-bundle', 'data-turbo-track': 'reload', defer: true) %>
<% else %>
<%# async: true is the recommended approach for modern apps (Shakapacker >= 8.2.0) %>
<%= javascript_pack_tag('client-bundle', 'data-turbo-track': 'reload', async: true) %>
<% end %>

<%= csrf_meta_tags %>
</head>
Expand Down
2 changes: 1 addition & 1 deletion spec/dummy/config/initializers/react_on_rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@ def self.adjust_props_for_client_side_hydration(component_name, props)
config.components_subdirectory = "startup"
config.auto_load_bundle = true
config.immediate_hydration = false
config.generated_component_packs_loading_strategy = :defer
config.generated_component_packs_loading_strategy = :async
end
Loading