Skip to content

Conversation

@justin808
Copy link
Member

@justin808 justin808 commented Nov 11, 2025

Summary

This PR reverts PR #1972 and adds a comprehensive investigation document that analyzes the component registration race condition in depth.

What This PR Does

  1. Reverts PR Fix component registration race condition with defer loading strategy #1972 - Restores the previous behavior where:

    • generated_component_packs_loading_strategy defaults to :async (Shakapacker >= 8.2.0)
    • Layout uses conditional logic based on uses_redux_shared_store?
    • Test expectations match the original behavior
  2. Adds Investigation Document - PR_1972_INVESTIGATION.md provides:

Why Revert PR #1972?

PR #1972 "fixed" intermittent test failures by changing the default loading strategy from :async to :defer. While this prevents the race condition, it:

  1. Hurts Performance - Forces sequential execution instead of parallel
  2. Masks Root Cause - React hydration shouldn't depend on script execution order
  3. Removes Nuance - Previous code only used defer when necessary (Redux shared stores)

The Real Issue

React on Rails has a component_registry_timeout feature (5000ms default) that should prevent this race condition. The fact that tests still fail suggests:

  • The timeout isn't working correctly
  • Hydration doesn't properly wait for component registrations
  • There's a bug in the component registry logic

Recommended Next Steps

From the investigation document:

  1. Investigate component registry timeout - Why isn't it preventing the race?
  2. Reproduce locally - Use network throttling to catch intermittent failures
  3. Add instrumentation - Log registration timing to understand the issue
  4. Fix properly - Ensure hydration waits for registrations, don't force execution order

Files Changed

  • lib/react_on_rails/configuration.rb - Reverted to async/sync strategy based on Shakapacker version
  • spec/dummy/app/views/layouts/application.html.erb - Restored conditional defer logic
  • spec/dummy/config/initializers/react_on_rails.rb - Removed defer-specific comments
  • spec/react_on_rails/configuration_spec.rb - Updated test expectations
  • PR_1972_INVESTIGATION.md - NEW: Comprehensive investigation document

Breaking Changes

None for users. This restores the previous behavior that was working for most use cases. The intermittent test failures were edge cases that need proper investigation.

Performance Impact

✅ Positive - Restores async loading for better performance

Test Plan

  • All RuboCop checks pass
  • Files properly formatted
  • Need to investigate and fix the component registry timeout before tests will be reliable

Related Issues


🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com


This change is Reviewable

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced loading strategy configuration with improved version compatibility detection for script loading.
  • Documentation

    • Added investigation notes detailing script loading and component registration analysis with recommendations.

justin808 and others added 2 commits November 11, 2025 09:36
Creates detailed analysis of the component registration race condition
that PR #1972 attempted to fix by changing the default loading strategy
from :async to :defer.

Key findings:
- PR #1972 treats symptom (execution order) not root cause (timing)
- Defer forces sequential execution, hurting performance
- React on Rails already has component_registry_timeout feature
- Race condition suggests timeout isn't working correctly
- Need to investigate why hydration doesn't wait for registrations

Recommendations:
- Keep async as default for performance
- Fix component registry timeout mechanism
- Use defer only when truly necessary (documented cases)
- Add proper synchronization instead of forced execution order

This investigation provides foundation for implementing a proper fix
that maintains performance while preventing race conditions.

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

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 11, 2025

Walkthrough

This PR investigates and implements a refined solution for component registration race conditions by introducing conditional loading strategy defaults based on Shakapacker version capabilities. The configuration now defaults to async for Shakapacker >= 8.2.0, sync for older versions, with conditional script loading in the layout and explicit strategy configuration in tests.

Changes

Cohort / File(s) Summary
Investigation Documentation
PR_1972_INVESTIGATION.md
New document analyzing the component registration race condition, critiquing the PR #1972 defer-based solution, and surveying remediation options including registry timeout, explicit dependencies, and smart loading strategies.
Configuration Logic
lib/react_on_rails/configuration.rb
Modified validate_generated_component_packs_loading_strategy to implement conditional defaults: async for Shakapacker >= 8.2.0 (if supported), sync for earlier versions with warning, and error handling for unsupported async configurations.
Layout & Initializer
spec/dummy/app/views/layouts/application.html.erb, spec/dummy/config/initializers/react_on_rails.rb
Updated client-bundle loading to conditionally use defer (with redux) or async (without), and explicitly set generated component packs strategy to defer in initializer.
Test Updates
spec/react_on_rails/configuration_spec.rb
Adjusted test expectations to reflect new defaults: async for Shakapacker >= 8.2.0 and sync for older versions, with assertion for warning emission on fallback.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Specific areas requiring attention:
    • lib/react_on_rails/configuration.rb: The new conditional logic for defaults and error handling across Shakapacker versions requires verification that all paths are correctly implemented and tested.
    • Version detection and compatibility: Ensure PackerUtils.supports_async_loading? is reliable and covers all edge cases.
    • Layout conditional logic: Verify the redux store check correctly determines when to use defer vs. async, and ensure it aligns with the updated configuration defaults.
    • Test coverage gaps: Check that all new conditional branches (async supported/not supported) are covered with explicit test cases.
    • Investigation document: Review whether all identified issues and recommendations are addressed or if follow-up work is needed.

Possibly related PRs

Suggested labels

review-needed

Suggested reviewers

  • AbanoubGhadban
  • Judahmeek
  • alexeyr-ci2

Poem

🐰 Hops through the scripts with care so keen,
Async, defer, and sync serene!
Race conditions? Not today, friend!
With Shakapacker's version check—no end!
Loading strategies, now aligned with grace, 🚀
Components register at the perfect pace!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR explicitly reverts PR #1972 but adds a detailed investigation document highlighting that the reverted approach (defer) masked the real race condition; it provides analysis and recommendations for a proper fix rather than implementing the linked issue's solution. Clarify the rationale in the PR description: this PR intentionally rejects PR #1972's solution in favor of root-cause investigation, so it does not implement the linked issue requirements but supercedes them.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change—reverting PR #1972 and adding a comprehensive investigation document—which aligns with the changeset.
Out of Scope Changes check ✅ Passed All changes relate to reverting PR #1972 and investigating the underlying race condition issue; no unrelated modifications are present.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch justin808/revert-pr-1972

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 93c15ef and bfada7d.

📒 Files selected for processing (5)
  • PR_1972_INVESTIGATION.md (1 hunks)
  • lib/react_on_rails/configuration.rb (2 hunks)
  • spec/dummy/app/views/layouts/application.html.erb (1 hunks)
  • spec/dummy/config/initializers/react_on_rails.rb (1 hunks)
  • spec/react_on_rails/configuration_spec.rb (2 hunks)
🧰 Additional context used
🧠 Learnings (9)
📚 Learning: 2024-07-27T10:08:35.868Z
Learnt from: theforestvn88
Repo: shakacode/react_on_rails PR: 1620
File: spec/dummy/client/app/startup/HelloTurboStream.jsx:3-3
Timestamp: 2024-07-27T10:08:35.868Z
Learning: The `RailsContext` import in `spec/dummy/client/app/startup/HelloTurboStream.jsx` is used later in the project, as clarified by the user theforestvn88.

Applied to files:

  • spec/dummy/app/views/layouts/application.html.erb
📚 Learning: 2025-09-15T21:24:48.207Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1781
File: node_package/src/ClientSideRenderer.ts:82-95
Timestamp: 2025-09-15T21:24:48.207Z
Learning: In React on Rails, the force_load feature includes both explicit `data-force-load="true"` usage and the ability to hydrate components during the page loading state (`document.readyState === 'loading'`). Both capabilities require a Pro license, so the condition `!railsContext.rorPro && (isComponentForceLoaded || document.readyState === 'loading')` correctly gates both scenarios.

Applied to files:

  • spec/dummy/app/views/layouts/application.html.erb
  • spec/dummy/config/initializers/react_on_rails.rb
  • lib/react_on_rails/configuration.rb
  • spec/react_on_rails/configuration_spec.rb
📚 Learning: 2025-02-13T16:50:26.861Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/turbolinksUtils.ts:34-36
Timestamp: 2025-02-13T16:50:26.861Z
Learning: In React on Rails, when checking for Turbolinks version 5 using `turbolinksVersion5()`, always ensure `Turbolinks` exists first by checking `turbolinksInstalled()` to prevent TypeError when accessing properties.

Applied to files:

  • spec/dummy/app/views/layouts/application.html.erb
📚 Learning: 2025-09-16T08:01:11.146Z
Learnt from: justin808
Repo: shakacode/react_on_rails PR: 1770
File: lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.jsx:2-2
Timestamp: 2025-09-16T08:01:11.146Z
Learning: React on Rails uses webpack CSS Modules configuration with namedExports: true, which requires the import syntax `import * as style from './file.module.css'` rather than the default export pattern. This configuration enables better tree shaking and bundle size optimization for CSS modules.

Applied to files:

  • spec/dummy/app/views/layouts/application.html.erb
  • spec/dummy/config/initializers/react_on_rails.rb
  • lib/react_on_rails/configuration.rb
  • spec/react_on_rails/configuration_spec.rb
📚 Learning: 2025-02-12T16:38:06.537Z
Learnt from: Romex91
Repo: shakacode/react_on_rails PR: 1697
File: package-scripts.yml:28-28
Timestamp: 2025-02-12T16:38:06.537Z
Learning: The file `node_package/lib/ReactOnRails.full.js` is autogenerated during the build process and should not be present in the repository.

Applied to files:

  • spec/dummy/config/initializers/react_on_rails.rb
📚 Learning: 2025-04-26T21:55:55.874Z
Learnt from: alexeyr-ci2
Repo: shakacode/react_on_rails PR: 1732
File: spec/dummy/client/app-react16/startup/ReduxSharedStoreApp.client.jsx:40-44
Timestamp: 2025-04-26T21:55:55.874Z
Learning: In the react_on_rails project, files under `app-react16` directories are copied/moved to corresponding `/app` directories during the conversion process (removing the `-react16` suffix), which affects their relative import paths at runtime.

Applied to files:

  • spec/dummy/config/initializers/react_on_rails.rb
  • spec/react_on_rails/configuration_spec.rb
📚 Learning: 2025-02-18T13:08:01.477Z
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.

Applied to files:

  • lib/react_on_rails/configuration.rb
📚 Learning: 2025-10-23T17:22:01.074Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1875
File: lib/react_on_rails/utils.rb:112-124
Timestamp: 2025-10-23T17:22:01.074Z
Learning: In React on Rails, when Pro is installed but not licensed, the intended behavior is to raise an error on boot. The `react_on_rails_pro?` method validates licenses and should raise errors early (including during path resolution in methods like `server_bundle?`) to enforce licensing requirements rather than failing later with obscure errors.

Applied to files:

  • lib/react_on_rails/configuration.rb
📚 Learning: 2025-07-08T05:57:29.630Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1745
File: node_package/src/RSCRequestTracker.ts:8-14
Timestamp: 2025-07-08T05:57:29.630Z
Learning: The global `generateRSCPayload` function in React on Rails Pro (RORP) is provided by the framework during rendering requests, not implemented in application code. The `declare global` statements are used to document the expected interface that RORP will inject at runtime.

Applied to files:

  • lib/react_on_rails/configuration.rb
🧬 Code graph analysis (2)
lib/react_on_rails/configuration.rb (1)
lib/react_on_rails/packer_utils.rb (1)
  • supports_async_loading? (35-37)
spec/react_on_rails/configuration_spec.rb (1)
lib/react_on_rails/configuration.rb (2)
  • configure (6-9)
  • configuration (14-54)

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.

@claude
Copy link

claude bot commented Nov 11, 2025

Code Review: PR #1980 - Revert PR #1972 and Add Investigation

Summary

This PR reverts PR #1972 (which changed the default loading strategy from :async to :defer) and adds a comprehensive investigation document. I've reviewed the code changes, documentation, and the rationale behind the revert.


✅ Strengths

1. Excellent Documentation

The PR_1972_INVESTIGATION.md file is outstanding. It provides:

  • Clear root cause analysis of the race condition
  • Detailed explanation of async vs defer behavior
  • Multiple solution approaches with trade-offs
  • Actionable next steps for proper resolution

This is exactly the kind of documentation that helps future maintainers understand the decision-making process.

2. Thoughtful Technical Analysis

The investigation correctly identifies that PR #1972 treated the symptom rather than the root cause:

  • The race condition exists because hydration doesn't properly wait for component registration
  • Forcing :defer everywhere hurts performance unnecessarily
  • The component_registry_timeout feature (5000ms default) should prevent this issue

3. Preserves Conditional Logic

Restoring the uses_redux_shared_store? helper shows good judgment - recognizing that only certain pages actually need :defer, not all pages.

4. Code Quality

  • All formatting follows RuboCop rules ✅
  • Test expectations correctly updated ✅
  • Comments are clear and helpful ✅

⚠️ Issues & Concerns

1. CRITICAL: Incomplete Revert in Initializer

# spec/dummy/config/initializers/react_on_rails.rb (line 45)
config.generated_component_packs_loading_strategy = :defer

Problem: The revert is incomplete. The initializer still explicitly sets :defer, which overrides the smart default logic in configuration.rb:179-180.

Impact:

  • The configuration will use :defer instead of :async even on Shakapacker >= 8.2.0
  • This defeats the purpose of reverting to async-by-default behavior
  • Performance gains from the revert won't be realized

Fix: This line should either be:

  1. Removed entirely (to use smart defaults), OR
  2. Commented out with an explanation

Location: spec/dummy/config/initializers/react_on_rails.rb:45

2. Test Coverage Gap

The investigation mentions "intermittent test failures" but the PR doesn't address:

  • Will the tests fail again after this revert?
  • Should we add retries for these tests (as suggested in the investigation)?
  • How will we verify the real fix works when implemented?

Recommendation: Consider adding:

# In spec/system/integration_spec.rb for these specific tests
describe "2 react components, 1 store, client only, defer", :js, retry: 3 do

3. Missing Critical Discovery

I reviewed the open-source ComponentRegistry.ts implementation and found something important:

// packages/react-on-rails/src/ComponentRegistry.ts:63-65
getOrWaitForComponent(_name: string): never {
  throw new Error('getOrWaitForComponent requires react-on-rails-pro package');
}

Key Finding: The component_registry_timeout functionality requires the Pro package! The open-source version has no waiting mechanism at all.

Implications:

  • If the dummy app uses the open-source package, the timeout never worked
  • The investigation assumes a feature that might not be present in the test environment
  • This explains why the race condition occurs despite the timeout configuration

Action Needed: The investigation document should be updated to clarify whether:

  1. The dummy app tests use the Pro package or open-source
  2. The timeout feature is available in the test environment
  3. This is purely a Pro feature that users expect to work in open-source

4. Rubocop Complexity Warnings

# lib/react_on_rails/configuration.rb:157-159
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def validate_generated_component_packs_loading_strategy
  # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

While this passes RuboCop, the method might benefit from refactoring to reduce complexity. Not a blocker for this PR, but worth noting for future cleanup.


🔒 Security Considerations

✅ No security concerns identified. Changes are configuration-only.


⚡ Performance Analysis

Expected Impact (Once Issue #1 is Fixed)

  • Positive: Restores parallel async loading on modern Shakapacker (>= 8.2.0)
  • Neutral: Pages with uses_redux_shared_store? still use defer (necessary)
  • Risk: Tests may fail intermittently until root cause is properly fixed

Current Impact (With Issue #1)

  • Neutral: Due to explicit :defer in initializer, no performance change yet

🧪 Test Coverage Assessment

What's Tested

✅ Configuration defaults based on Shakapacker version
✅ Validation of loading strategy values
✅ Warning messages for unsupported async loading

What's Missing

❌ No tests for the actual race condition scenario
❌ No integration tests verifying component registration timing
❌ No tests for uses_redux_shared_store? helper behavior

Recommendation: Future PR should add timing-based integration tests.


📋 Recommendations

Must Fix Before Merge

  1. Fix the incomplete revert in spec/dummy/config/initializers/react_on_rails.rb:45
    • Remove or comment out the explicit :defer setting

Should Address

  1. Update investigation document to clarify Pro vs open-source timeout behavior
  2. Add note about expected test failures until root cause is fixed
  3. Consider adding retry logic to intermittent tests

Nice to Have

  1. Consider adding a note in CHANGELOG.md about this revert (user-visible behavior change)
  2. Add TODO comments in relevant files pointing to the investigation document

🎯 Overall Assessment

Code Quality: ✅ Excellent (with one fix needed)
Documentation: ✅ Outstanding
Testing: ⚠️ Adequate (tests will likely fail intermittently)
Architecture: ✅ Sound reasoning and approach

Recommendation: Approve with changes - Fix the initializer issue (#1), and this is ready to merge.

The investigation document is valuable and the revert is justified. The key insight that PR #1972 treated symptoms rather than root cause is correct. However, the incomplete revert in the initializer needs to be fixed to actually restore the async-by-default behavior.


💡 Follow-up Questions

  1. Does the dummy app use react-on-rails-pro or open-source? (affects timeout availability)
  2. What's the plan for fixing tests that will fail after this merges?
  3. Should we create a GitHub issue to track the root cause investigation?
  4. Is there a timeline for implementing one of the recommended solutions from the investigation?

Review completed with Claude Code 🤖

@claude
Copy link

claude bot commented Nov 11, 2025

Code Review: PR #1980 - Revert PR #1972 and Add Investigation

Overall Assessment

This is a well-reasoned revert with excellent supporting documentation. The decision to revert PR #1972 and investigate the root cause is the right approach. The investigation document is thorough and provides valuable analysis.


Strengths

1. Excellent Documentation

The PR_1972_INVESTIGATION.md file is outstanding:

  • Clear problem statement and root cause analysis
  • Detailed explanation of async vs defer behavior
  • Multiple solution alternatives with trade-offs
  • Actionable next steps

This is exactly the kind of documentation that helps future maintainers.

2. Sound Technical Reasoning

The revert is justified:

3. Code Quality

  • ✅ Proper restoration of conditional logic based on uses_redux_shared_store?
  • ✅ Test expectations correctly updated to match reverted behavior
  • ✅ RuboCop compliance maintained
  • ✅ Comments explain the async/defer trade-offs

Areas for Improvement

1. Configuration Change Inconsistency

Issue: In spec/dummy/config/initializers/react_on_rails.rb, the change seems contradictory:

# Line 45: Explicitly sets defer instead of letting it default to async
config.generated_component_packs_loading_strategy = :defer

Problem: The PR description and investigation document advocate for async as the default, but the dummy app is explicitly setting defer. This creates confusion.

Recommendation:

# Option 1: Remove the line entirely to use the smart default
# (async on Shakapacker >= 8.2.0, sync otherwise)

# Option 2: If defer is needed for testing, add a clear comment:
# Explicitly use defer for testing Redux shared store race conditions
config.generated_component_packs_loading_strategy = :defer

2. Missing Test Coverage for Component Registry Timeout

The investigation correctly identifies that the component_registry_timeout feature isn't working as expected, but there's no test to verify this.

Recommendation: Add a test that verifies timeout behavior:

# spec/react_on_rails/component_registry_spec.rb
it "waits for component registration before hydrating" do
  # Test that hydration waits up to component_registry_timeout
  # for components to register
end

it "fails gracefully after timeout expires" do
  # Test that appropriate error is shown if component
  # never registers within timeout period
end

3. Potential Breaking Change Not Documented

Concern: This revert changes behavior for users who upgraded to the version with PR #1972:

Recommendation:

  1. Add a note to CHANGELOG.md warning users about the revert
  2. Provide migration guidance (e.g., explicitly set defer if needed)
  3. Consider this for the release notes

4. Investigation Document Location

Minor: PR_1972_INVESTIGATION.md in the root directory will accumulate over time.

Recommendation: Consider moving to docs/investigations/ or docs/adr/ (Architecture Decision Records)


Potential Bugs & Security

1. Race Condition Still Exists ⚠️

The intermittent test failures will likely return. The investigation document acknowledges this.

Impact: Users with slower networks or larger bundles may experience component registration errors.

Mitigation:

  • Fast-track the investigation into component registry timeout
  • Consider adding retry logic in tests temporarily
  • Document workaround (explicitly use defer) in troubleshooting guide

2. No Security Concerns

The changes are configuration-related and don't introduce security vulnerabilities.


Performance Considerations

Positive Impact ✅

Restoring async as default improves performance:

  • Scripts download in parallel
  • Execute as soon as ready (no waiting for HTML parsing to complete)
  • Better for users on fast connections

Conditional Optimization ✅

The uses_redux_shared_store? helper properly identifies pages that need defer, maintaining performance where possible.


Test Coverage

Current State

  • ✅ Configuration spec updated correctly
  • ✅ Integration tests exist for shared store scenarios
  • ✅ Both async and defer paths are tested

Gaps

  • ❌ No test for component registry timeout mechanism
  • ❌ No test verifying race condition is actually fixed (when proper fix lands)
  • ❌ No performance regression test

Recommendation: Add integration test that simulates slow network:

# spec/system/integration_spec.rb
it "handles slow component registration gracefully", :js do
  # Use network throttling or delays to test timeout behavior
  # Verify component_registry_timeout prevents race condition
end

Code Quality & Best Practices

Excellent

  • Clear separation of concerns (controller helper vs layout)
  • Self-documenting code with good comments
  • Follows repository's RuboCop standards
  • Consistent with CLAUDE.md requirements

Suggestions

  1. Extract magic strings: The action names in uses_redux_shared_store? could be constants
  2. DRY principle: Consider helper method for script tag generation with loading strategy

Recommendations Summary

Must Address Before Merge

  1. Clarify configuration intent in spec/dummy/config/initializers/react_on_rails.rb
  2. Add CHANGELOG entry warning about behavior change
  3. Document migration path for users affected by revert

Should Address Soon (Follow-up PRs)

  1. Investigate component registry timeout - highest priority
  2. Add test coverage for timeout mechanism
  3. Move investigation doc to docs/ directory
  4. Add network throttling tests for race conditions

Nice to Have

  1. Extract constants for action names
  2. Performance benchmarks comparing async vs defer
  3. User-facing documentation on when to use each strategy

Conclusion

Approve with minor changes

This PR demonstrates excellent software engineering:

  • Recognizes when a fix is treating symptoms
  • Documents the investigation thoroughly
  • Makes the hard choice to revert and do it right
  • Provides clear path forward

The only concern is potential impact on users who upgraded to the version with PR #1972. Add appropriate warnings and migration guidance.

Great work on the investigation document - this is exactly how complex technical decisions should be documented! 🎯


📚 Alignment with CLAUDE.md:

  • ✅ RuboCop compliance maintained
  • ✅ Test expectations updated correctly
  • ✅ Clear comments and documentation
  • ✅ No formatting issues (Prettier/RuboCop handle it)

🤖 Generated with Claude Code

@claude
Copy link

claude bot commented Nov 11, 2025

Code Review

Overall Assessment

This is a well-reasoned revert with excellent documentation. The PR demonstrates strong engineering judgment by recognizing that PR #1972's solution was treating symptoms rather than addressing root causes. The addition of PR_1972_INVESTIGATION.md is particularly valuable for future maintainers.


Strengths

1. Excellent Documentation

The investigation document (PR_1972_INVESTIGATION.md) is exemplary:

  • Clear root cause analysis with technical depth
  • Explains the trade-offs between async/defer loading strategies
  • Provides multiple solution paths with pros/cons
  • Sets up actionable next steps

2. Performance-Conscious Decision

Correctly prioritizes async loading for better performance:

  • Async allows parallel execution (faster page loads)
  • Defer forces sequential execution (slower but predictable)
  • The revert restores the performance-optimized default

3. Preserves Existing Nuance

The uses_redux_shared_store? helper demonstrates awareness that not all pages need defer:

  • Only pages with Redux shared stores require defer
  • Other pages benefit from async loading
  • This selective approach is better than forcing defer everywhere

4. Code Quality

  • All changes follow RuboCop style guidelines
  • Comments are clear and explain the "why" not just the "what"
  • Test expectations correctly updated to match behavior

Technical Analysis

Configuration Changes (lib/react_on_rails/configuration.rb)

Before PR #1972:

if PackerUtils.supports_async_loading?
  self.generated_component_packs_loading_strategy ||= :async
elsif generated_component_packs_loading_strategy.nil?
  self.generated_component_packs_loading_strategy = :sync
end

After PR #1972 (being reverted):

self.generated_component_packs_loading_strategy = :defer

This revert (correct):

if PackerUtils.supports_async_loading?
  self.generated_component_packs_loading_strategy ||= :async
# ... falls back to :sync with warning

Good: Restores smart defaults based on Shakapacker version
Good: Adds warning when falling back to sync
Good: Preserves backward compatibility

Layout Changes (spec/dummy/app/views/layouts/application.html.erb)

The conditional logic is appropriate:

<% if uses_redux_shared_store? %>
  <%= javascript_pack_tag('client-bundle', defer: true) %>
<% else %>
  <%= javascript_pack_tag('client-bundle', async: true) %>
<% end %>

Good: Uses defer only when necessary (Redux shared stores)
Good: Comments explain when to use each strategy
Good: References documentation for more details


Concerns & Recommendations

1. Critical Follow-up Required ⚠️

The investigation document correctly identifies that component_registry_timeout (5000ms default) should prevent this race condition. The fact that it doesn't work suggests a real bug. This needs immediate investigation:

Action items:

  • Review packages/react-on-rails/src/ComponentRegistry.ts
  • Verify timeout is honored during hydration
  • Add instrumentation/logging to catch timing issues
  • Test with network throttling to reproduce locally

Without fixing this, you're likely to see the intermittent failures return.

2. Potential Config Override Issue 🔍

In spec/dummy/config/initializers/react_on_rails.rb:

config.generated_component_packs_loading_strategy = :defer

This explicitly sets defer, which will override the smart defaults in configuration.rb. This might be intentional for the dummy app, but it means:

  • The dummy app always uses defer (good for testing)
  • But this doesn't test the async code path
  • CI might not catch async-related issues

Recommendation:
Consider having separate test scenarios that explicitly test both strategies, or use environment variables to toggle between them in CI.

3. Test Coverage Gap 📋

The investigation notes that the failing tests were:

  • spec/system/integration_spec.rb[1:1:6:1:2] - "defer" in name
  • spec/system/integration_spec.rb[1:1:6:2:2] - "defer" in name

These tests specifically test defer functionality. The document suggests they might fail because they're testing defer behavior but getting async. However, the dummy app config explicitly sets defer, so this seems inconsistent.

Recommendation:

  • Verify test setup correctly controls loading strategy
  • Ensure defer tests actually use defer
  • Add explicit async tests to catch regressions

4. RuboCop Disable Comment 💭

# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def validate_generated_component_packs_loading_strategy
  # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

While this change just adds PerceivedComplexity to an existing disable, the method might benefit from refactoring to reduce complexity. Not blocking, but worth considering for future cleanup.

5. Missing Changelog Update 📝

According to CLAUDE.md, user-visible changes should be documented in CHANGELOG.md. This revert:

  • Changes default behavior (async vs defer)
  • Could affect performance for users
  • Is definitely user-visible

Recommendation:
Add a changelog entry explaining:


Security Considerations

No security concerns identified. This change only affects:

  • Script loading strategy (async vs defer)
  • Internal configuration defaults
  • Test dummy app behavior

Performance Impact

Positive Performance Impact

Reverting to async as default will improve performance for most users:

  • Async allows scripts to execute as soon as ready (parallel execution)
  • Defer forces sequential execution (potentially slower)
  • Modern browsers benefit from async loading

The selective use of defer for Redux shared stores is the right balance.


Test Plan Assessment

The PR description mentions:

  • All RuboCop checks pass
  • Files properly formatted
  • Need to investigate component registry timeout

Concern: The last checkbox acknowledges tests might not be reliable yet. This is honest but concerning.

Recommendation:
Before merging, ensure:

  1. All existing tests pass consistently
  2. CI runs multiple times to catch intermittent failures
  3. Both async and defer code paths are tested

You can use the bin/ci-rerun-failures script mentioned in CLAUDE.md to verify.


Code Quality Details

Formatting ✅

  • All changes follow RuboCop conventions
  • Comments are well-written and helpful
  • Indentation and style are consistent

Error Handling ✅

  • Proper warning messages when falling back to sync
  • Clear error messages for invalid configurations
  • Maintains existing error handling patterns

Documentation ✅

  • Inline comments explain complex logic
  • References external documentation
  • Investigation document is comprehensive

Recommendations Summary

Before merging:

  1. ✅ Code changes look good - approve as-is
  2. ⚠️ Add CHANGELOG.md entry for user-visible changes
  3. ⚠️ Verify tests pass consistently (run CI multiple times)
  4. ⚠️ Consider if dummy app config should test both strategies

After merging (high priority):

  1. 🔴 Investigate why component_registry_timeout doesn't work
  2. 🔴 Add instrumentation to catch timing issues
  3. 🔴 Implement proper synchronization fix
  4. 🟡 Consider refactoring validate_generated_component_packs_loading_strategy to reduce complexity

Nice to have:

  • Add explicit tests for both async and defer strategies
  • Document performance implications in user docs
  • Consider per-component loading strategy option

Conclusion

Recommendation: Approve with minor changes requested

This is a well-thought-out revert that prioritizes performance and addresses architectural concerns. The investigation document is valuable and sets up the team for a proper fix. However:

  1. Add CHANGELOG entry - Required per CLAUDE.md guidelines
  2. Verify test stability - Critical before merging
  3. Plan follow-up work - The race condition still exists

The code quality is excellent, and the engineering judgment is sound. Once the changelog is updated and tests verified, this should be merged to unblock proper investigation of the root cause.


Great work on the thorough investigation! The detailed analysis in PR_1972_INVESTIGATION.md will be invaluable for anyone debugging this in the future. 👏

@claude
Copy link

claude bot commented Nov 11, 2025

Code Review: Revert PR #1972 and Investigation

I've reviewed this PR which reverts PR #1972 and adds a comprehensive investigation document. Here's my detailed feedback:


✅ Strengths

1. Excellent Investigation Document

The PR_1972_INVESTIGATION.md is thorough and well-structured:

  • Clear explanation of the race condition root cause
  • Detailed analysis of why defer "fixes" but doesn't solve the underlying issue
  • Multiple solution options with trade-offs
  • Actionable next steps

This is exactly the kind of documentation needed before implementing a proper fix.

2. Correct Architectural Decision

Reverting PR #1972 is the right call because:

  • Performance matters: Forcing defer everywhere removes the benefits of async loading
  • Nuance preserved: The conditional uses_redux_shared_store? logic was more precise
  • Treats cause, not symptom: Forcing execution order masks the real timing/coordination issue

3. Code Quality

  • All changes correctly restore the previous behavior
  • Tests properly updated to expect :async as default with Shakapacker >= 8.2.0
  • RuboCop violations properly addressed (added Metrics/PerceivedComplexity to disable comment)

🔍 Findings & Observations

1. The Real Issue: Component Registry Timeout Not Working

The open-source version of React on Rails has a non-functional waiting mechanism:

packages/react-on-rails/src/ComponentRegistry.ts:63-64:

getOrWaitForComponent(_name: string): never {
  throw new Error('getOrWaitForComponent requires react-on-rails-pro package');
}

This is a critical insight missing from the investigation document:

  • The component_registry_timeout config exists (5000ms default)
  • It's passed to the client via RailsContext
  • But the open-source package doesn't implement the waiting logic
  • Only the Pro version has CallbackRegistry with getOrWaitForItem() that actually waits

This explains why the race condition occurs! The open-source version immediately throws an error if a component isn't registered, rather than waiting up to 5000ms.

2. Test Expectations Issue

Looking at the config in spec/dummy/config/initializers/react_on_rails.rb:45:

config.generated_component_packs_loading_strategy = :defer

This explicitly sets defer, which means:

  • The dummy app is testing with defer mode
  • The test expectations should match this configuration
  • The tests at spec/react_on_rails/configuration_spec.rb are testing the default value, not the dummy app's explicit config

These are two different things:

  1. Default behavior (what the library does by default) - should be :async for performance
  2. Dummy app config (what the test app explicitly chooses) - set to :defer for testing

3. Conditional Logic Dependency

The restored uses_redux_shared_store? helper in application.html.erb:15 is a good approach, but:

  • This logic is in the dummy app, not the gem itself
  • Users need to implement similar logic in their own apps
  • Documentation should clearly explain when defer is needed

⚠️ Potential Issues

1. Investigation Document Location

The file PR_1972_INVESTIGATION.md is at the root of the repository. Consider:

  • Should it be in a docs/ or docs/investigations/ folder?
  • Is this intended to be temporary or permanent documentation?
  • Does it need a link from the main README or documentation index?

2. Missing Pro vs Open Source Clarification

The investigation document discusses component_registry_timeout but doesn't mention that:

  • The waiting mechanism only exists in Pro
  • Open-source users must use defer for guaranteed ordering
  • This is by design (Pro feature differentiation)

Recommendation: Add a section explaining this key difference:

## Pro vs Open Source Behavior

**React on Rails Pro** includes `CallbackRegistry` with timeout-based waiting:
- Hydration waits up to `component_registry_timeout` (5000ms) for components
- Async loading works safely with proper synchronization
- Better performance without sacrificing reliability

**React on Rails Open Source**:
- No waiting mechanism (`getOrWaitForComponent` throws immediately)
- Must rely on script execution order (defer) or explicit dependencies
- Race conditions possible with async loading

3. Test Failure Expectations

After this revert, the tests mentioned in PR #1972 might fail again:

  • spec/system/integration_spec.rb[1:1:6:1:2] - "2 react components, 1 store, client only, defer"
  • spec/system/integration_spec.rb[1:1:6:2:2] - "2 react components, 1 store, server side, defer"

Questions:

  1. Do these tests pass on the current master branch?
  2. Are these intermittent failures or consistent?
  3. Should we add retry: 3 to these specific tests as mentioned in the investigation?

4. RuboCop Comment Precision

In lib/react_on_rails/configuration.rb:157-159, the rubocop disable comment was updated:

# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def validate_generated_component_packs_loading_strategy
  # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

Best practice: Only disable the cops that are actually needed. The method should be refactored to reduce complexity rather than disabling warnings. Consider extracting helper methods:

def validate_generated_component_packs_loading_strategy
  handle_deprecated_defer_config
  validate_async_loading_support
  validate_strategy_value
end

🎯 Recommendations

Immediate Actions (Before Merging)

  1. Clarify test expectations

    • Run the full test suite to verify behavior
    • Document expected test failures (if any)
    • Consider adding retry: 3 for intermittent tests
  2. Update investigation document

    • Add "Pro vs Open Source" section explaining waiting mechanism
    • Note that open-source users should use defer for guaranteed ordering
    • Explain that the recommended solution (fixing timeout) only applies to Pro
  3. Documentation updates

    • Update main docs to explain async vs defer trade-offs
    • Document the uses_redux_shared_store? pattern for users to copy
    • Link to the investigation document from relevant docs

Long-term Solutions

The investigation document's recommendations are good, but should distinguish between Pro and open-source:

For Pro users:

  • Fix/enhance the CallbackRegistry timeout mechanism if needed
  • Ensure getOrWaitForComponent is used consistently

For open-source users:

  • Document that defer is required for Redux shared stores
  • Provide clear examples of when async is safe
  • Consider adding a helper to detect when defer is needed

🔒 Security & Performance

Security: No concerns. This is a configuration change with no security implications.

Performance:

  • Positive impact - Restores async loading as default
  • ✅ Defer only used where needed (Redux shared stores)
  • ✅ Better alignment with modern web performance best practices

📝 Test Coverage

Existing tests: The test changes properly reflect the reverted behavior.

Missing tests: Consider adding:

  1. Integration test that verifies async loading works without race conditions (for Pro)
  2. Test that verifies the fallback behavior when components aren't ready (should fail gracefully)
  3. Test for the uses_redux_shared_store? helper logic

Summary & Verdict

Recommendation: ✅ Approve with minor suggestions

This PR is well-executed with excellent documentation. The revert is justified and the investigation provides a solid foundation for future improvements.

Before merging:

  1. Decide on the investigation document's permanent location
  2. Add Pro vs open-source clarification to the investigation
  3. Verify test suite behavior and document any expected failures
  4. Consider refactoring the RuboCop-disabled method for better code quality

After merging:

  1. Update main documentation with async/defer guidance
  2. Plan the proper fix (Pro: enhance CallbackRegistry, OSS: improve documentation)
  3. Consider extracting the uses_redux_shared_store? pattern into the gem as a helper

Great work on the thorough investigation! 🎉


🤖 Generated with Claude Code

@claude
Copy link

claude bot commented Nov 11, 2025

Code Review - PR #1980: Revert PR #1972 and Add Investigation

I've reviewed this PR and have feedback on code quality, architecture, and testing. Overall, this is a well-reasoned revert with excellent documentation, though there are some concerns to address.


Strengths

1. Excellent Investigation Document

The PR_1972_INVESTIGATION.md is outstanding:

  • Clear root cause analysis of the race condition
  • Well-structured recommendations
  • Proper acknowledgment of trade-offs
  • Educational value for maintainers

2. Restores Nuanced Behavior

The conditional logic in application.html.erb is much better than the blanket defer: true:

<% if uses_redux_shared_store? %>
  <%= javascript_pack_tag('client-bundle', defer: true) %>
<% else %>
  <%= javascript_pack_tag('client-bundle', async: true) %>
<% end %>

This recognizes that only specific pages need defer.

3. Correct Default Behavior

The configuration change properly defaults to :async when Shakapacker >= 8.2.0:

if PackerUtils.supports_async_loading?
  self.generated_component_packs_loading_strategy ||= :async

4. Tests Updated Correctly

The spec changes properly reflect the restored behavior.


⚠️ Critical Issues

1. Configuration InconsistencyBLOCKING

Problem: spec/dummy/config/initializers/react_on_rails.rb line 45:

config.generated_component_packs_loading_strategy = :defer

This overrides the default :async behavior you just restored! This means:

  • The dummy app will still use defer
  • The conditional logic in application.html.erb won't work as intended for non-Redux-store pages
  • Tests may not catch issues with async loading

Solution: Remove this line entirely, or add a comment explaining why the dummy app needs to override the default:

# Explicitly set to :defer for testing purposes
# In production apps, this should default to :async for better performance
config.generated_component_packs_loading_strategy = :defer

Better solution: Remove the line and rely on the conditional logic in the layout to handle defer vs async appropriately.

2. Layout Logic May Not Work as Expected

Problem: The layout uses async: true for non-Redux pages:

<%= javascript_pack_tag('client-bundle', async: true) %>

But the initializer forces :defer globally, which affects generated component packs, not the main bundle. This creates confusion about what's being controlled where.

Clarification needed:

  • Does generated_component_packs_loading_strategy affect the main client-bundle.js?
  • Or only the generated component pack scripts?
  • The layout controls main bundle loading, but what controls generated pack loading?

3. Test Coverage Gap

The investigation mentions these tests were failing:

  • spec/system/integration_spec.rb[1:1:6:1:2] - client only, defer
  • spec/system/integration_spec.rb[1:1:6:2:2] - server side, defer

Questions:

  1. Will these tests still fail after the revert?
  2. If so, is that expected behavior that needs investigation?
  3. Should there be tests for :async mode that currently pass?

Recommendation: Add a comment in the test file explaining the current state and expected behavior.


🔍 Code Quality Issues

4. RuboCop Directive Formatting

lib/react_on_rails/configuration.rb:157-159:

# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def validate_generated_component_packs_loading_strategy
  # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

Minor issue: While technically correct, having the enable on the line after the method definition is unusual. Consider:

# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def validate_generated_component_packs_loading_strategy
  # ... method body ...
end
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

Or just disable for the method:

def validate_generated_component_packs_loading_strategy # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

5. Helper Method Duplication

uses_redux_shared_store? in application_controller.rb:29-40 lists actions explicitly:

action_name.in?(%w[
  server_side_hello_world_shared_store_defer
  client_side_hello_world_shared_store_defer
  # ...
])

Observation: Actions with _defer suffix are included, but they should always use defer regardless of the helper. This seems redundant. Consider:

  • Are these actions using inline registration? Then include them.
  • Do they have _defer in the route to force defer mode? Then maybe exclude them since they're already handled differently.

Not blocking, but worth clarifying the intent.


📊 Performance Considerations

6. Performance Testing Needed

The investigation correctly notes that :async is faster than :defer, but:

  • No benchmarks or measurements provided
  • Real-world impact on page load time unclear
  • May want to add performance metrics to validate the decision

Recommendation: Add a TODO or issue to measure the actual performance difference.


🔒 Security Concerns

No security issues identified. The changes are configuration-only.


🧪 Test Coverage Concerns

7. Missing Test for Async Behavior

Problem: The tests check :defer behavior, but after this revert, the default is :async. Are there tests that verify :async works correctly?

Recommendation:

describe "2 react components, 1 store, client only, async", :js do
  include_examples "React Component Shared Store", "/client_side_hello_world_shared_store"
end

This would validate that the default behavior works.

8. Component Registry Timeout Not Tested

The investigation mentions component_registry_timeout (5000ms) should prevent the race condition, but:

  • No tests verify the timeout works
  • No tests verify hydration waits for component registration
  • This is a critical piece of the architecture

Recommendation: Add tests or investigation tasks to verify this mechanism.


📝 Documentation & Comments

9. Changelog Missing?

Based on CLAUDE.md:

Update CHANGELOG.md for user-visible changes only (features, bug fixes, breaking changes)

Question: Should this revert be documented in the changelog? It does change behavior back, which could affect users who deployed PR #1972.

Recommendation: Add a changelog entry explaining:

10. Investigation Document Location

PR_1972_INVESTIGATION.md is at the repository root. Consider:

  • Should it be in docs/investigations/ or similar?
  • Or should it be removed after the PR is merged (put the content in the PR description)?
  • Leaving investigation docs in the repo root might clutter over time

Not blocking, but worth discussing with the team.


🎯 Actionable Summary

Must Fix Before Merge:

  1. Remove or clarify config.generated_component_packs_loading_strategy = :defer in the dummy app initializer (line 45)
  2. Clarify the relationship between generated component pack strategy and main bundle loading
  3. Verify that the conditional logic in application.html.erb actually works with the initializer setting

Should Address:

  1. Add tests for :async default behavior
  2. Add changelog entry for the revert
  3. Clarify RuboCop directive placement
  4. Consider investigation document location

Future Work (Not Blocking):

  1. Investigate component registry timeout mechanism
  2. Add performance benchmarks
  3. Create follow-up issue to properly fix the race condition

🎬 Conclusion

This is a well-reasoned revert with excellent documentation. The investigation document is particularly valuable for future maintainers. However, the configuration inconsistency in the initializer needs to be resolved before merging, as it undermines the purpose of the revert.

The real issue (component registry race condition) is correctly identified and documented for future work. This is the right approach: revert the symptomatic fix, document the problem, and plan a proper solution.

Recommendation: ✅ Approve after addressing the initializer configuration issue


Thanks for the thorough investigation and documentation! This is a model for how to handle reverting premature fixes.

🤖 Review generated by Claude Code

@justin808
Copy link
Member Author

Closing as PR #1981 already reverted PR #1972. Will create a new PR focused on fixing the failing tests.

@justin808 justin808 closed this Nov 11, 2025
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.

2 participants