Fix hydration mismatch when reactOnRailsPageLoaded() called multiple times#2211
Fix hydration mismatch when reactOnRailsPageLoaded() called multiple times#2211
Conversation
|
Caution Review failedThe pull request is closed. WalkthroughImplements deduplication of component renders to prevent hydration mismatches when Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20–30 minutes
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (8)
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. Comment |
size-limit report 📦
|
Code Review SummaryThis PR effectively fixes issue #2210 where calling reactOnRailsPageLoaded() multiple times caused hydration mismatch errors for client-rendered components. The solution is elegant and well-tested. Strengths1. Root Cause Analysis is Spot-OnThe fix correctly identifies that the problem occurs when reactOnRailsPageLoaded() is called multiple times - the shouldHydrate check (!!domNode.innerHTML) was incorrectly treating previously client-rendered content as server-rendered content. 2. Minimal, Surgical Fix
3. Comprehensive Test Coverage
4. Code Quality
Observations and Suggestions1. Missing Changelog Entry (BLOCKING)According to CLAUDE.md, user-visible bug fixes should be added to CHANGELOG.md. This is a user-facing fix for a regression. Suggested addition under Fixed section:
2. E2E Test StatusThe PR description shows unchecked Playwright E2E test items. Please confirm these are passing in CI. 3. Potential Edge CaseThe fix prevents re-rendering components once in renderedRoots. If users update props in DOM and call reactOnRailsPageLoaded() again, component will be skipped. Worth documenting if this is a known limitation. Security ConsiderationsNo security concerns identified. Overall AssessmentVerdict: Excellent fix, ready to merge after addressing the changelog. Risk Level: Low - minimal code change, comprehensive tests, no breaking changes. Recommendation: Add changelog entry, confirm E2E tests pass, then merge with confidence. Great work on this fix! |
c37fddb to
2295e78
Compare
Code Review - PR #2211Thank you for the fix! This addresses a real bug in the beta version. Here's my comprehensive review: ✅ Strengths
🔍 Code Quality ObservationsClientRenderer.ts ChangesLine 84-89: The guard clause is well-placed and properly returns early. if (renderedRoots.has(domNodeId)) {
if (trace) {
console.log(`Skipping already rendered component: ${name} (dom id: ${domNodeId})`);
}
return;
}✅ Good:
Test QualityClientRenderer.test.ts (lines 206-263):
// Current approach (works but indirect)
expect(mockHydrateOrRender.mock.calls.length).toBe(callCountAfterFirstRender);
// More explicit alternative
expect(mockHydrateOrRender).not.toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything()
);Verdict: Current approach is fine, but the test name could be even clearer about what it's testing. E2E Tests (page_loaded_multiple_calls.spec.js)✅ Excellent:
📝 Documentation Suggestion: Consider adding a comment in the spec explaining why this test is critical for regression prevention. 🐛 Potential Issues1. Edge Case: Manually Cleared DOM NodesScenario: What if user code manually clears a DOM node's innerHTML after it's been rendered? // Component rendered
ReactOnRails.reactOnRailsPageLoaded();
// User code clears the DOM
document.getElementById('MyComponent-1').innerHTML = '';
// Second call - will skip rendering due to renderedRoots.has() check
ReactOnRails.reactOnRailsPageLoaded();Impact: The component won't re-render because it's still in Assessment: This is an edge case and probably acceptable behavior (user shouldn't manually manipulate React-managed DOM). But worth documenting. Recommendation: Add a note in the JSDoc comment for 2. Memory Management for SPA-style NavigationThe current Current cleanup: Question: Does Turbo/Hotwire trigger Recommendation:
3. Test Checkbox Not Marked CompleteIn the PR description, this checkbox is unchecked: - [ ] Playwright E2E test: async_page_loaded_test page should show no hydration errorsAction: Please run the Playwright test locally and mark it as complete, or document if there are blockers. 🔒 Security Considerations✅ No security concerns identified. The change:
⚡ Performance Considerations✅ Positive Impact: The early return on line 88 prevents unnecessary:
The Estimated performance improvement: For apps calling 📋 RecommendationsHigh Priority
Medium Priority
Low Priority
🎯 Final VerdictLGTM with minor recommendations ✅ This is a solid fix for a real bug. The implementation is clean, well-tested, and should have minimal risk. The concerns raised are edge cases that likely won't affect most users, but documenting them would improve long-term maintainability. Estimated CI Risk: Low - the change is defensive (adds a guard clause) and comprehensive tests should catch regressions. 📝 Pre-Merge Checklist (from CLAUDE.md)Based on the project's requirements:
Note: Please verify the above before merging. The CLAUDE.md guidelines are very explicit about these requirements. Great work on the fix! Let me know if you'd like me to elaborate on any of these points. 🚀 |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/react-on-rails/tests/ClientRenderer.test.ts (1)
205-264: Make the regression test deterministic: clear mocks per test + preferjest.requireMock.beforeEach(() => { + jest.clearAllMocks(); // Clear registries ComponentRegistry.clear(); StoreRegistry.clearHydratedStores(); @@ - const mockHydrateOrRender = require('../src/reactHydrateOrRender.ts').default as jest.Mock; + const mockHydrateOrRender = jest.requireMock('../src/reactHydrateOrRender.ts') + .default as jest.Mock; const callCountAfterFirstRender = mockHydrateOrRender.mock.calls.length;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
packages/react-on-rails/src/ClientRenderer.ts(1 hunks)packages/react-on-rails/tests/ClientRenderer.test.ts(1 hunks)react_on_rails/spec/dummy/app/views/pages/async_page_loaded_test.html.erb(1 hunks)react_on_rails/spec/dummy/client/app/packs/client-bundle.js(2 hunks)react_on_rails/spec/dummy/client/app/startup/AsyncComponent.jsx(1 hunks)react_on_rails/spec/dummy/config/routes.rb(1 hunks)react_on_rails/spec/dummy/e2e/playwright/e2e/react_on_rails/page_loaded_multiple_calls.spec.js(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- react_on_rails/spec/dummy/client/app/startup/AsyncComponent.jsx
- react_on_rails/spec/dummy/e2e/playwright/e2e/react_on_rails/page_loaded_multiple_calls.spec.js
- react_on_rails/spec/dummy/client/app/packs/client-bundle.js
- react_on_rails/spec/dummy/config/routes.rb
- react_on_rails/spec/dummy/app/views/pages/async_page_loaded_test.html.erb
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{rb,js,ts,jsx,tsx,json,yml,yaml,md}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{rb,js,ts,jsx,tsx,json,yml,yaml,md}: ALWAYS runbundle exec rubocopand fix ALL violations before every commit/push
ALWAYS ensure files end with a newline character before committing
Files:
packages/react-on-rails/src/ClientRenderer.tspackages/react-on-rails/tests/ClientRenderer.test.ts
**/*.{js,ts,jsx,tsx,json,css,scss,md}
📄 CodeRabbit inference engine (CLAUDE.md)
ALWAYS let Prettier handle ALL formatting - never manually format code
Files:
packages/react-on-rails/src/ClientRenderer.tspackages/react-on-rails/tests/ClientRenderer.test.ts
**/*.{js,ts,jsx,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Only use
pnpmas the JavaScript package manager, never usenpmoryarn
Files:
packages/react-on-rails/src/ClientRenderer.tspackages/react-on-rails/tests/ClientRenderer.test.ts
🧠 Learnings (6)
📓 Common learnings
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Applies to /CHANGELOG.md : Update `/CHANGELOG.md` for open-source features, bug fixes, breaking changes, deprecations, and performance improvements using format: `[PR 1818](https://github.com/shakacode/react_on_rails/pull/1818) by [username](https://github.com/username)`
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.
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/clientStartup.ts:18-21
Timestamp: 2025-02-13T16:50:47.848Z
Learning: In the react_on_rails module, the `reactOnRailsPageUnloaded` function in clientStartup.ts is intentionally kept private as it's only used internally as a callback for `onPageUnloaded`.
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Applies to spec/dummy/**/*.{spec,test}.{js,ts} : Playwright E2E tests use `cypress-on-rails` gem for Rails integration; use helpers like `app('clean')`, `appEval()`, and `appFactories()` for test setup instead of UI automation
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.
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 is handled in deeper level calls of the React on Rails Pro codebase, so it doesn't need to be validated again in the `rsc_payload_react_component` helper method.
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.
📚 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:
packages/react-on-rails/src/ClientRenderer.tspackages/react-on-rails/tests/ClientRenderer.test.ts
📚 Learning: 2025-02-13T16:50:47.848Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/clientStartup.ts:18-21
Timestamp: 2025-02-13T16:50:47.848Z
Learning: In the react_on_rails module, the `reactOnRailsPageUnloaded` function in clientStartup.ts is intentionally kept private as it's only used internally as a callback for `onPageUnloaded`.
Applied to files:
packages/react-on-rails/src/ClientRenderer.ts
📚 Learning: 2024-12-12T13:07:09.929Z
Learnt from: alexeyr-ci
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/ReactOnRailsRSC.ts:87-87
Timestamp: 2024-12-12T13:07:09.929Z
Learning: When handling errors in 'node_package/src/ReactOnRailsRSC.ts', include the error stack in error messages in development and test environments to aid debugging.
Applied to files:
packages/react-on-rails/src/ClientRenderer.ts
📚 Learning: 2025-02-13T19:09:15.991Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/RSCWebpackLoader.ts:0-0
Timestamp: 2025-02-13T19:09:15.991Z
Learning: In React Server Components webpack loader, using `new Function('return import("react-server-dom-webpack/node-loader")')()` is necessary as a workaround to bypass TypeScript compilation issues with direct dynamic imports.
Applied to files:
packages/react-on-rails/src/ClientRenderer.ts
📚 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:
packages/react-on-rails/src/ClientRenderer.ts
🧬 Code graph analysis (1)
packages/react-on-rails/tests/ClientRenderer.test.ts (1)
packages/react-on-rails/src/ClientRenderer.ts (1)
renderComponent(133-147)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
- GitHub Check: examples (3.4, latest)
- GitHub Check: examples (3.2, minimum)
- GitHub Check: rspec-package-tests (3.4, latest)
- GitHub Check: rspec-package-tests (3.2, minimum)
- GitHub Check: build
- GitHub Check: build-dummy-app-webpack-test-bundles
- GitHub Check: precompile-check
- GitHub Check: pro-lint-js-and-ruby
- GitHub Check: build-dummy-app-webpack-test-bundles
- GitHub Check: check-bundle-size
- GitHub Check: claude-review
🔇 Additional comments (1)
packages/react-on-rails/src/ClientRenderer.ts (1)
96-100: Hydration heuristic still hinges oninnerHTML; please sanity-check against React 19 expectations.Given React 19’s hydration behavior is sensitive, it’s worth verifying
const shouldHydrate = !!domNode.innerHTML;is still the desired “SSR marker” (vs e.g. childNodes checks / an explicit attribute) now that dedupe is in place.
Code Review: Fix hydration mismatch when reactOnRailsPageLoaded() called multiple times✅ Overall AssessmentThis is a well-implemented fix with comprehensive testing. The solution correctly addresses the root cause and includes both unit and E2E tests. Great work! 🎯 Strengths
🔍 Code Quality ObservationsClientRenderer.ts changes (lines 81-113)Positive:
Minor Concern: const sameNode = existing.domNode === domNode && existing.domNode.isConnected;This check compares const sameNode = existing.domNode === domNode && domNode.isConnected;Since they're the same reference, this is functionally identical, just slightly more semantically clear. 🧪 TestingUnit Tests (ClientRenderer.test.ts):
E2E Tests (page_loaded_multiple_calls.spec.js):
Suggestion: Consider adding a test case for rapid successive calls (e.g., calling 📝 Documentation & ChangelogAccording to the project's #### Fixed
[PR 2211](https://github.com/shakacode/react_on_rails/pull/2211) by [justin808](https://github.com/justin808): Fixed hydration mismatch errors when `reactOnRailsPageLoaded()` is called multiple times for asynchronously loaded content. Previously rendered client-side components are now correctly skipped during subsequent calls.🔒 Security Considerations✅ No security concerns identified. The changes are purely internal logic improvements. ⚡ Performance ConsiderationsPositive:
Minor optimization opportunity: 🎨 Code Style✅ Follows project conventions 🐛 Potential Edge Cases to Consider
📋 Action ItemsBefore merging:
Optional enhancements:
🎉 ConclusionThis is a high-quality fix that demonstrates good understanding of React rendering lifecycle and edge case handling. The comprehensive test coverage gives confidence that this won't regress. With the changelog entry added, this is ready to merge! Recommendation: Approve with minor changelog addition |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/react-on-rails/src/ClientRenderer.ts (1)
205-220: Consider extracting the unmount logic to reduce duplication.The unmount pattern (checking
supportsRootApi, thenroot.unmount()vsunmountComponentAtNode) is duplicated between here and lines 96-105. While not critical, extracting a helper function would improve maintainability.+function unmountRoot({ root, domNode }: { root: RenderReturnType; domNode: Element }): void { + if (supportsRootApi && root && typeof root === 'object' && 'unmount' in root) { + root.unmount(); + } else { + unmountComponentAtNode(domNode); + } +} + function unmountAllComponents(): void { renderedRoots.forEach(({ root, domNode }) => { try { - if (supportsRootApi && root && typeof root === 'object' && 'unmount' in root) { - // React 18+ Root API - root.unmount(); - } else { - // React 16-17 legacy API - unmountComponentAtNode(domNode); - } + unmountRoot({ root, domNode }); } catch (error) { console.error('Error unmounting component:', error); } }); renderedRoots.clear(); }Then reuse
unmountRootin the renderElement cleanup block as well.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
packages/react-on-rails/src/ClientRenderer.ts(1 hunks)packages/react-on-rails/tests/ClientRenderer.test.ts(1 hunks)react_on_rails/spec/dummy/app/views/pages/async_page_loaded_test.html.erb(1 hunks)react_on_rails/spec/dummy/e2e/playwright/e2e/react_on_rails/page_loaded_multiple_calls.spec.js(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- react_on_rails/spec/dummy/e2e/playwright/e2e/react_on_rails/page_loaded_multiple_calls.spec.js
- packages/react-on-rails/tests/ClientRenderer.test.ts
- react_on_rails/spec/dummy/app/views/pages/async_page_loaded_test.html.erb
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{rb,js,ts,jsx,tsx,json,yml,yaml,md}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{rb,js,ts,jsx,tsx,json,yml,yaml,md}: ALWAYS runbundle exec rubocopand fix ALL violations before every commit/push
ALWAYS ensure files end with a newline character before committing
Files:
packages/react-on-rails/src/ClientRenderer.ts
**/*.{js,ts,jsx,tsx,json,css,scss,md}
📄 CodeRabbit inference engine (CLAUDE.md)
ALWAYS let Prettier handle ALL formatting - never manually format code
Files:
packages/react-on-rails/src/ClientRenderer.ts
**/*.{js,ts,jsx,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Only use
pnpmas the JavaScript package manager, never usenpmoryarn
Files:
packages/react-on-rails/src/ClientRenderer.ts
🧠 Learnings (6)
📓 Common learnings
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Applies to /CHANGELOG.md : Update `/CHANGELOG.md` for open-source features, bug fixes, breaking changes, deprecations, and performance improvements using format: `[PR 1818](https://github.com/shakacode/react_on_rails/pull/1818) by [username](https://github.com/username)`
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.
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 is handled in deeper level calls of the React on Rails Pro codebase, so it doesn't need to be validated again in the `rsc_payload_react_component` helper method.
Learnt from: alexeyr-ci
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/ReactOnRailsRSC.ts:87-87
Timestamp: 2024-12-12T13:07:09.929Z
Learning: When handling errors in 'node_package/src/ReactOnRailsRSC.ts', include the error stack in error messages in development and test environments to aid debugging.
📚 Learning: 2025-02-13T16:50:47.848Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/clientStartup.ts:18-21
Timestamp: 2025-02-13T16:50:47.848Z
Learning: In the react_on_rails module, the `reactOnRailsPageUnloaded` function in clientStartup.ts is intentionally kept private as it's only used internally as a callback for `onPageUnloaded`.
Applied to files:
packages/react-on-rails/src/ClientRenderer.ts
📚 Learning: 2024-12-12T13:07:09.929Z
Learnt from: alexeyr-ci
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/ReactOnRailsRSC.ts:87-87
Timestamp: 2024-12-12T13:07:09.929Z
Learning: When handling errors in 'node_package/src/ReactOnRailsRSC.ts', include the error stack in error messages in development and test environments to aid debugging.
Applied to files:
packages/react-on-rails/src/ClientRenderer.ts
📚 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:
packages/react-on-rails/src/ClientRenderer.ts
📚 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:
packages/react-on-rails/src/ClientRenderer.ts
📚 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:
packages/react-on-rails/src/ClientRenderer.ts
🧬 Code graph analysis (1)
packages/react-on-rails/src/ClientRenderer.ts (1)
packages/react-on-rails-pro-node-renderer/src/shared/tracing.ts (1)
trace(87-89)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
- GitHub Check: build-dummy-app-webpack-test-bundles (3.4, 22, latest)
- GitHub Check: examples (3.2, minimum)
- GitHub Check: examples (3.4, latest)
- GitHub Check: rspec-package-tests (3.4, latest)
- GitHub Check: rspec-package-tests (3.2, minimum)
- GitHub Check: pro-lint-js-and-ruby
- GitHub Check: build
- GitHub Check: build-dummy-app-webpack-test-bundles
- GitHub Check: build-dummy-app-webpack-test-bundles
- GitHub Check: check-bundle-size
- GitHub Check: claude-review
🔇 Additional comments (4)
packages/react-on-rails/src/ClientRenderer.ts (4)
14-15: LGTM! Good approach to track rendered roots.The
renderedRootsMap correctly stores both the root and domNode, enabling proper cleanup and the "same node" comparison needed to prevent hydration issues.
81-113: LGTM! Well-structured handling of already-rendered and replaced components.The logic correctly:
- Skips re-rendering when the same DOM node is still connected
- Cleans up the old root when a node with the same ID was replaced
- Handles both React 18+ Root API and legacy unmount gracefully
- Swallows unmount errors appropriately since the old DOM is already gone
This properly addresses the hydration mismatch issue described in #2210.
120-142: Good documentation of the fix rationale.The comment on lines 120-122 clearly explains why the
innerHTMLcheck now works correctly - it's the combination of the skip logic above that makes this check accurate. Previously client-rendered content would haveinnerHTMLand incorrectly trigger hydration; now those cases are caught by therenderedRootscheck.
222-223: LGTM! Proper cleanup registration.Registering
unmountAllComponentsviaonPageUnloadedensures React roots are properly cleaned up on page navigation, preventing memory leaks.
| // Check if this component was already rendered by a previous call | ||
| // This prevents hydration errors when reactOnRailsPageLoaded() is called multiple times | ||
| // (e.g., for asynchronously loaded content) | ||
| const existing = renderedRoots.get(domNodeId); | ||
| if (existing) { | ||
| // Only skip if it's the exact same DOM node and it's still connected to the document | ||
| const sameNode = existing.domNode === domNode && existing.domNode.isConnected; | ||
| if (sameNode) { | ||
| if (trace) { | ||
| console.log(`Skipping already rendered component: ${name} (dom id: ${domNodeId})`); | ||
| } | ||
| return; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
# Check if CHANGELOG.md mentions this PR or the related issue
rg -n "2211|2210" CHANGELOG.md || echo "No mention of PR #2211 or issue #2210 found in CHANGELOG.md"Repository: shakacode/react_on_rails
Length of output: 127
Add entry to CHANGELOG.md for this bug fix.
CHANGELOG.md must be updated for bug fixes using the format: [PR 2211](https://github.com/shakacode/react_on_rails/pull/2211) by [justin808](https://github.com/justin808)
🤖 Prompt for AI Agents
In packages/react-on-rails/src/ClientRenderer.ts around lines 81 to 92, the PR
added a guard to skip re-rendering already rendered components; update
CHANGELOG.md to record this bug fix by adding an entry in the required format:
`[PR 2211](https://github.com/shakacode/react_on_rails/pull/2211) by
[justin808](https://github.com/justin808)` under the appropriate unreleased or
version section, ensuring the wording indicates it fixes skipping duplicate
renders/hydration errors and follows existing changelog entry style.
|
@claude review this PR |
|
Claude finished @justin808's task in 2m 7s —— View job PR Review for #2211: Fix hydration mismatch when reactOnRailsPageLoaded() called multiple times✅ Overall AssessmentVerdict: LGTM with one minor action item (CHANGELOG entry) This is an excellent fix for issue #2210. The implementation is surgical, well-tested, and handles edge cases thoughtfully. The root cause analysis is spot-on, and the solution is minimal yet comprehensive. 🎯 Strengths1. Correct Root Cause AnalysisThe PR correctly identifies that calling 2. Minimal, Surgical Implementation (ClientRenderer.ts:81-113)
Key logic (lines 84-93): const existing = renderedRoots.get(domNodeId);
if (existing) {
const sameNode = existing.domNode === domNode && existing.domNode.isConnected;
if (sameNode) {
// Skip re-rendering same component
return;
}
// DOM node was replaced - clean up old root and continue
}3. Excellent Edge Case Handling (lines 94-113)The fix intelligently handles the replaced DOM node scenario that CodeRabbit identified:
4. Comprehensive Test CoverageUnit Tests (ClientRenderer.test.ts:206-311):
E2E Tests (page_loaded_multiple_calls.spec.js):
Test Page (async_page_loaded_test.html.erb):
5. Clear Documentation
🔍 Code Quality ObservationsClientRenderer.tsLine 87: const sameNode = existing.domNode === domNode && existing.domNode.isConnected;Smart use of Lines 96-105: Dual Unmount Support if (supportsRootApi && existing.root && typeof existing.root === 'object' && 'unmount' in existing.root) {
existing.root.unmount();
} else {
unmountComponentAtNode(existing.domNode);
}Proper handling of both React 18+ Root API and legacy unmount. The type guards are appropriate. Line 123: Hydration Check Now Correct const shouldHydrate = !!domNode.innerHTML;The comment above (lines 120-122) correctly explains why this now works: previously rendered components are skipped above, so Unit TestsMinor: Mock Clearing (line 258) const mockHydrateOrRender = require('../src/reactHydrateOrRender.ts').default as jest.Mock;Consider using Call Count Verification (line 265) expect(mockHydrateOrRender.mock.calls.length).toBe(callCountAfterFirstRender);This correctly verifies the mock wasn't called again. Could be slightly more explicit with: expect(mockHydrateOrRender).not.toHaveBeenCalledTimes(callCountAfterFirstRender + 1);But current approach is fine. E2E TestsConsole Error Filtering (lines 59-66) if (text.toLowerCase().includes('hydrat') ||
text.toLowerCase().includes('did not match') ||
text.toLowerCase().includes('mismatch'))Good filtering for hydration-specific errors. This prevents false positives from other console errors. Test Robustness
|
Code Review: Fix hydration mismatch when reactOnRailsPageLoaded() called multiple timesOverall AssessmentThis PR effectively addresses issue #2210 with a well-thought-out solution. The fix is elegant, properly tested, and includes comprehensive E2E coverage. Core Fix AnalysisThe Problem: When reactOnRailsPageLoaded() is called multiple times, previously client-rendered components were being re-hydrated because !!domNode.innerHTML returned true, causing hydration mismatch errors in React 19. The Solution: Track rendered roots and skip re-rendering components already mounted, while handling DOM node replacement edge cases. Strengths
RecommendationsHigh Priority:
Medium Priority:
Low Priority:
Test CoverageExcellent test coverage with both unit tests and comprehensive E2E tests covering initial load, multiple calls, multiple additions, DOM replacement, and multiple replacements. Security & PerformanceNo security concerns. Performance is good with O(1) Map lookups and early returns. VerdictApprove with minor recommendations This is high-quality work that correctly fixes the root cause with comprehensive testing. Recommendations are mostly for documentation and edge case coverage, not blocking issues. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (7)
react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook (2)
9-16: Avoid brittle “4 levels up” repo-root detection (prefer self-validating resolution).Hardcoding
repo_root = File.expand_path("../../../..", __dir__)will silently break if the dummy app moves (or the repo is vendored differently). Consider resolving relative to this file by searching upward for a repo marker (e.g.,.git,pnpm-workspace.yaml, etc.) or at least adding a sanity check thatreact_on_rails/spec/support/...exists before assuming repo_root, so failures are clearer.
14-19: Minor: log/loader polish (emoji +load).
- The “❌” in
warncan be noisy/problematic in some CI log pipelines; plain ASCII might be safer.- If there’s any chance this hook is evaluated multiple times in the same Ruby process,
require shared_hookis safer thanloadto avoid double execution.react_on_rails/spec/dummy/e2e/playwright/e2e/react_on_rails/page_loaded_multiple_calls.spec.js (1)
197-197: Consider reducing the hard-coded timeout.The 200ms timeout using
page.waitForTimeout(200)is a hard-coded delay. While it may work, consider using event-driven waits or shorter timeouts if possible.If the DOM replacement is synchronous or triggers observable changes, you could replace this with:
- await page.waitForTimeout(200); + await page.waitForFunction(() => window.replacementComplete === true);Or set a flag in the replacement logic and wait for it. This would make the test more reliable and faster.
bin/ci-local (1)
210-216: Dummy app setup: consider pinning install behavior for reproducibility
For local CI parity, it may be worth using the repo’s standard pnpm flags (if any) like--frozen-lockfile(when the intent is “CI-like”) vs permissive installs.react_on_rails/spec/dummy/app/views/pages/manual_render_test.html.erb (3)
65-73: Restoreconsole.erroron cleanup to avoid cross-test bleed
Since this page is intended for Playwright runs, consider restoringconsole.error = originalErroronbeforeunload/pagehide(or after each scenario) so other dummy pages/tests aren’t affected if they share the same context.
165-174: Reduce flakiness: replace fixedsetTimeout(100)with a bounded poll / MutationObserver
A hard 100ms delay is a common CI flake source. Consider polling until rendered (with an upper bound like 2s) or using aMutationObserveron#components-containerand resolving when the node content changes.
168-172: Brittle assertion: checkinginnerHTMLsubstring may break on markup changes
If the component’s markup/class changes, this test will fail even if the behavior is correct. Prefer asserting on a stable test id/attribute rendered byManualRenderComponent(e.g.,data-testid="manual-render-component").
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
bin/ci-local(6 hunks)bin/ci-rerun-failures(3 hunks)bin/ci-switch-config(2 hunks)react_on_rails/spec/dummy/Procfile.dev(1 hunks)react_on_rails/spec/dummy/app/views/pages/manual_render_test.html.erb(1 hunks)react_on_rails/spec/dummy/bin/setup(1 hunks)react_on_rails/spec/dummy/client/app/packs/client-bundle.js(2 hunks)react_on_rails/spec/dummy/client/app/startup/ManualRenderComponent.jsx(1 hunks)react_on_rails/spec/dummy/config/routes.rb(1 hunks)react_on_rails/spec/dummy/e2e/playwright/e2e/react_on_rails/page_loaded_multiple_calls.spec.js(1 hunks)react_on_rails_pro/spec/dummy/Procfile.static(1 hunks)react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook(1 hunks)react_on_rails_pro/spec/execjs-compatible-dummy/bin/setup(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- react_on_rails/spec/dummy/config/routes.rb
- react_on_rails/spec/dummy/client/app/packs/client-bundle.js
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{rb,js,ts,jsx,tsx,json,yml,yaml,md}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{rb,js,ts,jsx,tsx,json,yml,yaml,md}: ALWAYS runbundle exec rubocopand fix ALL violations before every commit/push
ALWAYS ensure files end with a newline character before committing
Files:
react_on_rails/spec/dummy/client/app/startup/ManualRenderComponent.jsxreact_on_rails/spec/dummy/e2e/playwright/e2e/react_on_rails/page_loaded_multiple_calls.spec.js
**/*.{js,ts,jsx,tsx,json,css,scss,md}
📄 CodeRabbit inference engine (CLAUDE.md)
ALWAYS let Prettier handle ALL formatting - never manually format code
Files:
react_on_rails/spec/dummy/client/app/startup/ManualRenderComponent.jsxreact_on_rails/spec/dummy/e2e/playwright/e2e/react_on_rails/page_loaded_multiple_calls.spec.js
**/*.{js,ts,jsx,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Only use
pnpmas the JavaScript package manager, never usenpmoryarn
Files:
react_on_rails/spec/dummy/client/app/startup/ManualRenderComponent.jsxreact_on_rails/spec/dummy/e2e/playwright/e2e/react_on_rails/page_loaded_multiple_calls.spec.js
🧠 Learnings (27)
📓 Common learnings
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Applies to /CHANGELOG.md : Update `/CHANGELOG.md` for open-source features, bug fixes, breaking changes, deprecations, and performance improvements using format: `[PR 1818](https://github.com/shakacode/react_on_rails/pull/1818) by [username](https://github.com/username)`
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.
📚 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:
react_on_rails/spec/dummy/client/app/startup/ManualRenderComponent.jsxreact_on_rails/spec/dummy/Procfile.devreact_on_rails/spec/dummy/bin/setupreact_on_rails_pro/spec/dummy/Procfile.staticreact_on_rails_pro/spec/execjs-compatible-dummy/bin/setup
📚 Learning: 2024-10-08T20:53:47.076Z
Learnt from: theforestvn88
Repo: shakacode/react_on_rails PR: 1620
File: spec/dummy/client/app/startup/HelloTurboStream.jsx:3-3
Timestamp: 2024-10-08T20:53:47.076Z
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:
react_on_rails/spec/dummy/client/app/startup/ManualRenderComponent.jsxreact_on_rails_pro/spec/dummy/Procfile.staticreact_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
📚 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:
react_on_rails/spec/dummy/client/app/startup/ManualRenderComponent.jsxreact_on_rails/spec/dummy/Procfile.devreact_on_rails/spec/dummy/bin/setupreact_on_rails_pro/spec/dummy/Procfile.staticreact_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hookreact_on_rails_pro/spec/execjs-compatible-dummy/bin/setup
📚 Learning: 2025-02-13T19:09:15.991Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/RSCWebpackLoader.ts:0-0
Timestamp: 2025-02-13T19:09:15.991Z
Learning: In React Server Components webpack loader, using `new Function('return import("react-server-dom-webpack/node-loader")')()` is necessary as a workaround to bypass TypeScript compilation issues with direct dynamic imports.
Applied to files:
react_on_rails/spec/dummy/client/app/startup/ManualRenderComponent.jsx
📚 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:
react_on_rails/spec/dummy/client/app/startup/ManualRenderComponent.jsxreact_on_rails/spec/dummy/Procfile.devreact_on_rails/spec/dummy/bin/setupreact_on_rails_pro/spec/dummy/Procfile.staticreact_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hookreact_on_rails_pro/spec/execjs-compatible-dummy/bin/setup
📚 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:
react_on_rails/spec/dummy/client/app/startup/ManualRenderComponent.jsxreact_on_rails/spec/dummy/app/views/pages/manual_render_test.html.erbbin/ci-localreact_on_rails/spec/dummy/e2e/playwright/e2e/react_on_rails/page_loaded_multiple_calls.spec.js
📚 Learning: 2025-12-10T09:27:45.105Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Applies to /CHANGELOG.md : Update `/CHANGELOG.md` for open-source features, bug fixes, breaking changes, deprecations, and performance improvements using format: `[PR 1818](https://github.com/shakacode/react_on_rails/pull/1818) by [username](https://github.com/username)`
Applied to files:
react_on_rails/spec/dummy/client/app/startup/ManualRenderComponent.jsxreact_on_rails/spec/dummy/Procfile.devreact_on_rails_pro/spec/dummy/Procfile.staticreact_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
📚 Learning: 2025-02-13T16:50:47.848Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/clientStartup.ts:18-21
Timestamp: 2025-02-13T16:50:47.848Z
Learning: In the react_on_rails module, the `reactOnRailsPageUnloaded` function in clientStartup.ts is intentionally kept private as it's only used internally as a callback for `onPageUnloaded`.
Applied to files:
react_on_rails/spec/dummy/client/app/startup/ManualRenderComponent.jsxreact_on_rails/spec/dummy/app/views/pages/manual_render_test.html.erb
📚 Learning: 2025-12-10T09:27:45.105Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Applies to react_on_rails_pro/**/*.{js,ts,jsx,tsx,json,css,scss} : The `react_on_rails_pro/` directory has its own Prettier/ESLint configuration separate from the root; CI lints both directories separately
Applied to files:
react_on_rails/spec/dummy/Procfile.devreact_on_rails_pro/spec/dummy/Procfile.staticreact_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hookreact_on_rails_pro/spec/execjs-compatible-dummy/bin/setupbin/ci-local
📚 Learning: 2025-12-10T09:27:45.105Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Applies to spec/dummy/**/*.{spec,test}.{js,ts} : Playwright E2E tests use `cypress-on-rails` gem for Rails integration; use helpers like `app('clean')`, `appEval()`, and `appFactories()` for test setup instead of UI automation
Applied to files:
react_on_rails/spec/dummy/Procfile.devreact_on_rails/spec/dummy/bin/setupreact_on_rails_pro/spec/dummy/Procfile.staticreact_on_rails/spec/dummy/app/views/pages/manual_render_test.html.erbbin/ci-rerun-failuresreact_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hookreact_on_rails_pro/spec/execjs-compatible-dummy/bin/setupbin/ci-localreact_on_rails/spec/dummy/e2e/playwright/e2e/react_on_rails/page_loaded_multiple_calls.spec.js
📚 Learning: 2025-01-23T18:20:45.824Z
Learnt from: alexeyr-ci
Repo: shakacode/react_on_rails PR: 1687
File: spec/dummy/package.json:0-0
Timestamp: 2025-01-23T18:20:45.824Z
Learning: When adding or updating dependencies in spec/dummy/package.json, maintain version consistency with other package.json files in the codebase to avoid potential version conflicts.
Applied to files:
react_on_rails/spec/dummy/Procfile.devreact_on_rails/spec/dummy/bin/setupreact_on_rails_pro/spec/dummy/Procfile.staticreact_on_rails_pro/spec/execjs-compatible-dummy/bin/setupbin/ci-localbin/ci-switch-config
📚 Learning: 2025-12-10T09:27:45.105Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Applies to spec/dummy/e2e/playwright/app_commands/**/*.rb : Create custom app commands for Playwright E2E tests in this directory using the `command` syntax provided by `cypress-on-rails` gem
Applied to files:
react_on_rails/spec/dummy/Procfile.devreact_on_rails/spec/dummy/bin/setupreact_on_rails_pro/spec/dummy/Procfile.staticbin/ci-rerun-failuresreact_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hookreact_on_rails/spec/dummy/e2e/playwright/e2e/react_on_rails/page_loaded_multiple_calls.spec.js
📚 Learning: 2025-12-10T09:27:45.105Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Monorepo contains separate open-source and Pro packages; changes affecting both require updating both `/CHANGELOG.md` and `/CHANGELOG_PRO.md`
Applied to files:
react_on_rails/spec/dummy/Procfile.devbin/ci-local
📚 Learning: 2025-12-10T09:27:45.105Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Applies to lib/react_on_rails/**/*.rb : Create corresponding RBS signature files in `sig/react_on_rails/` for new Ruby files and add them to Steepfile for type checking
Applied to files:
react_on_rails/spec/dummy/Procfile.devreact_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
📚 Learning: 2025-12-10T09:27:45.105Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Applies to package.json,pnpm-lock.yaml : Test build scripts after modifying package.json or dependencies: run `pnpm run prepack` and `pnpm run yalc.publish` to verify build pipeline
Applied to files:
react_on_rails/spec/dummy/Procfile.devreact_on_rails_pro/spec/dummy/Procfile.staticbin/ci-rerun-failuresreact_on_rails_pro/spec/execjs-compatible-dummy/bin/setupbin/ci-localbin/ci-switch-config
📚 Learning: 2025-12-10T09:27:45.105Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Applies to **/*.{js,ts,jsx,tsx} : Only use `pnpm` as the JavaScript package manager, never use `npm` or `yarn`
Applied to files:
react_on_rails/spec/dummy/Procfile.devreact_on_rails/spec/dummy/bin/setupbin/ci-rerun-failuresreact_on_rails_pro/spec/execjs-compatible-dummy/bin/setupbin/ci-localbin/ci-switch-config
📚 Learning: 2025-12-10T09:27:45.105Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Applies to spec/dummy/config/webpack/**/*.js : When debugging Webpack configuration issues, create temporary debug scripts to inspect rules and loaders; clean them up before committing
Applied to files:
react_on_rails/spec/dummy/Procfile.devreact_on_rails/spec/dummy/bin/setup
📚 Learning: 2025-12-10T09:27:45.105Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Applies to spec/dummy/**/*.{js,ts} : Install Playwright browsers via `cd spec/dummy && pnpm playwright install --with-deps` before running E2E tests
Applied to files:
react_on_rails/spec/dummy/bin/setupbin/ci-rerun-failuresreact_on_rails_pro/spec/execjs-compatible-dummy/bin/setupbin/ci-localbin/ci-switch-configreact_on_rails/spec/dummy/e2e/playwright/e2e/react_on_rails/page_loaded_multiple_calls.spec.js
📚 Learning: 2025-12-10T09:27:45.105Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Applies to lib/generators/react_on_rails/**/*.rb : Generators run in host app context, not engine context; do not assume host app structure (e.g., `app/javascript/` may not exist in older apps)
Applied to files:
react_on_rails/spec/dummy/bin/setupreact_on_rails_pro/spec/dummy/Procfile.staticreact_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hookreact_on_rails_pro/spec/execjs-compatible-dummy/bin/setup
📚 Learning: 2025-12-10T09:27:45.105Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Applies to **/*.{rb,js,ts,jsx,tsx,json,yml,yaml,md} : ALWAYS run `bundle exec rubocop` and fix ALL violations before every commit/push
Applied to files:
react_on_rails/spec/dummy/bin/setupreact_on_rails_pro/spec/dummy/Procfile.staticbin/ci-rerun-failuresreact_on_rails_pro/spec/execjs-compatible-dummy/bin/setupbin/ci-local
📚 Learning: 2025-09-29T14:45:42.687Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1833
File: lib/react_on_rails/dev/process_manager.rb:72-83
Timestamp: 2025-09-29T14:45:42.687Z
Learning: In Ruby bundler contexts, when bundler intercepts system commands for executables not in the Gemfile, both version checks (like `system("foreman", "--version")`) and execution commands (like `system("foreman", "start", ...)`) fail equally, both returning false. This means availability checks using version flags accurately reflect whether execution commands will work in the current bundler context.
Applied to files:
react_on_rails/spec/dummy/bin/setupreact_on_rails_pro/spec/execjs-compatible-dummy/bin/setup
📚 Learning: 2025-12-10T09:27:45.105Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Applies to lib/react_on_rails/**/*.rb : Engine code has limited access to host application; use `Rails.root` for host app paths and `ReactOnRails.configure` for engine configuration
Applied to files:
react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
📚 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:
react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hookbin/ci-local
📚 Learning: 2025-12-10T09:27:45.105Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Applies to **/*.rbs : Add RBS signature files for all new Ruby files in `lib/react_on_rails/` and register them in Steepfile; validate with `bundle exec rake rbs:validate`
Applied to files:
react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
📚 Learning: 2025-12-10T09:27:45.105Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-10T09:27:45.105Z
Learning: Applies to **/.github/workflows/*.yml : Changes to CI workflows require comprehensive local testing BEFORE pushing to avoid CI failure cycles
Applied to files:
bin/ci-local
📚 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:
bin/ci-local
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
- GitHub Check: dummy-app-integration-tests (3.2, 20, minimum)
- GitHub Check: dummy-app-integration-tests (3.4, 22, latest)
- GitHub Check: rspec-package-tests (3.4, latest)
- GitHub Check: rspec-package-tests (3.2, minimum)
- GitHub Check: examples (3.2, minimum)
- GitHub Check: examples (3.4, latest)
- GitHub Check: precompile-check
- GitHub Check: build-dummy-app-webpack-test-bundles
- GitHub Check: build
- GitHub Check: build-dummy-app-webpack-test-bundles
- GitHub Check: pro-lint-js-and-ruby
- GitHub Check: check-bundle-size
- GitHub Check: claude-review
🔇 Additional comments (23)
react_on_rails/spec/dummy/bin/setup (1)
21-21: LGTM: Consistent with PNPM migration.The change from
bin/yarntopnpm installaligns with the repository-wide migration to PNPM as the JavaScript package manager.As per coding guidelines, ensure this aligns with the broader migration across all setup scripts and CI tooling.
react_on_rails_pro/spec/execjs-compatible-dummy/bin/setup (1)
23-23: LGTM: Correct flag preservation during migration.The migration to
pnpm install --frozen-lockfilecorrectly preserves the lockfile constraint, ensuring deterministic dependency installation in the test environment.react_on_rails/spec/dummy/Procfile.dev (1)
8-8: LGTM: Procfile updated for PNPM.The ReScript build command correctly uses
pnpminstead ofyarn, consistent with the repository-wide migration.react_on_rails/spec/dummy/client/app/startup/ManualRenderComponent.jsx (1)
1-40: LGTM: Well-structured test component with clear purpose.The component is appropriately designed for testing hydration behavior:
- PropTypes validation is correct for the required
nameprop- Inline styles intentionally use camelCase to detect hydration mismatches (kebab-case vs camelCase CSS properties)
- Clear documentation explains the testing purpose and scope
data-testidattribute supports E2E test assertionsreact_on_rails_pro/spec/dummy/Procfile.static (1)
5-8: LGTM: Procfile commands migrated to PNPM.Both asset build and node-renderer commands correctly use
pnpm run, consistent with the repository-wide migration from Yarn.bin/ci-rerun-failures (3)
191-197: LGTM: CI commands migrated to PNPM.All CI job commands correctly use PNPM:
- Linting uses
pnpm run eslintandpnpm start format.listDifferent- Testing uses
pnpm testThis ensures consistency with the repository-wide PNPM migration.
260-260: LGTM: Help text updated for PNPM.The echo message correctly reflects the updated
pnpm testcommand, maintaining consistency with the actual commands executed.
292-292: LGTM: Dependency installation uses PNPM.The dependency check correctly uses
pnpm installfor both bundle and node package installation.react_on_rails/spec/dummy/e2e/playwright/e2e/react_on_rails/page_loaded_multiple_calls.spec.js (5)
24-50: LGTM: Initial load test is well-structured.The test correctly:
- Monitors console errors for hydration-related messages
- Waits for network idle before assertions
- Verifies component visibility and content
- Asserts no hydration errors occurred
52-108: LGTM: Multiple call test validates the fix.This test directly addresses issue #2210 by:
- Verifying initial render
- Triggering
reactOnRailsPageLoaded()via button click- Confirming new component renders without hydration errors
- Asserting existing component remains functional
The comprehensive console monitoring (hydration, mismatch, "did not match") ensures proper detection of React 19 hydration issues.
110-155: LGTM: Multiple additions test provides thorough coverage.The test validates that multiple successive calls to
reactOnRailsPageLoaded()work correctly by:
- Adding 3 components dynamically
- Verifying each component renders with correct content
- Asserting total component count
- Confirming no hydration errors across all additions
173-213: LGTM: DOM replacement edge case is well-tested.This test addresses the CodeRabbit-identified edge case where a DOM node is replaced with a new element using the same ID. The test correctly:
- Verifies initial render
- Triggers DOM node replacement
- Confirms re-rendering to the new node
- Validates success indicators
This ensures the fix doesn't incorrectly skip rendering when the DOM node reference changes.
215-238: LGTM: Multiple replacements test ensures stability.The test validates that multiple DOM node replacements work correctly, ensuring the component tracking logic handles repeated replacements without issues.
bin/ci-switch-config (4)
270-275: LGTM: Lockfile handling correct for minimum config.The script correctly:
- Checks for and removes
pnpm-lock.yamlinstead ofyarn.lock- Uses
pnpm install --no-frozen-lockfileto allow dependency downgradesThis is appropriate for switching to the minimum dependency configuration.
280-284: LGTM: spec/dummy lockfile handling consistent.The spec/dummy directory lockfile handling mirrors the root directory approach, correctly using
pnpm install --no-frozen-lockfilefor the minimum configuration.
411-416: LGTM: Lockfile handling correct for latest config.The script correctly:
- Checks for and removes
pnpm-lock.yaml- Uses
pnpm install --frozen-lockfileto ensure exact dependency versionsThis is appropriate for restoring to the latest configuration with deterministic builds.
421-425: LGTM: spec/dummy restoration uses frozen lockfile.The spec/dummy directory correctly uses
pnpm install --frozen-lockfilefor the latest configuration, ensuring deterministic dependency installation.bin/ci-local (5)
156-161: Confirm Pro install uses the intended pnpm mode (workspace vs standalone)
pnpm installinsidereact_on_rails_pro/will behave differently depending on whether the repo is configured as a pnpm workspace and how the Pro package is linked. Please verify this succeeds from a clean clone (nonode_modules/, no pnpm store primed).
170-174: Good: hard-stop when root dependency install fails
Failing fast here prevents running partial CI with missing gems or JS deps.
182-185: Verifypnpm start format.listDifferentis actually wired the same as the prior yarn invocation
pnpm start <args>forwards args to thestartscript, but this assumesstartis the right entrypoint forformat.listDifferentin this repo (often this isnps-driven). Ifformat.listDifferentis an nps task, consider calling it explicitly (e.g.,pnpm run nps format.listDifferent) for clarity and parity.
189-190: Verifypnpm testtargets the same suite as CI expects
Depending onpackage.json,pnpm testmay run a different subset than the previous yarn command(s). Worth confirming this matches CI.
244-247: Pro checks: validate thepnpm run nps <task>command names
These will fail ifnpsisn’t defined as a script alias or if task names differ between root and Pro. Please confirm from a clean state that each command resolves (eslint/format/check-typescript/test). Based on learnings, Pro has separate lint/format config, so it’s good that you run these fromreact_on_rails_pro/.Also applies to: 256-258
react_on_rails/spec/dummy/app/views/pages/manual_render_test.html.erb (1)
94-129: Nice: DOM construction mirrors react_on_rails client-only markup
This is a solid way to reproduce the “async inserted component +reactOnRailsPageLoaded()” path without relying on server rendering.
…times (#2210) When reactOnRailsPageLoaded() is called multiple times (e.g., for asynchronously loaded content), previously rendered client-side components were being re-hydrated, causing hydration mismatch errors in React 19. The fix: - Track rendered components in renderedRoots Map (already existed for cleanup) - Skip rendering if component is already tracked, preventing re-hydration - This ensures only new components are rendered on subsequent calls Includes: - Unit test to verify the fix - E2E Playwright test with test page in dummy app - AsyncComponent for testing the scenario Closes #2210 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
Address CodeRabbit feedback: the previous guard would skip rendering even when the DOM node with the same ID was replaced (e.g., via async HTML injection). This could cause legitimate renders to be skipped and leak old React roots. Now the guard: - Checks if the stored DOM node is the same instance AND still connected - If the node was replaced, unmounts the old root and removes the entry - Only skips rendering when it's truly the same connected node Added unit test for the "same id, new node" scenario. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add "Replace Component" button to test page that demonstrates the CodeRabbit edge case: when a DOM node is removed and recreated with the same ID, the component should still render to the new node. Added E2E tests: - Test that component renders to replaced DOM node - Test multiple consecutive DOM replacements work correctly 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
The term "async" was confusing as it suggested Pro's async hydration feature. This test is for the core package's manual rendering API (reactOnRailsPageLoaded called multiple times), not Pro's async hydration feature. Changes: - Renamed AsyncComponent.jsx to ManualRenderComponent.jsx - Renamed async_page_loaded_test route/view to manual_render_test - Updated data-testid from "async-component" to "manual-render-component" - Added clarifying notes in component and test file comments 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
|
@claude review this PR. |
|
Claude finished @justin808's task in 3m 6s —— View job PR Review for #2211: Fix hydration mismatch when reactOnRailsPageLoaded() called multiple times✅ Overall AssessmentVerdict: Excellent fix - LGTM with one required action (CHANGELOG entry) This PR provides a clean, well-tested solution to issue #2210. The implementation is minimal yet comprehensive, with excellent test coverage and thoughtful handling of edge cases. 🎯 Core Fix AnalysisThe Problem (Lines 81-92 in ClientRenderer.ts)When
The SolutionBrilliant use of existing infrastructure: The fix leverages the existing
✨ Strengths1. Minimal Code FootprintOnly 37 lines added to production code, with zero breaking changes. The fix is surgical and focused. 2. Smart Edge Case HandlingThe CodeRabbit-identified edge case (DOM node replacement with same ID) is brilliantly handled:
3. Excellent Test CoverageUnit Tests (ClientRenderer.test.ts:206-311):
E2E Tests (page_loaded_multiple_calls.spec.js):
Manual Test Page (manual_render_test.html.erb):
4. Performance Benefits
5. Clean Architecture
🔍 Code Quality ObservationsClientRenderer.tsLine 87: Smart const sameNode = existing.domNode === domNode && existing.domNode.isConnected;Excellent use of Lines 96-105: Dual Unmount Support if (supportsRootApi && existing.root && typeof existing.root === 'object' && 'unmount' in existing.root) {
existing.root.unmount();
} else {
unmountComponentAtNode(existing.domNode);
}Proper handling of both React 18+ Root API and legacy unmount. The type guards are appropriate. Lines 106-111: Graceful Error Handling catch (unmountError) {
// Ignore unmount errors for replaced nodes
if (trace) {
console.log(`Error unmounting replaced component: ${name}`, unmountError);
}
}Smart to swallow unmount errors here - if the DOM node was replaced, unmounting the old node may legitimately fail, and we want to proceed with rendering to the new node. Lines 120-122: Updated Comment Unit TestsLine 258: Mock Retrieval Pattern const mockHydrateOrRender = require('../src/reactHydrateOrRender.ts').default as jest.Mock;This works but could be slightly cleaner with Line 265: Call Count Verification expect(mockHydrateOrRender.mock.calls.length).toBe(callCountAfterFirstRender);This correctly verifies the mock wasn't called again. Could be slightly more explicit with E2E TestsLines 62-68: Comprehensive Error Filtering if (text.toLowerCase().includes('hydrat') ||
text.toLowerCase().includes('did not match') ||
text.toLowerCase().includes('mismatch'))Excellent filtering for hydration-specific errors. This prevents false positives from other console errors. Line 197: Hard-coded Timeout await page.waitForTimeout(200);This is a minor flake risk. Consider using
|
941d195 to
df482fc
Compare
Code Review - PR #2211Overall Assessment: ✅ LGTM with minor suggestions This is a well-thought-out fix for Issue #2210. The implementation correctly addresses the hydration mismatch problem while also handling the edge case of replaced DOM nodes. ✅ Strengths1. Correct Root Cause AnalysisThe fix properly identifies that 2. Handles Edge Case WellThe CodeRabbit feedback about replaced DOM nodes (same ID, new element) is correctly handled:
3. Excellent Test Coverage
4. Good Code Quality
🔍 Security & Performance ConsiderationsSecurity: ✅ No Concerns
Performance: ✅ Good
💡 Suggestions for Improvement1. Minor: Type Safety in Unmount Check (Low Priority)Current code (lines 96-100): if (
supportsRootApi &&
existing.root &&
typeof existing.root === 'object' &&
'unmount' in existing.root
) {Suggestion: The type checking is a bit verbose. Consider using a type guard: function isReactRoot(root: RenderReturnType): root is ReactRoot {
return supportsRootApi && root !== null && typeof root === 'object' && 'unmount' in root;
}
// Then use:
if (isReactRoot(existing.root)) {
existing.root.unmount();
} else {
unmountComponentAtNode(existing.domNode);
}Impact: Minor readability improvement, not critical. 2. Minor: Consider Logging Cleanup (Low Priority)Current code: Only logs errors during unmount, not successful cleanup. Suggestion: Add trace logging for the cleanup path to help debugging: if (trace) {
console.log(`Cleaning up replaced component: \${name} (dom id: \${domNodeId})`);
}Impact: Better observability during development. 3. Consider: Edge Case Documentation (Documentation)The logic handles an important edge case (replaced nodes), but it might not be obvious to future maintainers why we check Suggestion: Add a comment explaining the edge case: // Only skip if it's the exact same DOM node and it's still connected to the document
// If the DOM node was replaced (e.g., via innerHTML or async HTML injection),
// we need to unmount the old root and render to the new node to prevent:
// 1. Memory leaks from orphaned React roots
// 2. Components not rendering when legitimately needed
const sameNode = existing.domNode === domNode && existing.domNode.isConnected;🎯 Specific Code Review NotesClientRenderer.ts:87-93 ✅The reference equality check ( ClientRenderer.ts:106-111 ✅Swallowing unmount errors for replaced nodes is appropriate—the old node is likely already detached and unable to unmount cleanly. ClientRenderer.ts:141 ✅Storing both Test Coverage ✅The test suite covers:
📋 Testing RecommendationsBefore merge, verify:
🚀 ConclusionThis is a high-quality fix that:
The suggestions above are minor improvements, not blockers. The PR is ready to merge as-is. Recommendation: Approve & Merge ✅ Reviewed using CLAUDE.md guidelines for code quality, testing, and best practices. |
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
Code Review: Fix Hydration Mismatch on Multiple reactOnRailsPageLoaded() CallsOverall Assessment: This is a well-implemented fix with excellent test coverage. The solution correctly addresses the root cause and handles edge cases thoughtfully. Strengths1. Root Cause AnalysisThe fix correctly identifies that the issue stems from shouldHydrate = !!domNode.innerHTML returning true for previously client-rendered components. The solution (checking renderedRoots Map) is clean and efficient. 2. Excellent Test Coverage
3. Edge Case HandlingThe DOM node replacement logic is particularly well done - existing.domNode === domNode && existing.domNode.isConnected correctly distinguishes between same render calls (skip) vs replaced DOM nodes (clean up old root, render to new node). 4. Cleanup on ReplacementProper unmounting of the old root when a DOM node is replaced prevents memory leaks, with support for both React 18+ (createRoot API) and React 17- (render API). Observations & Minor Suggestions1. TypeScript Safety in Unmount CheckThe unmount check is defensive but could benefit from a comment explaining it handles both React 18+ and React 17- gracefully. 2. Performance ConsiderationThe renderedRoots Map grows with every component render. In long-running SPAs, this could accumulate entries for components no longer on the page. Minor concern: If someone programmatically removes components without calling React unmount, entries will leak. However, this is an edge case and the current implementation is fine for typical usage. Future enhancement: Consider cleanup on page unload or provide a manual cleanup API for SPAs with dynamic content. 3. Test Quality NoteThe unit tests verify mock call counts rather than actual DOM state changes. While this works, the E2E tests compensate by testing actual browser behavior, so overall coverage is excellent. Security & Performance
Test Coverage: ExcellentUnit Tests: Skip already-rendered components, Re-render when DOM node replaced E2E Tests: Initial render, Multiple reactOnRailsPageLoaded() calls, Multiple dynamic additions, DOM replacement, Console error detection Potential Issues: None FoundThe implementation correctly handles:
Recommendation: APPROVEThis PR is ready to merge pending CI passing. Minor follow-ups (optional, can be done in future PRs):
Great work on the thorough investigation and implementation! |


Summary
Fixes #2210 - When
reactOnRailsPageLoaded()is called multiple times (e.g., for asynchronously loaded content), previously rendered client-side components were being re-hydrated, causing hydration mismatch errors in React 19.Root cause: The
shouldHydratecheck (!!domNode.innerHTML) was true for previously client-rendered components because they already had HTML content. This caused React to try to hydrate content that wasn't server-rendered.Fix: Skip rendering for components that are already tracked in the
renderedRootsMap. This prevents re-hydration of already-rendered components while still allowing new components to be rendered.Changes
renderElement()Test plan
renderComponent()is called twiceasync_page_loaded_testpage should show no hydration errors when adding components dynamically/async_page_loaded_test, click "Add Component" multiple times, verify no console errorsWorkaround mentioned in issue
The user noted that using
reactOnRailsComponentLoaded(domId)per component works without issues. This fix makesreactOnRailsPageLoaded()work correctly as well.🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
Bug Fixes
reactOnRailsPageLoaded()is called multiple times, eliminating hydration errors.Tests
reactOnRailsPageLoaded()invocation scenarios.✏️ Tip: You can customize this high-level summary in your review settings.