You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: warn about react_component helper collision with react-rails (#3143) (#3160)
## Summary
- Addresses #3143: incremental migrations from `react-rails` break
silently as soon as `react_on_rails` is added because both gems define a
`react_component` view helper with incompatible signatures, and Rails
helper auto-loading lets the second-defined one win. Un-migrated views
then raise `ArgumentError` at render time.
- Expands `docs/oss/migrating/migrating-from-react-rails.md` with a
preflight callout and a new **Coexistence: keeping both gems installed
during a staged migration** section covering:
- why the collision happens (both helpers, signature diff, Rails helper
load order),
- a one-liner `rg` / `grep` to audit legacy positional calls **before**
adding the gem,
- Option A — migrate all call sites in the same PR (recommended),
- Option B — preserve legacy `react-rails` semantics via a prepended
`LegacyReactComponent` shim and expose an explicit
`react_on_rails_component(...)` alias for migrated mounts (the pattern
the `hackclub/hcb` migration actually used),
- known limitations of Option B (app-level only, temporary, remove when
done).
- Docs-only change. No CHANGELOG entry per
`.claude/docs/changelog-guidelines.md` (docs fixes are excluded).
## Why docs, not a code change
The issue proposes either (1) avoid taking over the default helper when
`react-rails` is detected, or (2) document the collision. Option 1 would
mean changing the semantics of a public view helper based on runtime gem
detection — brittle and a backwards-compat risk for everyone who already
migrated. The reporter ended up using exactly the two shims now
documented in Option B, so codifying that path is the pragmatic fix and
leaves room for a future product-level coexistence mode if demand
warrants it.
## Test plan
- [x] `npx prettier --check
docs/oss/migrating/migrating-from-react-rails.md` passes.
- [x] Pre-commit hooks (trailing newlines, lychee offline link check,
prettier) pass.
- [x] Pre-push lychee online link check passes — all 6 links resolve.
- [x] Internal anchor
`#legacy-compatibility-fixes-that-often-make-migration-one-shot` matches
an existing heading in the same file.
- [ ] Reviewer sanity-checks the Option B shim snippet against a toy
`react-rails` + `react_on_rails` app (not verified in this PR — the
pattern is lifted from the reporter's working migration).
Fixes#3143.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Low risk: documentation-only changes. Main risk is misleading guidance
for coexistence shims if copied verbatim, but no runtime code is changed
in this repo.
>
> **Overview**
> Adds a new preflight warning and an expanded **coexistence** section
in `migrating-from-react-rails.md` explaining that `react-rails` and
`react_on_rails` both define `react_component` with incompatible
signatures, which can cause runtime `ArgumentError` or silent prop loss
during staged migrations.
>
> Provides quick audit commands to find legacy call sites and documents
two mitigation paths: migrate all mounts to the options-hash form in the
same PR, or temporarily add an app-level initializer shim that preserves
legacy `react_component` behavior and introduces an explicit
`react_on_rails_component` alias (with limitations and cleanup
guidance).
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
5394237. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Documentation**
* Added guidance for staged migrations where two React integration
libraries coexist, warning about a view-helper signature collision that
can cause runtime errors or silent prop loss. Includes commands to
locate affected call sites and two mitigation paths: update mounts to
the newer options-style form, or apply a temporary app-level shim/alias
during migration (with listed limitations and restart requirements).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/oss/migrating/migrating-from-react-rails.md
+102Lines changed: 102 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -45,6 +45,7 @@ Before swapping gems, check these first:
45
45
4.**Package manager metadata**: if you have `yarn.lock`, `pnpm-lock.yaml`, or `bun.lock*`, ensure `package.json` has a matching `packageManager` field (for example `npm@10.9.2`, `yarn@1.22.22`, `pnpm@10.12.1`, or `bun@1.2.13`).
46
46
5.**Browserslist source**: use one source only. If `.browserslistrc` exists, remove `browserslist` from `package.json`.
47
47
6.**JSX-in-.js projects**: current install generator auto-switches to Babel when JSX is detected in `.js` files. If your project has custom transpiler setup, review `config/shakapacker.yml` after generation.
48
+
7.**`react_component` helper collision**: if you plan to keep `react-rails` installed during a staged migration, read [Coexistence: keeping both gems installed during a staged migration](#coexistence-keeping-both-gems-installed-during-a-staged-migration) before adding `react_on_rails`. Both gems define a `react_component` view helper with incompatible signatures; once `react_on_rails` is present, its helper takes precedence in all views regardless of gem load order.
48
49
49
50
If you are already on `shakapacker` 7+ and React 18+, the migration is mostly about helper syntax, component registration, and generated defaults.
50
51
@@ -159,6 +160,107 @@ Older `react-rails` apps frequently need these additional fixes after the genera
159
160
160
161
For published repo examples, including older and Rails 7-era `react-rails` migrations, see [Example Migrations](./example-migrations.md).
161
162
163
+
## Coexistence: keeping both gems installed during a staged migration
164
+
165
+
Large apps often cannot swap every `react-rails` mount in a single PR. If you need `react-rails` and `react_on_rails` installed side-by-side while you migrate views incrementally, plan for the `react_component` helper collision **before** adding the gem.
166
+
167
+
### Why it collides
168
+
169
+
Both gems ship a view helper named `react_component` that ends up available in Rails views:
-`react_on_rails` (`ReactOnRailsHelper` → `ReactOnRails::Helper#react_component`) takes `react_component(name, options = {})` where props are nested under `options[:props]`.
173
+
174
+
`react-rails` includes `React::Rails::ViewHelper` directly into `ActionView::Base` from its railtie. `react_on_rails` ships `ReactOnRailsHelper` as a normal Rails helper module, and under Rails' default `include_all_helpers` behavior that helper is pulled into the controller/view helper module that sits earlier in method lookup than `ActionView::Base`. In a standard Rails app, that means `ReactOnRailsHelper#react_component` wins once the gem is present. This is a helper-precedence issue, not `app/helpers/` file order or gem-name alphabetics. If your app customizes helper loading, verify which helper owns `react_component` before relying on coexistence.
175
+
176
+
Once you add `react_on_rails` to the `Gemfile`, every existing legacy call starts resolving to `ReactOnRails::Helper#react_component(name, options = {})`, which behaves differently depending on how many positional arguments you pass. As of Rails 7/8, Rails gives no boot-time warning in either case:
177
+
178
+
-**Three or more positional arguments** — e.g. `react_component "command_bar/CommandBar", props, { camelize_props: false }` — raise `ArgumentError` at render time on the first request to any un-migrated view, because the new helper only takes two arguments.
179
+
-**Two positional arguments** — e.g. `react_component "command_bar/CommandBar", props` — are silently accepted. The `props` hash is bound to `options`, but React on Rails reads props only from `options[:props]`, so the component renders with no props instead of failing loudly. This is the more dangerous case: un-migrated views do not error; they just lose their data.
180
+
181
+
### Detecting the collision quickly
182
+
183
+
Before adding the gem, audit existing positional-style calls so you know what needs a shim or a same-PR migration. Pay particular attention to two-argument calls, which fail silently rather than raising:
`app/helpers` catches view-helper wrappers that call `react_component` from Ruby rather than a template. Expand the path list further if you mount React from other locations (Phlex views, gem engines, etc.), or drop the path argument entirely to search the whole project and filter out false positives manually.
192
+
193
+
Any call that passes props as the second positional argument (rather than `{ props: ... }`) will break as soon as `react_on_rails` is loaded — either by raising `ArgumentError` (3+ args) or by silently dropping props (2 args).
194
+
195
+
### Option A: migrate all call sites in the same PR (recommended)
196
+
197
+
The cleanest path is to update every `react_component` call to the options-hash form in the same PR that adds the gem. See the syntax change under [Legacy compatibility fixes](#legacy-compatibility-fixes-that-often-make-migration-one-shot). After this, there is no collision to manage — the new helper is the only helper.
198
+
199
+
### Option B: preserve the legacy helper and use an explicit alias
200
+
201
+
If a single-PR migration is impractical, you can keep `react-rails`'s `react_component` semantics for un-migrated views and introduce a separate helper name for migrated mounts.
202
+
203
+
Define the shim module directly in an initializer so it lives outside Zeitwerk's autoload paths. The module:
204
+
205
+
1. Prepends an override so legacy `react_component(...)` calls keep delegating to `React::Rails::ViewHelper`.
206
+
2. Exposes an explicit `react_on_rails_component(...)` alias for migrated mounts.
207
+
208
+
> **Note:** this initializer was contributed by a community member migrating a production app. It is not covered by the `react_on_rails` test suite. Verify it works in a staging environment before relying on it in production.
Defining the module inline in the initializer avoids a subtle issue: files under `app/helpers/` are on Zeitwerk's autoload paths, and `require`-ing such a file from an initializer can confuse Zeitwerk's bookkeeping in production eager-load mode. Keeping the module in `config/initializers/` sidesteps that entirely.
246
+
247
+
Use `react_on_rails_component(...)` in new or migrated views:
Leave existing `react_component(...)` calls untouched until you are ready to migrate them. When every call site has been converted, update each migrated call site from `react_on_rails_component(...)` back to `react_component(...)` and delete `config/initializers/react_on_rails_coexistence.rb`. A project-wide find-and-replace over `react_on_rails_component` makes the final pass quick. See [Known limitations of Option B](#known-limitations-of-option-b) below for the full cost of this approach.
254
+
255
+
### Known limitations of Option B
256
+
257
+
-**Two project-wide renames.** Every migrated call site is renamed twice: `react_component` → `react_on_rails_component` while the shim is active, then `react_on_rails_component` → `react_component` once the shim is removed. On a large app this can equal or exceed the effort of migrating call sites in a single PR (Option A). Factor this in before choosing Option B.
258
+
- This is a migration-only pattern. Carry the shim only as long as legacy calls remain, then remove it.
259
+
- Edits to `config/initializers/react_on_rails_coexistence.rb` require a server restart in development, like any initializer.
260
+
-**The shim is app-level and can hard-fail in restricted view contexts.** In gem-provided engines, Rails engines, ViewComponent, or Action Mailer views, the receiver may be missing helper methods used by `react-rails` or React on Rails. That means legacy `react_component(...)` calls and migrated `react_on_rails_component(...)` calls can both fail at render time even though the method name is visible. Explicitly include the needed helper module or add a context-local wrapper before using either helper in those contexts.
261
+
-**Remove the initializer before (or at the same time as) removing `react-rails` from the `Gemfile`.** The shim's method body references `React::Rails::ViewHelper`, so once the gem is gone any request that still routes through the legacy delegate raises `NameError: uninitialized constant React::Rails::ViewHelper` at render time. Delete `config/initializers/react_on_rails_coexistence.rb` in the same commit that drops the gem.
262
+
- Server rendering, Pro features, and auto-bundling all work through the explicit `react_on_rails_component` alias — the shim only forwards the default helper name back to `react-rails`.
263
+
162
264
## Practical checklist for Webpacker-era apps
163
265
164
266
See [Preferred path for Webpacker-era apps](#preferred-path-for-webpacker-era-apps) above for the recommended staging. The concrete checklist follows.
0 commit comments