Skip to content

refactor: add RenderRequest, JsCodeBuilder, and RenderingStrategy abstractions#3094

Merged
justin808 merged 7 commits intomainfrom
jg/2905-strategy-pattern
Apr 13, 2026
Merged

refactor: add RenderRequest, JsCodeBuilder, and RenderingStrategy abstractions#3094
justin808 merged 7 commits intomainfrom
jg/2905-strategy-pattern

Conversation

@justin808
Copy link
Copy Markdown
Member

@justin808 justin808 commented Apr 9, 2026

Summary

Implements Steps 1+2 of #2905 — introduces three new abstractions to replace runtime react_on_rails_pro? checks in the rendering pipeline:

  • RenderRequest — structured data object encapsulating everything needed for a server render (component name, props, rails context, store initializations, render options), replacing ad-hoc parameter passing of js_code strings and render_options through the delegation chain
  • JsCodeBuilder — structured builder with overridable section methods for JS code generation, replacing the two divergent heredoc generators (core and Pro). Pro inherits and overrides only the sections it needs (RSC manifest, pre-hooks, dynamic render function name)
  • RenderingStrategy — strategy interface with ExecJsStrategy (core) and NodeStrategy (Pro) concrete implementations, replacing the conditional dispatch in ServerRenderingPool and ServerRenderingJsCode

All new code is additive — the main rendering path in helper.rb still uses the existing ServerRenderingPool and ServerRenderingJsCode classes. The switchover will happen in a follow-up PR (Step 3 of the issue).

Configuration wiring

  • ReactOnRails.rendering_strategy and ReactOnRails.js_code_builder module-level accessors, set by engine initializers at boot time
  • Core engine sets ExecJsStrategy as default
  • Pro engine overrides with NodeStrategy and Pro JsCodeBuilder

New files

File Purpose
react_on_rails/lib/react_on_rails/render_request.rb RenderRequest data object
react_on_rails/lib/react_on_rails/js_code_builder.rb Base JS code builder
react_on_rails/lib/react_on_rails/rendering_strategy.rb Strategy interface module
react_on_rails/lib/react_on_rails/rendering_strategy/exec_js_strategy.rb ExecJS strategy
react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb Pro JS code builder (RSC, pre-hooks)
react_on_rails_pro/lib/react_on_rails_pro/rendering_strategy/node_strategy.rb Pro strategy (wraps ProRendering)

Closes #2905 (partially — Steps 1+2 of 4)

Test plan

  • New unit tests for RenderRequest, JsCodeBuilder, and ExecJsStrategy (23 specs, all passing)
  • RuboCop passes on all 14 changed files
  • Pre-commit and pre-push hooks pass
  • CI passes (existing tests unaffected since changes are additive)

🤖 Generated with Claude Code


Note

Low Risk
Mostly additive abstractions with unit tests; existing SSR rendering path is unchanged, so behavior impact is low aside from new boot-time configurable globals.

Overview
Introduces new SSR abstractions: RenderRequest to encapsulate render inputs, JsCodeBuilder to generate SSR JavaScript via overridable sections, and a RenderingStrategy interface with a core ExecJsStrategy implementation.

Adds boot-time configuration hooks (ReactOnRails.rendering_strategy and ReactOnRails.js_code_builder) and a Pro engine initializer that overrides these with Pro’s NodeStrategy and extended JsCodeBuilder (adds RSC/streaming/pre-hook sections). Updates Steep/RBS signatures and adds unit specs covering the new objects and strategies.

Reviewed by Cursor Bugbot for commit ea1e3ea. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features
    • Configurable rendering strategy and pluggable JS code builder for server-side rendering.
    • Pro: Node-based rendering option with React Server Components (RSC) support and streaming payload handling.
    • Safer server-render payloads: props and Rails context are JSON-serialized and sanitized for embedding.
  • Tests
    • Added tests covering JS generation, render request behavior, and rendering strategies.

…tractions (#2905)

Introduce three new abstractions to replace runtime react_on_rails_pro?
checks in the rendering pipeline (Step 1+2 of issue #2905):

- RenderRequest: structured data object encapsulating everything needed
  for a server render, replacing ad-hoc parameter passing
- JsCodeBuilder: structured builder with overridable section methods,
  replacing heredoc-based JS code generation
- RenderingStrategy: strategy interface with ExecJsStrategy (core) and
  NodeStrategy (Pro) implementations, replacing conditional dispatch

All new code is additive — the main rendering path in helper.rb still
uses the existing ServerRenderingPool and ServerRenderingJsCode classes.
The switchover to the new strategy pattern will happen in a follow-up PR.

Configuration wiring:
- ReactOnRails.rendering_strategy and ReactOnRails.js_code_builder
  accessors added, set by engine initializers at boot time
- Core engine sets ExecJsStrategy as default
- Pro engine overrides with NodeStrategy and Pro JsCodeBuilder

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds three new abstractions—RenderRequest, RenderingStrategy (with ExecJsStrategy), and JsCodeBuilder—plus Pro-specific subclasses and engine wiring; exposes module-level accessors for strategy and builder. Changes are additive; core rendering path is not yet swapped.

Changes

Cohort / File(s) Summary
Module Loading
react_on_rails/lib/react_on_rails.rb, react_on_rails_pro/lib/react_on_rails_pro.rb
Require new core and Pro modules so builders and strategies load at boot.
Configuration Accessors
react_on_rails/lib/react_on_rails/configuration.rb, react_on_rails/sig/react_on_rails.rbs
Add ReactOnRails.rendering_strategy and ReactOnRails.js_code_builder getters/setters with memoized defaults; update RBS signatures.
Render Request
react_on_rails/lib/react_on_rails/render_request.rb, react_on_rails/sig/react_on_rails/render_request.rbs
New ReactOnRails::RenderRequest encapsulates render params, validates server bundle presence for prerender, provides to_js, props_string, and rails_context_json.
JS Code Builder (Core)
react_on_rails/lib/react_on_rails/js_code_builder.rb, react_on_rails/sig/react_on_rails/js_code_builder.rbs
New JsCodeBuilder composes overridable JS sections into an IIFE via build(render_request); protected section hooks for subclasses.
Rendering Strategy Interface & ExecJS
react_on_rails/lib/react_on_rails/rendering_strategy.rb, react_on_rails/lib/react_on_rails/rendering_strategy/exec_js_strategy.rb, react_on_rails/sig/react_on_rails/rendering_strategy.rbs, react_on_rails/sig/react_on_rails/rendering_strategy/exec_js_strategy.rbs
Add RenderingStrategy interface (methods: execute, reset, reset_if_bundle_changed) and ExecJsStrategy adapter delegating to existing RubyEmbeddedJavaScript pool.
Pro: Builder, Strategy & Engine Wiring
react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb, react_on_rails_pro/lib/react_on_rails_pro/rendering_strategy/node_strategy.rb, react_on_rails_pro/lib/react_on_rails_pro/engine.rb
Pro subclass JsCodeBuilder adds RSC/streaming-specific sections and pre-hook injection; NodeStrategy delegates to Pro rendering pool; engine initializer assigns Pro strategy and builder.
Tests
react_on_rails/spec/react_on_rails/js_code_builder_spec.rb, react_on_rails/spec/react_on_rails/render_request_spec.rb, react_on_rails/spec/react_on_rails/rendering_strategy/exec_js_strategy_spec.rb
New RSpec coverage for builder output structure, RenderRequest serialization/validation/escaping, and ExecJsStrategy delegation.
Type config
react_on_rails/Steepfile, react_on_rails/sig/.../*.rbs
Include new source files in Steep checks and add RBS definitions for new classes/modules.
Pro module load additions
react_on_rails_pro/lib/react_on_rails_pro.rb
Require Pro-specific js_code_builder and rendering_strategy/node_strategy.

Sequence Diagram(s)

sequenceDiagram
  participant Helper as Helper (caller)
  participant RenderReq as RenderRequest
  participant Builder as JsCodeBuilder
  participant Strategy as RenderingStrategy
  participant Pool as ServerRenderingPool

  Helper->>+RenderReq: build render_request(...)
  RenderReq-->>-Helper: return RenderRequest
  Helper->>+Strategy: reset_if_bundle_changed()
  Strategy-->>-Helper: (maybe resets pool)
  Helper->>+Strategy: execute(render_request)
  Strategy->>+RenderReq: render_request.to_js
  RenderReq-->>-Strategy: JS string
  Strategy->>+Pool: exec_server_render_js(js, render_options)
  Pool-->>-Strategy: rendered result
  Strategy-->>-Helper: result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I stitched requests into tidy stacks,

Sections hum and strategies track,
Props escape, JS wraps tight,
Exec or Node — ready to write.
Hooray! — a rabbit’s little delight 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main architectural refactoring: adding three new abstractions (RenderRequest, JsCodeBuilder, RenderingStrategy) to improve the rendering pipeline structure.
Linked Issues check ✅ Passed The PR successfully implements Steps 1 and 2 of issue #2905: adds RenderRequest class with required methods, JsCodeBuilder base class with overridable sections, RenderingStrategy interface with ExecJsStrategy and Pro NodeStrategy implementations, and configuration wiring via ReactOnRails.rendering_strategy and ReactOnRails.js_code_builder accessors.
Out of Scope Changes check ✅ Passed All changes are scoped to the Step 1-2 objectives: new abstractions for RenderRequest, JsCodeBuilder, RenderingStrategy, their configuration, type signatures, and corresponding unit tests. No existing rendering logic, helper.rb, or pool dispatch code is modified, maintaining the additive nature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jg/2905-strategy-pattern

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread react_on_rails/lib/react_on_rails/configuration.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/render_request.rb
Comment thread react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 9, 2026

Review: Strategy Pattern Refactoring (Steps 1+2)

Overall this is a clean, well-scoped additive PR. The three abstractions map clearly to their purpose, the existing rendering path is untouched, and the 23 new specs are well-structured. A few things worth addressing before Step 3 wires this into production:

Bugs / correctness

  1. rendering_strategy can return nil — unlike js_code_builder, which uses ||= to lazily construct a default, rendering_strategy returns @rendering_strategy directly. Any call before the engine initializer fires (e.g. unit tests that don't load the full Rails engine) produces NoMethodError: undefined method 'execute' for nil. See inline comment on configuration.rb:33.

  2. validate_server_bundle_configured! uses strict == true — truthy but non-boolean prerender values silently skip validation and allow a render to proceed without a server bundle, leading to a confusing downstream error. See inline on render_request.rb:61.

Design / maintainability

  1. Implicit coupling between render_call_section and wrap_in_iife in Pro buildername: componentName and props: usedProps reference bare JavaScript identifiers that only exist because wrap_in_iife wraps the body in a specific function signature. This coupling is invisible at the Ruby level; renaming either side silently breaks the generated JS. A comment documenting the contract would help. See inline on react_on_rails_pro/js_code_builder.rb:83.

  2. RenderingStrategy module interface stubs and :nocov: — the pattern is idiomatic Ruby, but be aware that include makes these stubs into real instance methods that are silently shadowed when a subclass defines the same method. If a subclass accidentally omits one, the NotImplementedError only appears at runtime, not load time. This is fine as-is; just worth documenting for future implementors.

Minor observations

  • The renderingRequest JavaScript variable in rsc_payload_function_section is a node-renderer global — confirmed by cross-referencing the original ServerRenderingJsCode.generate_rsc_payload_js_function. The new builder faithfully preserves the original behaviour here.
  • Engine initializer ordering relies on Pro loading after core (currently guaranteed by gem dependency order), which the ||= guard in the core initializer already handles correctly.
  • Tests are thorough for the new surface; the only gap is a unit test confirming rendering_strategy returns a usable default when no engine has run (which would also catch the nil regression).

🤖 Generated with Claude Code

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 9, 2026

Greptile Summary

This PR introduces three new additive abstractions — RenderRequest, JsCodeBuilder, and RenderingStrategy — as the first two steps of a larger refactoring (#2905) to replace runtime react_on_rails_pro? checks with boot-time strategy objects. All new code is additive and correctly not yet wired into the main rendering path in helper.rb. The new Pro JsCodeBuilder is a faithful port of ServerRenderingJsCode, preserving the same IIFE signature and renderingRequest outer-scope reference used by the Node renderer.

Confidence Score: 5/5

Safe to merge — all changes are additive, existing rendering path is untouched, and new code has 23 passing specs.

All findings are P2 style/consistency suggestions (lazy-init inconsistency between the two accessors, module-level methods placed in configuration.rb, misleading method name in Pro builder). No P0/P1 defects were identified. The new code faithfully ports the existing JS generation logic and the engine initializer ordering is correct.

react_on_rails/lib/react_on_rails/configuration.rb — the rendering_strategy getter lacks a lazy default that js_code_builder has, which will need addressing before Step 3 wires the strategy into the live path.

Vulnerabilities

No security concerns identified. The new code builds JS strings for SSR in the same manner as the existing ServerRenderingJsCode classes, and props/component names are serialized via .to_json which handles escaping. The unicode line/paragraph separator escaping in props_string is preserved correctly.

Important Files Changed

Filename Overview
react_on_rails/lib/react_on_rails/configuration.rb Adds rendering_strategy and js_code_builder module-level accessors; js_code_builder has a lazy default but rendering_strategy does not, creating an inconsistency that may cause nil errors when the strategy is wired in.
react_on_rails/lib/react_on_rails/render_request.rb New data object encapsulating SSR parameters; clean keyword-arg constructor, correct unicode escaping in props_string, and server bundle validation guard.
react_on_rails/lib/react_on_rails/js_code_builder.rb New base JS builder with overridable section methods; correctly generates an IIFE with railsContext, store inits, props, and the render call.
react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb Faithful port of ServerRenderingJsCode with RSC support, pre-hooks, and parameterized IIFE; resolve_render_function_name returns a JS expression not just a name, making the method name misleading.
react_on_rails/lib/react_on_rails/rendering_strategy/exec_js_strategy.rb Thin wrapper around RubyEmbeddedJavaScript; correctly delegates execute/reset/reset_if_bundle_changed to existing pool class.
react_on_rails_pro/lib/react_on_rails_pro/rendering_strategy/node_strategy.rb Thin wrapper around ProRendering; correctly delegates all three strategy methods and properly includes the RenderingStrategy interface module.
react_on_rails/lib/react_on_rails/engine.rb Adds after_initialize block using
react_on_rails_pro/lib/react_on_rails_pro/engine.rb Adds after_initialize block that overrides core's default rendering strategy with NodeStrategy and Pro JsCodeBuilder; ordering relative to core engine is correct since Pro loads after core.

Reviews (1): Last reviewed commit: "refactor: add RenderRequest, JsCodeBuild..." | Re-trigger Greptile

Comment thread react_on_rails/lib/react_on_rails/configuration.rb
Comment thread react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/configuration.rb
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb (1)

58-62: Clarify the regex replacement intent with a comment.

The regex /\\(\\s*\\)\\s*$/ that replaces trailing empty parentheses with component name and props is non-obvious. A brief inline comment explaining this transformation would help future maintainers understand the RSC request rewriting mechanism.

📝 Suggested comment
         if (typeof generateRSCPayload !== 'function') {
           globalThis.generateRSCPayload = function generateRSCPayload(componentName, props, railsContext) {
             const { renderingRequest, rscBundleHash } = railsContext.serverSideRSCPayloadParameters;
             const propsString = JSON.stringify(props);
+            // Replace trailing empty () in the IIFE with the RSC component name and props
             const newRenderingRequest = renderingRequest.replace(
               /\\(\\s*\\)\\s*$/,
               function() { return `(${JSON.stringify(componentName)}, ${propsString})`; }
             );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb` around lines 58
- 62, Add an inline comment above the regex replacement in the renderingRequest
-> newRenderingRequest transformation to explain that the pattern
/\\(\\s*\\)\\s*$/ matches and replaces trailing empty call parentheses (e.g.
"foo()") so the code can inject the RSC component name and serialized props;
reference the variables involved (renderingRequest, newRenderingRequest,
componentName, propsString, rscBundleHash, runOnOtherBundle) and briefly state
that this rewrites an empty-call RSC request into a call with the component and
props for runOnOtherBundle.
react_on_rails/lib/react_on_rails/render_request.rb (1)

41-43: Consider unicode escaping for rails_context_json as well.

props_string escapes \u2028/\u2029 for JavaScript safety, but rails_context_json does not. If rails_context can contain user-provided data with these characters, the same escaping should apply to avoid JavaScript parsing issues when embedded in a <script> tag or evaluated via ExecJS.

♻️ Proposed fix for consistency
     def rails_context_json
-      rails_context.to_json
+      rails_context.to_json.gsub("\u2028", '\u2028').gsub("\u2029", '\u2029')
     end

Alternatively, extract a shared helper method for JSON-with-unicode-escaping to DRY up both methods.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@react_on_rails/lib/react_on_rails/render_request.rb` around lines 41 - 43,
rails_context_json currently returns rails_context.to_json without escaping
U+2028/U+2029, which can break JS when embedding in <script> or Eval; update
rails_context_json to apply the same unicode-escape logic used by props_string
(escape \u2028 and \u2029 after to_json) or extract a shared helper (e.g.,
json_with_js_safe_unicode) and use it from both props_string and
rails_context_json so both outputs consistently replace those characters with
their \u escapes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb`:
- Around line 58-62: Add an inline comment above the regex replacement in the
renderingRequest -> newRenderingRequest transformation to explain that the
pattern /\\(\\s*\\)\\s*$/ matches and replaces trailing empty call parentheses
(e.g. "foo()") so the code can inject the RSC component name and serialized
props; reference the variables involved (renderingRequest, newRenderingRequest,
componentName, propsString, rscBundleHash, runOnOtherBundle) and briefly state
that this rewrites an empty-call RSC request into a call with the component and
props for runOnOtherBundle.

In `@react_on_rails/lib/react_on_rails/render_request.rb`:
- Around line 41-43: rails_context_json currently returns rails_context.to_json
without escaping U+2028/U+2029, which can break JS when embedding in <script> or
Eval; update rails_context_json to apply the same unicode-escape logic used by
props_string (escape \u2028 and \u2029 after to_json) or extract a shared helper
(e.g., json_with_js_safe_unicode) and use it from both props_string and
rails_context_json so both outputs consistently replace those characters with
their \u escapes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b665d2e2-fc5b-44ca-8d4d-a3f0e802138a

📥 Commits

Reviewing files that changed from the base of the PR and between 16ac897 and 57990d1.

📒 Files selected for processing (14)
  • react_on_rails/lib/react_on_rails.rb
  • react_on_rails/lib/react_on_rails/configuration.rb
  • react_on_rails/lib/react_on_rails/engine.rb
  • react_on_rails/lib/react_on_rails/js_code_builder.rb
  • react_on_rails/lib/react_on_rails/render_request.rb
  • react_on_rails/lib/react_on_rails/rendering_strategy.rb
  • react_on_rails/lib/react_on_rails/rendering_strategy/exec_js_strategy.rb
  • react_on_rails/spec/react_on_rails/js_code_builder_spec.rb
  • react_on_rails/spec/react_on_rails/render_request_spec.rb
  • react_on_rails/spec/react_on_rails/rendering_strategy/exec_js_strategy_spec.rb
  • react_on_rails_pro/lib/react_on_rails_pro.rb
  • react_on_rails_pro/lib/react_on_rails_pro/engine.rb
  • react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb
  • react_on_rails_pro/lib/react_on_rails_pro/rendering_strategy/node_strategy.rb

… pattern

Give `rendering_strategy` the same `||=` lazy-initialization that
`js_code_builder` already uses, so any code path that calls
`ReactOnRails.rendering_strategy` before the engine initializer fires
gets a usable `ExecJsStrategy` instance instead of nil.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@justin808
Copy link
Copy Markdown
Member Author

Review Comment Summary

Addressed (1):

  • configuration.rb:33 — Added lazy default for rendering_strategy to match js_code_builder pattern (01579a2). Threads resolved.

Declined with rationale (4):

  • render_request.rb:61prerender == true strict check intentionally mirrors existing ServerRenderingJsCode pattern
  • pro/js_code_builder.rb:83 — IIFE parameter coupling comment is a documentation nit, out of scope
  • pro/js_code_builder.rb:109 — Method rename is a style preference, out of scope for additive PR
  • configuration.rb:50 — Accessor placement is a code organization preference, out of scope

Duplicates (1):

  • greptile lazy-init comment — duplicate of claude[bot] comment, addressed together

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
react_on_rails/lib/react_on_rails/configuration.rb (1)

36-38: Consider fail-fast contract checks in setters.

Both setters currently accept any object. A small interface guard would catch misconfiguration earlier.

Suggested hardening
 def self.rendering_strategy=(strategy)
+  if strategy && !%i[execute reset reset_if_bundle_changed].all? { |method_name| strategy.respond_to?(method_name) }
+    raise ArgumentError, "rendering_strategy must respond to :execute, :reset, and :reset_if_bundle_changed"
+  end
   `@rendering_strategy` = strategy
 end

 def self.js_code_builder=(builder)
+  if builder && !builder.respond_to?(:build)
+    raise ArgumentError, "js_code_builder must respond to :build"
+  end
   `@js_code_builder` = builder
 end

Also applies to: 47-49

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@react_on_rails/lib/react_on_rails/configuration.rb` around lines 36 - 38, The
setter rendering_strategy= currently accepts any object; add a fail-fast
contract check that validates the provided strategy implements the required
interface (for example, respond_to?(:render) or respond_to?(:call) depending on
your expected API) and raise ArgumentError with a clear message if it does not;
apply the same pattern to the other setter methods in this file so
misconfiguration is caught early.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@react_on_rails/lib/react_on_rails/configuration.rb`:
- Around line 36-38: The setter rendering_strategy= currently accepts any
object; add a fail-fast contract check that validates the provided strategy
implements the required interface (for example, respond_to?(:render) or
respond_to?(:call) depending on your expected API) and raise ArgumentError with
a clear message if it does not; apply the same pattern to the other setter
methods in this file so misconfiguration is caught early.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7a3b7f62-ec48-4bbe-8071-8746069fda87

📥 Commits

Reviewing files that changed from the base of the PR and between 57990d1 and 01579a2.

📒 Files selected for processing (1)
  • react_on_rails/lib/react_on_rails/configuration.rb

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 12, 2026

Review: Step 1+2 Refactoring (RenderRequest, JsCodeBuilder, RenderingStrategy)

The overall structure is clean and the strategy pattern split makes sense. The code faithfully mirrors the existing Pro/core JS generation logic. A few things worth addressing before or during Step 3:


🔴 Step 3 footgun: rails_context type mismatch

RenderRequest documents and expects rails_context to be a Hash, and rails_context_json calls .to_json on it. However, the existing call site in helper.rb:766 already serializes it:

rails_context: rails_context(server_side: true).to_json,   # already a JSON string

When Step 3 wires RenderRequest into the helper, passing the current .to_json result would double-encode the context into a JSON-escaped string literal instead of an object. The call site will need to drop the .to_json. Suggest a # NOTE(Step 3) comment in rails_context_json — or even a runtime guard (raise if rails_context.is_a?(String)) to catch the mistake early.


🟡 Redundant lazy initialization for rendering_strategy

configuration.rb defines:

def self.rendering_strategy
  @rendering_strategy ||= ReactOnRails::RenderingStrategy::ExecJsStrategy.new
end

And engine.rb then adds:

ReactOnRails.rendering_strategy ||= ReactOnRails::RenderingStrategy::ExecJsStrategy.new

Because the getter always returns a non-nil value, the ||= in the initializer is always a no-op — the getter fires first, sets @rendering_strategy, and the RHS of ||= is never evaluated. The engine initializer creates a second ExecJsStrategy object on every boot that is immediately discarded. Pick one ownership model: either the getter provides the default or the initializer does, not both.


🟡 Asymmetry: js_code_builder has no engine initializer

rendering_strategy gets an explicit boot-time initializer; js_code_builder only has the getter's ||=. If someone clears @js_code_builder = nil in a test, the getter silently re-creates a core builder — but there's no Pro initializer that would re-set the Pro builder in that scenario (since Pro's engine only runs once at boot). The asymmetry isn't harmful today (since the code is additive), but it could cause surprising test pollution once Step 3 lands.


🟡 renderingRequest global assumption undocumented (Pro JsCodeBuilder)

In the non-RSC-payload-streaming branch of rsc_payload_function_section, the generated JS references renderingRequest as a free variable:

railsContext.serverSideRSCPayloadParameters = {
  renderingRequest,   // ← where does this come from?
  rscBundleHash: ...
}

This variable is injected by the Node renderer runtime (confirmed by the existing ServerRenderingJsCode which has the same pattern). The new code should carry a comment explaining the assumption, as it's non-obvious to readers unfamiliar with the Pro Node renderer protocol.


🟢 Minor: spec for to_js has an implicit global dependency

render_request_spec.rb tests #to_js by stubbing only ReactOnRails.configuration.server_bundle_js_file, but lets ReactOnRails.js_code_builder resolve to whatever the global @js_code_builder state happens to be. Stubbing the builder explicitly would make the test hermetic and the contract clearer.


Overall this is well-structured foundational work. The concerns above are mostly prep-notes for Step 3 rather than blockers — the code is correctly marked additive-only.

Comment thread react_on_rails/lib/react_on_rails/render_request.rb
Comment thread react_on_rails/lib/react_on_rails/configuration.rb
Comment thread react_on_rails/lib/react_on_rails/engine.rb Outdated
Comment thread react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb
Comment thread react_on_rails/spec/react_on_rails/render_request_spec.rb
Comment thread react_on_rails/lib/react_on_rails/engine.rb Outdated
Remove the rendering_strategy engine initializer that was dead code —
the getter already provides the same ||= lazy default, so the initializer's
||= never fired. Add an ArgumentError guard in rails_context_json to catch
accidental double-encoding when rails_context is passed as a String.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@justin808
Copy link
Copy Markdown
Member Author

Review comments addressed in c19a744

Fixed (2):

  • engine.rb:112 — Removed dead engine initializer for rendering_strategy. The getter's ||= at configuration.rb:32 is now the single canonical default. Pro's engine continues to override via the setter.
  • render_request.rb:41 — Added ArgumentError guard in rails_context_json to catch accidental double-encoding when rails_context is passed as a String instead of a Hash. Spec added.

Declined with rationale (4):

  • js_code_builder.rb:51 (Pro) — renderingRequest free variable comment: documentation nit, follows existing ServerRenderingJsCode pattern
  • render_request_spec.rb:121js_code_builder test isolation stub: speculative concern, no other spec modifies this global
  • render_request.rb:61== true strict check: matches existing ServerRenderingJsCode pattern at line 23
  • js_code_builder.rb:83 (Pro) — IIFE parameter coupling comment: follows existing pattern, clear from proximity

All 10 review threads resolved.

Comment thread react_on_rails/lib/react_on_rails/configuration.rb
Comment thread react_on_rails/lib/react_on_rails/render_request.rb
Comment thread react_on_rails/lib/react_on_rails/js_code_builder.rb
Comment thread react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb
Comment thread react_on_rails/spec/react_on_rails/render_request_spec.rb
Comment thread react_on_rails/lib/react_on_rails/rendering_strategy.rb
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 12, 2026

Review: RenderRequest, JsCodeBuilder, and RenderingStrategy abstractions

Good architectural direction — separating JS-code generation from execution, and replacing scattered react_on_rails_pro? checks with boot-time strategy injection, is exactly the right decomposition. The additive-only constraint makes this safe to land without touching the hot path.

A few issues worth addressing before the switchover in Step 3:

Substantive issues

1. Missing unicode escaping in rails_context_json (inline on render_request.rb)
props_string explicitly escapes U+2028/U+2029. rails_context_json does not, relying implicitly on ActiveSupport's encoder. Even if that encoder happens to escape them today, the asymmetry is confusing and fragile. Apply the same .gsub treatment.

2. renderingRequest in Pro builder is undocumented ambient state (inline on Pro js_code_builder.rb)
The non-payload RSC section emits renderingRequest as a shorthand property — but that variable is never declared in the generated IIFE. It only exists because the Node renderer injects it into the evaluation context. The original ServerRenderingJsCode has the same implicit contract, but at least there's a nearby comment explaining the pattern. The new builder needs an equivalent comment, otherwise every reader will think it's a bug.

3. Module-level accessors belong in react_on_rails.rb, not configuration.rb (inline on configuration.rb)
rendering_strategy and js_code_builder are module-level methods on ReactOnRails, not slots on the Configuration struct. Placing them in configuration.rb conflates two responsibilities. More practically: ReactOnRails.configure does not reset these ivars, so calling configure again in tests leaves stale strategies behind.

Minor issues

4. compact vs compact_blank in JsCodeBuilder#build
Empty-string sections (e.g. store_initializations = "") are not filtered by compact, producing blank lines in generated JS. compact_blank is already polyfilled in this codebase for Rails < 6.1.

5. Test isolation for RenderRequest#to_js (inline on spec)
The to_js specs exercise the real global ReactOnRails.js_code_builder without resetting it. Stub it explicitly so these specs remain correct when run in the Pro test suite or after other specs mutate the global.

6. RenderingStrategy interface contract (inline on module)
Minor doc nit: add a note that implementers must define instance methods (not class methods) to satisfy the include-based interface.


Overall this is a clean foundation for the Step 3 switchover. Issues 2 and 1 are the most important to address before wiring into the live rendering path.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb`:
- Around line 42-45: The long JavaScript error string inside the
generateRSCPayload function exceeds the Ruby 120-char limit; split the message
into shorter concatenated JS strings inside the heredoc so each Ruby source line
stays <=120 chars, e.g., break the single Error('...') argument into two or more
adjacent string segments joined with + (preserving the exact message and
punctuation) within the same heredoc block where globalThis.generateRSCPayload
is defined.
- Around line 40-64: The current code uses a typeof guard so the first request's
version of globalThis.generateRSCPayload persists across reused renderer
contexts, causing request-order dependent behavior; always overwrite the global
handler per request instead of only setting it when typeof generateRSCPayload
!== 'function' — i.e., remove the typeof checks and unconditionally assign
globalThis.generateRSCPayload in both branches (the throwing stub when
render_request.rsc_payload_streaming? and the dispatcher that uses
railsContext.serverSideRSCPayloadParameters, renderingRequest, rscBundleHash and
runOnOtherBundle in the other branch) so each request installs the correct
implementation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0337e380-f8ad-4c19-9984-60e60ebb9d99

📥 Commits

Reviewing files that changed from the base of the PR and between c19a744 and 30f5b9a.

📒 Files selected for processing (11)
  • react_on_rails/Steepfile
  • react_on_rails/lib/react_on_rails/configuration.rb
  • react_on_rails/lib/react_on_rails/render_request.rb
  • react_on_rails/lib/react_on_rails/rendering_strategy.rb
  • react_on_rails/sig/react_on_rails.rbs
  • react_on_rails/sig/react_on_rails/js_code_builder.rbs
  • react_on_rails/sig/react_on_rails/render_request.rbs
  • react_on_rails/sig/react_on_rails/rendering_strategy.rbs
  • react_on_rails/sig/react_on_rails/rendering_strategy/exec_js_strategy.rbs
  • react_on_rails/spec/react_on_rails/render_request_spec.rb
  • react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb
✅ Files skipped from review due to trivial changes (3)
  • react_on_rails/sig/react_on_rails/rendering_strategy.rbs
  • react_on_rails/lib/react_on_rails/rendering_strategy.rb
  • react_on_rails/spec/react_on_rails/render_request_spec.rb
🚧 Files skipped from review as they are similar to previous changes (2)
  • react_on_rails/lib/react_on_rails/configuration.rb
  • react_on_rails/lib/react_on_rails/render_request.rb

Comment thread react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb
Comment thread react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 12, 2026

Review: RenderRequest, JsCodeBuilder, and RenderingStrategy abstractions

Overall this is a clean, well-structured refactor. The additive approach is the right call — keeping the existing path intact while introducing the new abstractions side-by-side makes the transition safe. A few issues worth addressing before Step 3 wires these in:

Issues

1. RBS type mismatch: store_initializations is nullable

render_request.rbs declares attr_reader store_initializations: String but the field can be nil (the spec has a nil case and JsCodeBuilder#build_sections calls .compact specifically to handle it). Should be String?.

2. configure reset won't survive Pro test setups

When ReactOnRails.configure {} is called inside a test (a common pattern in this repo), @rendering_strategy and @js_code_builder are nilled. The lazy ||= fallback then returns a fresh ExecJsStrategy/JsCodeBuilder — not Pro's overrides — because after_initialize only fires once at boot and won't re-run. This is harmless now (additive only), but should be addressed before Step 3 or any Pro unit tests that call configure will silently use the wrong strategy. One approach: have the Pro engine expose a restore_rendering_strategy! class method callable from test helpers, or wire the accessors so configure invokes Pro's override if the Pro engine is loaded.

3. Minor thread-safety note on ||=

The ||= idiom on module instance variables (@rendering_strategy, @js_code_builder) is not atomic. In a Puma multi-threaded setup two threads could both see nil after a configure call and race to initialize the default. The worst outcome is creating two throwaway objects (last write wins, no data corruption), but it's worth noting since these are global mutable singletons.

4. Implicit renderingRequest dependency in Pro builder

In rsc_payload_function_section, the generated JS references renderingRequest — a variable that lives in the Node renderer's outer execution scope, not defined anywhere in the IIFE. This matches the existing ServerRenderingJsCode behavior exactly (so it's correct), but the implicit contract between the Ruby code generator and the Node runtime is undocumented. A short comment citing where renderingRequest comes from would prevent future confusion.

Minor

  • validate_server_bundle_configured! uses render_options.prerender == true (strict equality). Consistent with the existing ServerRenderingJsCode check, so not a regression.
  • props_string silently passes through a non-JSON string while rails_context_json raises on non-Hash. The asymmetry is fine for now but worth standardizing during Step 3.

What's good

  • Unicode escape handling (\u2028/\u2029) is correctly applied in both props_string and rails_context_json.
  • The section-based override design (build_sections) is significantly cleaner than duplicating heredocs; Pro only overrides what differs.
  • rails_context_json raising ArgumentError on non-Hash prevents double-encoding bugs — good defensive check.
  • 23 new specs with good coverage of edge cases (nil stores, unicode, Hash vs String props).

Comment thread react_on_rails/sig/react_on_rails/render_request.rbs Outdated
Comment thread react_on_rails/sig/react_on_rails/render_request.rbs Outdated
Comment thread react_on_rails/lib/react_on_rails/configuration.rb Outdated
Comment thread react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb
…igure reset

- Fix RBS type for store_initializations to String? (attr_reader and
  initialize) — the spec passes nil and JsCodeBuilder#build_sections
  calls .compact to filter it.
- Remove @rendering_strategy/@js_code_builder reset from configure —
  Pro only sets these once during after_initialize, so resetting them
  on reconfigure silently drops Pro behavior. Since the strategy path
  isn't wired in yet, the reset was premature; the proper reconfigure
  mechanism should be designed when Step 3 lands.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@justin808
Copy link
Copy Markdown
Member Author

Review comments addressed in 6a44410

Fixed (2):

  • render_request.rbs:6,13 — Fixed RBS type for store_initializations: StringString? in both attr_reader and initialize signature (@claude[bot])
  • configuration.rb:29-30 — Removed premature @rendering_strategy = nil / @js_code_builder = nil reset from configure — Pro only sets these once during after_initialize, so resetting them on reconfigure would silently drop Pro behavior. Since the strategy path is additive-only, the reset was unnecessary; proper reconfigure mechanism deferred to Step 3. (@chatgpt-codex-connector[bot], @claude[bot])

Declined with rationale (3):

  • js_code_builder.rb:40-64typeof guard on generateRSCPayload: faithful port of existing server_rendering_js_code.rb pattern; not a regression (@coderabbitai[bot])
  • js_code_builder.rb:42-45 — Line length in JS heredoc: 120-char limit applies to Ruby logic, not embedded JS strings (@coderabbitai[bot])
  • js_code_builder.rb:51renderingRequest implicit scope comment: same contract as legacy code; belongs in separate cleanup PR (@claude[bot])

All 7 review threads resolved.

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 13, 2026

Tip

For best results, initiate chat on the files or code changes.

@justin808 Thanks for the detailed update!

  • render_request.rbs store_initializations: String? — Correct fix; the type now matches the nullable return in both the attr_reader and initializer signature.
  • configure reset removal — Good call on reverting. Resetting @rendering_strategy/@js_code_builder inside configure would silently clobber the Pro engine's after_initialize assignment on any reconfigure, which is a real footgun for Pro users. Deferring the proper reconfigure mechanism to Step 3 is the right sequencing.

Both of my declined items (typeof guard and heredoc line length) are already captured in long-term memory from the prior round — no new entries needed for those.


✏️ Learnings added
Learnt from: justin808
URL: https://github.com/shakacode/react_on_rails/pull/3094

Timestamp: 2026-04-13T02:29:20.890Z
Learning: In `react_on_rails_pro/lib/react_on_rails_pro/engine.rb` and `react_on_rails/lib/react_on_rails/configuration.rb` (PR `#3094`): Do NOT reset `rendering_strategy` or `js_code_builder` inside `ReactOnRails.configure`. The Pro engine sets these exactly once during `after_initialize`; resetting them on reconfigure would silently drop Pro's NodeStrategy/ProJsCodeBuilder assignments. A proper reconfigure mechanism for these accessors is deferred to Step 3 of the strategy-pattern refactor (`#2905`).

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: justin808
Repo: shakacode/react_on_rails PR: 3094
File: react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb:40-64
Timestamp: 2026-04-13T02:28:34.069Z
Learning: In `react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb`, the `typeof generateRSCPayload !== 'function'` guards in `rsc_payload_function_section` are an intentional faithful port of the existing pattern in `server_rendering_js_code.rb` (lines 21 and 39). Do not flag this guard as a bug in this file; any fix should be tracked as a separate issue covering both files.

Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 2464
File: packages/react-on-rails-pro-node-renderer/src/worker.ts:343-393
Timestamp: 2026-02-21T20:46:38.404Z
Learning: In packages/react-on-rails-pro-node-renderer, the `Promise.all` pattern for per-bundle operations (e.g., in `/upload-assets` endpoint and `handleNewBundlesProvided` in handleRenderRequest.ts) is intentional. Copy operations like `copyUploadedAssets` are idempotent and retries are safe, so fast-fail behavior on lock acquisition or copy errors is acceptable. Switching to `Promise.allSettled` to surface partial success would require Ruby-side protocol changes.

Learnt from: ihabadham
Repo: shakacode/react_on_rails PR: 2674
File: react_on_rails/lib/react_on_rails/doctor.rb:2325-2345
Timestamp: 2026-03-19T16:13:07.375Z
Learning: In `react_on_rails/lib/react_on_rails/doctor.rb`, `check_rsc_payload_route` and `check_rsc_procfile_watcher` filter commented lines (`/^\s*#/`) before checking for the target string, consistent with the `config_has_async_loading_strategy?` pattern. The doctor avoids a full Rails boot for resilience and uses file-content scanning instead of actual Rails route inspection. Remaining accepted limitations are dynamic/engine-mounted routes and routes loaded via the `draw` macro (uncommon, since the generator places `rsc_payload_route` directly in `config/routes.rb`).

Learnt from: justin808
Repo: shakacode/react_on_rails PR: 3094
File: react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb:42-45
Timestamp: 2026-04-13T02:28:36.439Z
Learning: In `react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb` (and similar Ruby files in the react_on_rails / react_on_rails_pro codebases), the 120-character Ruby line-length limit applies only to Ruby logic lines, NOT to embedded JavaScript string content inside heredocs (`<<~JS`). Do not flag long JS string lines inside heredocs for exceeding the Ruby style limit, as splitting them would hurt readability for no functional benefit.

Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: react_on_rails_pro/CLAUDE.md:0-0
Timestamp: 2026-02-24T16:52:47.694Z
Learning: Applies to react_on_rails_pro/spec/dummy/commonWebpackConfig.js : Changes to `commonWebpackConfig.js` affect all three webpack bundles (client, server, and RSC) in the Pro dummy app

Learnt from: ihabadham
Repo: shakacode/react_on_rails PR: 2284
File: react_on_rails/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt:10-10
Timestamp: 2026-02-12T20:43:26.843Z
Learning: In React on Rails Pro initializer templates (e.g., `react_on_rails_pro.rb.tt`), the `config.renderer_password` uses `ENV.fetch("RENDERER_PASSWORD", "devPassword")` as a development starter configuration. This pattern is intentional: the node renderer requires explicit production deployment setup, and adding a production-only raise would break generator execution since initializers are loaded during `rails generate`. The pattern is consistent with existing manual setup documentation.

Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation in React on Rails Pro is handled through a chain of validations:
1. Pro version check in `run_stream_inside_fiber`
2. RSC support check during pack generation via `ReactOnRailsPro.configuration.enable_rsc_support`
3. RSC support validation during component registration
This makes additional validation in the helper methods unnecessary.

Learnt from: justin808
Repo: shakacode/react_on_rails PR: 2850
File: docs/oss/api-reference/rails-view-rendering-from-inline-javascript.md:0-0
Timestamp: 2026-03-26T07:01:43.653Z
Learning: In `packages/react-on-rails/src/ClientRenderer.ts`, the duplicate-render guard (which skips rendering if the DOM node is already connected/mounted) applies only to the automatic page-load rendering path. The public `ReactOnRails.render(...)` API routes through `packages/react-on-rails/src/base/client.ts` and calls `reactHydrateOrRender(...)` directly, without any deduplication guard. Therefore, repeated calls to the public `ReactOnRails.render(...)` API on the same DOM node (e.g., `#root`) WILL invoke React again — they are NOT deduplicated. Documentation stating this behavior is correct and should not be flagged.

Learnt from: justin808
Repo: shakacode/react_on_rails PR: 2497
File: react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb:0-0
Timestamp: 2026-04-09T06:56:01.856Z
Learning: In `react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb`, config overrides for the error scenarios hub are NOT applied by mutating the ReactOnRails/ReactOnRailsPro global config singletons. Instead, the private helper `error_hub_config_value(key, default)` reads overrides directly from `params` and falls back to the live singleton value — no global state is mutated during request handling. Do not flag the absence of an `around_action` restore pattern here, as the design intentionally avoids mutation altogether.

Learnt from: ihabadham
Repo: shakacode/react_on_rails PR: 2773
File: react_on_rails/lib/react_on_rails/doctor.rb:1262-1265
Timestamp: 2026-03-22T23:36:07.592Z
Learning: In React on Rails, the RSC generator always installs `stream_react_component` in `app/views/`. Therefore, the view-file scan in `uses_prerender_in_views?` (in `react_on_rails/lib/react_on_rails/doctor.rb`) reliably detects RSC-enabled apps via `stream_react_component`, and there is no need to additionally scan `config/routes.rb` for `rsc_payload_route` to cover the route-driven RSC entry point.

Learnt from: justin808
Repo: shakacode/react_on_rails PR: 2530
File: react_on_rails_pro/lib/react_on_rails_pro/concerns/stream.rb:0-0
Timestamp: 2026-03-04T23:37:22.169Z
Learning: In `react_on_rails_pro/lib/react_on_rails_pro/concerns/stream.rb`, the `stream_view_containing_react_components` method intentionally does NOT set `Cache-Control: no-transform`. The PR `#2530` is an alternative fix for the Live streaming + compression middleware deadlock (issue `#2519`): instead of using `no-transform` to make compression middleware short-circuit, gzip is handled in-process via a custom `GzipOutputStream` class wrapping `Zlib::GzipWriter`. This keeps compression enabled for clients that support gzip while bypassing the middleware stack entirely. Do not suggest adding `Cache-Control: no-transform` to this method.

Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-17T22:10:33.821Z
Learning: Applies to react_on_rails_pro/** : Ask before modifying the Pro package (`react_on_rails_pro/`)

Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: react_on_rails_pro/CLAUDE.md:0-0
Timestamp: 2026-02-24T16:52:47.694Z
Learning: Applies to react_on_rails_pro/spec/dummy/*WebpackConfig.js : The Pro dummy app has three separate webpack configs: `clientWebpackConfig.js`, `serverWebpackConfig.js`, and `rscWebpackConfig.js`

Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: react_on_rails_pro/CLAUDE.md:0-0
Timestamp: 2026-02-24T16:52:47.694Z
Learning: Applies to react_on_rails_pro/spec/dummy/spec/**/*.rb : Pro integration tests should be run with: `cd react_on_rails_pro/spec/dummy && bundle exec rspec spec/`

Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: react_on_rails_pro/CLAUDE.md:0-0
Timestamp: 2026-02-24T16:52:47.694Z
Learning: Applies to react_on_rails_pro/spec/react_on_rails_pro/**/*.rb : Pro gem unit tests should be run with: `cd react_on_rails_pro && bundle exec rspec spec/react_on_rails_pro/`

Comment thread react_on_rails/lib/react_on_rails/configuration.rb
Comment thread react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb
Comment thread react_on_rails/lib/react_on_rails/render_request.rb
Comment thread react_on_rails/lib/react_on_rails/rendering_strategy.rb
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON, but it could not run because on-demand usage is turned off. To enable Bugbot Autofix, turn on on-demand usage and set a spend limit in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6a44410. Configure here.

Comment thread react_on_rails/lib/react_on_rails/configuration.rb
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 13, 2026

Review: RenderRequest, JsCodeBuilder, and RenderingStrategy abstractions

The architecture is sound and the additive approach (zero impact on the live rendering path until Step 3) is the right way to land a refactor of this scope. The unit tests are thorough and the RBS signatures are a nice touch.

Issues

1. Module-level state in the wrong file (see inline on configuration.rb)
rendering_strategy and js_code_builder are boot-time module accessors, not Configuration class members. Placing them in configuration.rb is confusing, and the lazy ||= initializer has a latent thread-safety issue in parallel test runs.

2. Hidden cross-method invariant in ReactOnRailsPro::JsCodeBuilder (see inline)
Three overrides — props_section, render_call_section, and wrap_in_iife — form an undocumented contract: wrap_in_iife introduces componentName into JS scope, props_section emits usedProps instead of props, and render_call_section references both. Overriding any one without the others silently produces broken JS. This is especially risky given Pro is designed to be further subclassed (RSC vs. non-RSC rendering modes).

3. Duplicate validation in RenderRequest#validate_server_bundle_configured! (see inline)
Fine as temp duplication, but needs a TODO so it doesn't survive into Step 3.

4. NotImplementedError messages in RenderingStrategy (see inline)
Minor wording improvement to avoid confusion when a concrete subclass mis-implements the interface.

Observations

  • The renderingRequest bare variable reference in the RSC payload section of the Pro builder is intentional — the Node renderer injects it into the execution context. Worth a one-line comment since it looks like a bug on first read (the same implicit dependency exists in the old ServerRenderingJsCode, but it should be documented in the new code).
  • The Pro render_call_section uses ReactOnRails['serverRenderReactComponent']({...}) bracket notation in the non-RSC path. This is a carry-over from the existing ServerRenderingJsCode, so it's consistent, but dot notation (ReactOnRails.serverRenderReactComponent) would be cleaner in the non-dynamic branch.
  • The RBS for rendering_strategy returning RenderingStrategy (a module type) is correct — Steep handles module-as-type fine here.

Overall this is clean, well-tested code. The inline comments above are the main items I'd want addressed before Step 3 lands.

The base and Pro JsCodeBuilder have cross-method coupling where
props_section, render_call_section, and wrap_in_iife share JS variable
names. Document this invariant so future subclass authors know to keep
all three methods aligned when overriding any one of them.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@justin808
Copy link
Copy Markdown
Member Author

Review comments addressed in ea1e3ea

Fixed (1):

  • js_code_builder.rb:74-95 — Documented the JS variable contract between props_section, render_call_section, and wrap_in_iife in both base and Pro JsCodeBuilder classes (@claude[bot]). Threads resolved.

Declined with rationale (4):

Comment thread react_on_rails/lib/react_on_rails/render_request.rb
Comment thread react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb
Comment thread react_on_rails/sig/react_on_rails/js_code_builder.rbs
Comment thread react_on_rails/lib/react_on_rails/configuration.rb
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 13, 2026

Review: refactor — RenderRequest, JsCodeBuilder, RenderingStrategy abstractions

Overall the design is clean and the additive approach is the right call for this size of refactor. The three-abstraction split (data object / builder / strategy) mirrors the existing layering well, and the JS variable contract documentation in the builder is especially appreciated. A few things worth addressing before Step 3 wires this into the live path:


1. Pro-specific methods on core RenderRequest (important for Step 3)

RenderRequest#rsc_payload_streaming? and #streaming? are public methods on the core class, but rsc_payload_streaming? only makes sense for Pro — and ReactOnRails::ReactComponent::RenderOptions (the base class) doesn't implement it. When Step 3 connects this to the real rendering path, any code that calls render_request.rsc_payload_streaming? with a non-Pro render_options will raise NoMethodError.

The safest fix before Step 3: add def rsc_payload_streaming? = false to the base RenderOptions so it's always safe to call. See inline comment on render_request.rb:55.


2. No tests for ReactOnRailsPro::JsCodeBuilder or NodeStrategy

The PR adds 87 / 168 / 67 spec lines for the three core objects, but the Pro builder — which is the more complex of the two builders (RSC sections, pre-hook, IIFE-with-params, dynamic render function) — ships without a spec file. The Pro builder already has one non-obvious behavioral invariant (the IIFE default-parameter trick that lets the Node renderer re-invoke the serialized function with different component+props) that would benefit from regression coverage before Step 3.

Suggested specs:

  • rsc_payload_function_section output when rsc_payload_streaming? is true vs false
  • wrap_in_iife includes the default-parameter signature
  • props_section uses usedProps / typeof props guard
  • render_call_section picks the right RSC ternary for streaming vs non-streaming

3. Parity: base JsCodeBuilder vs existing ServerRenderingJsCode#render

Confirmed that the new builder's output matches the existing heredoc for both core and Pro. One minor point: the existing core ServerRenderingJsCode#render uses a <<-JS heredoc with no chomp, so the generated JS has a trailing newline; the new builder uses <<~JS.chomp throughout, producing tighter output. This is an improvement, just noting it in case any downstream consumer (Pro node renderer?) pattern-matches the raw JS string structure.


Minor / Non-blocking

  • ||= race on module-level accessors — harmless in practice since Pro's after_initialize sets these before requests arrive, but see inline note on configuration.rb:33.
  • RBS protected gapbuild_sections and the section helpers are protected in Ruby but listed as public in the RBS; Steep won't prevent external callers. See inline on js_code_builder.rbs:4.
  • renderingRequest JS scope — matches existing ServerRenderingJsCode behavior (Node renderer injects it); added a clarifying comment suggestion inline at pro/js_code_builder.rb:51.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb (1)

74-80: Document the IIFE-call-shape coupling too.

The contract here is broader than variable names: Line 58 rewrites the trailing () from wrap_in_iife. A later wrapper refactor can break payload dispatch without touching the RSC code path.

♻️ Suggested doc tweak
     # These three methods are coupled by JS variable names:
     # - wrap_in_iife introduces `componentName` and `props` as IIFE default parameters
+    # - rsc_payload_function_section rewrites the trailing empty IIFE invocation,
+    #   so wrap_in_iife must continue ending in a bare `()`
     # - props_section declares `usedProps` (falls back to render_request.props_string
     #   when `props` IIFE param is undefined, i.e. normal invocation)
     # - render_call_section references both `componentName` and `usedProps`

Also applies to: 58-60, 101-102

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb` around lines 74
- 80, The comment block documents JS variable coupling but omits the IIFE
call-shape coupling — note that wrap_in_iife rewrites the trailing () and other
wrappers must preserve that exact call shape; update the doc in
react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb near the existing
comment (mentioning build_sections, wrap_in_iife, props_section,
render_call_section) to explicitly state that wrap_in_iife removes or rewrites
the trailing invocation token, that props_section and render_call_section expect
that modified call shape (usedProps fallback behavior), and that any future
wrapper refactors must preserve this IIFE-call-shape to avoid breaking payload
dispatch (also add a short reference to the related spots around wrap_in_iife
and the lines noted).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb`:
- Around line 74-80: The comment block documents JS variable coupling but omits
the IIFE call-shape coupling — note that wrap_in_iife rewrites the trailing ()
and other wrappers must preserve that exact call shape; update the doc in
react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb near the existing
comment (mentioning build_sections, wrap_in_iife, props_section,
render_call_section) to explicitly state that wrap_in_iife removes or rewrites
the trailing invocation token, that props_section and render_call_section expect
that modified call shape (usedProps fallback behavior), and that any future
wrapper refactors must preserve this IIFE-call-shape to avoid breaking payload
dispatch (also add a short reference to the related spots around wrap_in_iife
and the lines noted).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5e81bf62-1b37-42d1-bdc9-6f2e71e8cae4

📥 Commits

Reviewing files that changed from the base of the PR and between 30f5b9a and ea1e3ea.

📒 Files selected for processing (4)
  • react_on_rails/lib/react_on_rails/configuration.rb
  • react_on_rails/lib/react_on_rails/js_code_builder.rb
  • react_on_rails/sig/react_on_rails/render_request.rbs
  • react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb
🚧 Files skipped from review as they are similar to previous changes (2)
  • react_on_rails/lib/react_on_rails/js_code_builder.rb
  • react_on_rails/sig/react_on_rails/render_request.rbs

@justin808
Copy link
Copy Markdown
Member Author

Review comments addressed — final round

All 4 remaining review threads from the latest review have been replied to and resolved. No code changes were needed.

Declined with rationale (4):

  • render_request.rb:55rsc_payload_streaming? NoMethodError claim is incorrect: base RenderOptions already defines the method at render_options.rb:166–169. The Pro call site is also guarded by rsc_streaming?.
  • pro/js_code_builder.rb:51renderingRequest JS scope comment: already documented in build_sections contract comments (ea1e3ea).
  • js_code_builder.rbs:4 — RBS protected gap: known RBS limitation, Ruby-level enforcement is intact.
  • configuration.rb:33||= thread safety: harmless in practice (stateless objects, Pro sets before first request).

All review threads on this PR are now resolved.

@justin808 justin808 merged commit 18c2f48 into main Apr 13, 2026
67 checks passed
@justin808 justin808 deleted the jg/2905-strategy-pattern branch April 13, 2026 06:36
justin808 added a commit that referenced this pull request Apr 18, 2026
…ages

* origin/main:
  Fix initial page startup race for late-loading client bundles (#3151)
  chore: apply prettier formatting to tracked docs files (#3153)
  docs: comprehensive RSC API documentation and registration consolidation (#3140)
  Split rspec-package-tests into parallel generator/unit shards (#3134)
  fix: add concurrency groups to long-running CI workflows (#3133)
  refactor: add RenderRequest, JsCodeBuilder, and RenderingStrategy abstractions (#3094)
  fix: address deferred review items from PR #2849 (#3093)
  Add complimentary OSS license policy for React on Rails Pro (#3123)
  fix: centralize CI docs-only detection and add CLI flag validation (#3091)
  refactor: replace stub-throw + Object.assign with capability-based composition (#3096)
  Enhance address-review with parallel fixes, self-review, and Greptile verification (#3121)
  fix: Doctor no longer fails custom projects for missing bin/dev (#3117)
  fix: cap webpack <5.106.0 to prevent ExecJS SSR breakage (#3095)
  Add Rspack + RSC compatibility tests and documentation (#1828) (#3120)
  Add error scenarios hub and test pages (#2497)
  docs: document polyfill requirements for web-targeted server bundles (#3092)
  docs: RSC integration pitfalls from tutorial app (#3087)
  docs: fix render function/helper API documentation (#3088)
  Doctor: accept TS/TSX server bundle suffixes (#3111)
  feat: add CI guard requiring sidebar updates when adding docs (#3089)
justin808 added a commit that referenced this pull request Apr 18, 2026
…tractions (#3094)

## Summary

Implements Steps 1+2 of #2905 — introduces three new abstractions to
replace runtime `react_on_rails_pro?` checks in the rendering pipeline:

- **`RenderRequest`** — structured data object encapsulating everything
needed for a server render (component name, props, rails context, store
initializations, render options), replacing ad-hoc parameter passing of
`js_code` strings and `render_options` through the delegation chain
- **`JsCodeBuilder`** — structured builder with overridable section
methods for JS code generation, replacing the two divergent heredoc
generators (core and Pro). Pro inherits and overrides only the sections
it needs (RSC manifest, pre-hooks, dynamic render function name)
- **`RenderingStrategy`** — strategy interface with `ExecJsStrategy`
(core) and `NodeStrategy` (Pro) concrete implementations, replacing the
conditional dispatch in `ServerRenderingPool` and
`ServerRenderingJsCode`

All new code is **additive** — the main rendering path in `helper.rb`
still uses the existing `ServerRenderingPool` and
`ServerRenderingJsCode` classes. The switchover will happen in a
follow-up PR (Step 3 of the issue).

### Configuration wiring

- `ReactOnRails.rendering_strategy` and `ReactOnRails.js_code_builder`
module-level accessors, set by engine initializers at boot time
- Core engine sets `ExecJsStrategy` as default
- Pro engine overrides with `NodeStrategy` and Pro `JsCodeBuilder`

### New files

| File | Purpose |
|------|---------|
| `react_on_rails/lib/react_on_rails/render_request.rb` | RenderRequest
data object |
| `react_on_rails/lib/react_on_rails/js_code_builder.rb` | Base JS code
builder |
| `react_on_rails/lib/react_on_rails/rendering_strategy.rb` | Strategy
interface module |
|
`react_on_rails/lib/react_on_rails/rendering_strategy/exec_js_strategy.rb`
| ExecJS strategy |
| `react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb` | Pro
JS code builder (RSC, pre-hooks) |
|
`react_on_rails_pro/lib/react_on_rails_pro/rendering_strategy/node_strategy.rb`
| Pro strategy (wraps ProRendering) |

Closes #2905 (partially — Steps 1+2 of 4)

## Test plan

- [x] New unit tests for `RenderRequest`, `JsCodeBuilder`, and
`ExecJsStrategy` (23 specs, all passing)
- [x] RuboCop passes on all 14 changed files
- [x] Pre-commit and pre-push hooks pass
- [ ] CI passes (existing tests unaffected since changes are additive)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Mostly additive abstractions with unit tests; existing SSR rendering
path is unchanged, so behavior impact is low aside from new boot-time
configurable globals.
> 
> **Overview**
> Introduces new SSR abstractions: `RenderRequest` to encapsulate render
inputs, `JsCodeBuilder` to generate SSR JavaScript via overridable
sections, and a `RenderingStrategy` interface with a core
`ExecJsStrategy` implementation.
> 
> Adds boot-time configuration hooks (`ReactOnRails.rendering_strategy`
and `ReactOnRails.js_code_builder`) and a Pro engine initializer that
overrides these with Pro’s `NodeStrategy` and extended `JsCodeBuilder`
(adds RSC/streaming/pre-hook sections). Updates Steep/RBS signatures and
adds unit specs covering the new objects and strategies.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
ea1e3ea. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Configurable rendering strategy and pluggable JS code builder for
server-side rendering.
* Pro: Node-based rendering option with React Server Components (RSC)
support and streaming payload handling.
* Safer server-render payloads: props and Rails context are
JSON-serialized and sanitized for embedding.
* **Tests**
* Added tests covering JS generation, render request behavior, and
rendering strategies.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: Replace runtime react_on_rails_pro? checks with strategy pattern + RenderRequest + JsCodeBuilder

1 participant