diff --git a/docs/oss/migrating/migrating-from-react-rails.md b/docs/oss/migrating/migrating-from-react-rails.md index a3ed797cd1..d8b545b712 100644 --- a/docs/oss/migrating/migrating-from-react-rails.md +++ b/docs/oss/migrating/migrating-from-react-rails.md @@ -6,6 +6,18 @@ If you want repo-shaped references before touching your own app, start with [Example Migrations](./example-migrations.md) and then come back here for the mechanics. +## Pick the right first target + +Not every `react-rails` app is a good candidate for a low-friction first migration. Before you start, classify what you have: + +- **Rails-owned island mounts on Shakapacker 6+ and Rails 6+.** This is the smoothest path. The generator + the steps below usually get you there with small, localized edits. (Note: `server_bundle_output_path` auto-detection requires Shakapacker 9.0+; on 6–8, set it explicitly in the initializer.) +- **Webpacker-era apps (`gem "webpacker"`, Webpack 4).** Current React on Rails does not support Webpacker — `react_on_rails doctor` flags it as a removed breaking-change issue, and the gem requires `shakapacker >= 6.0`. You must migrate off Webpacker before installing current React on Rails. See [Preferred path for Webpacker-era apps](#preferred-path-for-webpacker-era-apps) below. +- **Client-routed SPA shells (Rails is mostly a shell around a React Router / TanStack Router app).** Render the top-level SPA component from one ERB view using `react_component` (or `react_component_hash` when SSR needs to return multiple regions such as `componentHtml`, `title`, and other head tags). + - One `react_component` call mounts the whole app. + - If you additionally want to break the SPA into several Rails-owned islands, treat that as a separate product decision rather than bundling it with the bundler/integration change. + +The wrong first target usually leads teams to conclude "React on Rails is broken" when the real problem is legacy bundler compatibility, or to bundle a SPA re-architecture into what should have been a bundler migration. + ## Choose a first slice Pick a small first slice before you touch the whole app: @@ -14,11 +26,20 @@ Pick a small first slice before you touch the whole app: 2. Good first wins are often maintainability-first: replacing `ReactRailsUJS` on one low-risk mount, splitting a large shell into smaller boundaries, or moving one legacy Rails page behind a documented helper path. 3. The first PR does not need to eliminate every `react_component` call. It only needs to prove that one mount can move cleanly. +## Preferred path for Webpacker-era apps + +If the app still uses `gem "webpacker"`, the recommended path is: + +1. **Migrate to Shakapacker first, as its own PR.** Keep the bundler change separate from the React on Rails change. This makes each step reviewable and isolates any compatibility issues. See the [Shakapacker v6 upgrade guide](https://github.com/shakacode/shakapacker/blob/v6.6.0/docs/v6_upgrade.md) for the concrete Webpacker → Shakapacker steps. +2. **Then run the React on Rails install generator** against the Shakapacker-based app. + +The generator is not designed to bridge Webpack 4 + Webpacker to current React on Rails defaults for you — it assumes a Shakapacker baseline. If you cannot migrate off Webpacker yet, pin `react_on_rails` to `~> 14.2` (v15.0.0 is retracted; v16 is the release that removed Webpacker support) rather than trying to use current React on Rails with Webpacker. + ## Preflight Before swapping gems, check these first: -1. **Webpacker vs Shakapacker**: if the app still uses `webpacker`, migrate to `shakapacker` first. +1. **Webpacker vs Shakapacker**: if the app still uses `webpacker`, see [Preferred path for Webpacker-era apps](#preferred-path-for-webpacker-era-apps) above. 2. **Bundler age**: some older `react-rails` apps still carry Bundler 1.x lockfiles. Those can fail on modern Ruby before you even reach the migration work. 3. **Rails age**: current `react_on_rails` requires Rails 5.2+. Rails 5.1 / Webpacker 3 apps are usually a staged migration, not a one-command migration. 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`). @@ -140,6 +161,8 @@ For published repo examples, including older and Rails 7-era `react-rails` migra ## Practical checklist for Webpacker-era apps +See [Preferred path for Webpacker-era apps](#preferred-path-for-webpacker-era-apps) above for the recommended staging. The concrete checklist follows. + If your app looks like this: - `gem "webpacker"` in `Gemfile` @@ -149,9 +172,11 @@ If your app looks like this: then treat the migration as: -1. Move from `webpacker` to `shakapacker`. +1. Move from `webpacker` to `shakapacker` in its own PR. 2. If the app is still on Rails 5.1, upgrade Rails to 5.2+ before adding current `react_on_rails`. 3. Remove `react_ujs`. 4. Run the React on Rails install generator. 5. Replace helper syntax and component registration. 6. Review `bin/dev`, `config/shakapacker.yml`, and webpack config diffs before committing. + +Current React on Rails does not support `gem "webpacker"`. The install generator adds Shakapacker rather than enforcing a hard install-time block, and `react_on_rails doctor` flags Webpacker as a removed/breaking-change issue when it detects `config/webpacker.yml` or `bin/webpacker`. Migrate to Shakapacker first (step 1 above) rather than budgeting time for Webpacker compatibility shims. diff --git a/docs/oss/migrating/migrating-from-vite-rails.md b/docs/oss/migrating/migrating-from-vite-rails.md index d0f91f3ae3..8766485752 100644 --- a/docs/oss/migrating/migrating-from-vite-rails.md +++ b/docs/oss/migrating/migrating-from-vite-rails.md @@ -17,6 +17,17 @@ React on Rails is a better fit when you want one or more of these: If your app is already happy with a Vite-only client-rendered setup, this migration is optional. +## Two different starting points + +Not all `vite_rails` + React apps are the same shape, and the migration effort differs for each: + +- **Rails-owned island mounts.** Rails renders real ERB views and mounts one or more React components inside them. The migration is incremental: you can cut over one page (or one mount) at a time. +- **Client-routed SPA shells.** Rails serves a minimal layout and a single `
`, and a client-side router (React Router / TanStack Router) owns everything after the first render. You have two reasonable migration shapes here: + 1. **Keep the SPA shape.** Render the top-level SPA component from a single ERB view using `react_component` (or `react_component_hash` when you need SSR that returns multiple regions such as `componentHtml`, `title`, and other head tags). One React on Rails call mounts the whole app — this is the pattern used by the largest React on Rails Pro deployment in production, Popmenu (for example, [110grill.com](https://www.110grill.com/) and other Popmenu-powered restaurant sites), where the entire app is a single top-level component call. + 2. **Break the SPA into island mounts** by moving Rails back to being the view-owner. This is a real product decision and should not be bundled with the bundler/integration change. + +For most teams, the **Keep the SPA shape** path is the fastest first step: you're swapping Vite's build integration for Shakapacker, not re-architecting the app. The main friction is usually not the Rails-side `react_component` call — it's the Vite-specific runtime behavior (`import.meta.env`, `import.meta.glob`, Vite plugins with no direct Shakapacker analogue) that the client code may depend on. See [Replace Vite-specific asset and env usage](#5-replace-vite-specific-asset-and-env-usage) for the concrete replacements. + ## Preflight Before you start, make sure the current app still installs cleanly on the Ruby and Node versions you plan to use for the migration. @@ -137,6 +148,46 @@ Vite-specific `import.meta.env` usage needs to be replaced. In a React on Rails - `railsContext` for request-aware values - `process.env` in server-rendered bundles (available natively in Node); for client bundles, values must be injected via webpack's `DefinePlugin` or `EnvironmentPlugin` +### `import.meta.glob` + +`import.meta.glob` has no direct Webpack equivalent. Replace it with [`require.context`](https://webpack.js.org/guides/dependency-management/#requirecontext): + +- the glob-pattern syntax differs (Webpack uses a regex argument, not a glob string) +- lazy/eager behavior is selected via a `mode` argument (`'sync'`, `'lazy'`, `'lazy-once'`, `'eager'`, `'weak'`) rather than the per-call options `import.meta.glob` exposes +- the returned context function requires explicit `.keys()` + key lookup, not the object-map shape `import.meta.glob` returns + +A minimal before/after — note two semantic mismatches that bite during migration: + +1. **Key paths differ.** Vite returns paths relative to the _calling module_ (`'./dir/foo.js'`); `require.context` returns paths relative to the _context directory_ (`'./foo.js'`). Code that derives names from keys (routing, registration, etc.) needs to account for this. +2. **Sync vs async.** `import.meta.glob` is lazy by default — values are `() => import(...)` returning a Promise. The default `require.context(dir, recursive, regex)` (no 4th argument) is synchronous, so `ctx(key)` returns the module directly. For the lazy case, pass `'lazy'` as the 4th argument so `ctx(key)` returns a `Promise` (see the lazy example below). + +Eager / synchronous case: + +```js +// Vite (eager) +const modules = import.meta.glob('./dir/**/*.js', { eager: true }); +// { './dir/foo.js': , ... } ← keys relative to current file + +// Webpack (Shakapacker) — synchronous equivalent +const ctx = require.context('./dir', true, /\.js$/); +// ctx.keys() → ['./foo.js', ...] ← keys relative to context dir, NOT './dir/foo.js' +// ctx('./foo.js') → the module (synchronous) +``` + +Lazy (default Vite) case — pass `'lazy'` as the 4th `require.context` argument so `ctx(key)` returns a `Promise`: + +```js +// Vite (lazy, the default) +const modules = import.meta.glob('./dir/**/*.js'); +// { './dir/foo.js': () => import('./dir/foo.js'), ... } + +// Webpack (Shakapacker) — lazy equivalent +const ctx = require.context('./dir', true, /\.js$/, 'lazy'); +// ctx.keys() → ['./foo.js', ...] ← keys relative to context dir, NOT './dir/foo.js' +// ctx(key) now returns Promise, matching Vite's lazy semantics +const lazyModules = Object.fromEntries(ctx.keys().map((key) => [key, () => ctx(key)])); +``` + ## 6. Replace the development workflow Vite apps usually have a dev command like: