diff --git a/CLAUDE.md b/CLAUDE.md
index af69392b38..3a02089dd6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -59,6 +59,20 @@ These requirements are non-negotiable. CI will fail if not followed.
---
+## 🚀 COMMIT AND PUSH BY DEFAULT
+
+**When confident in your changes, commit and push without asking for permission.**
+
+- After completing a task successfully, commit and push immediately
+- Run relevant tests locally first to verify changes work
+- Don't wait for explicit user approval if you've tested and are confident
+- **ALWAYS monitor CI after pushing** - check status and address any failures proactively
+- Keep monitoring until CI passes or issues are resolved
+
+This saves time and keeps the workflow moving efficiently.
+
+---
+
## 🚨 AVOIDING CI FAILURE CYCLES
**CRITICAL**: Large-scale changes (directory structure, configs, workflows) require comprehensive local testing BEFORE pushing.
diff --git a/react_on_rails/lib/react_on_rails/git_utils.rb b/react_on_rails/lib/react_on_rails/git_utils.rb
index 151d2aeedf..d0b2debc49 100644
--- a/react_on_rails/lib/react_on_rails/git_utils.rb
+++ b/react_on_rails/lib/react_on_rails/git_utils.rb
@@ -5,7 +5,9 @@
module ReactOnRails
module GitUtils
def self.uncommitted_changes?(message_handler, git_installed: true)
- return false if ENV["COVERAGE"] == "true"
+ # Skip check in CI environments - CI often makes temporary modifications
+ # (e.g., script/convert for minimum version testing) before running generators
+ return false if ENV["CI"] == "true" || ENV["COVERAGE"] == "true"
status = `git status --porcelain`
return false if git_installed && status&.empty?
diff --git a/react_on_rails/spec/dummy/Gemfile.lock b/react_on_rails/spec/dummy/Gemfile.lock
index eb919c4013..ce0a77dffb 100644
--- a/react_on_rails/spec/dummy/Gemfile.lock
+++ b/react_on_rails/spec/dummy/Gemfile.lock
@@ -345,7 +345,7 @@ GEM
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
semantic_range (3.1.0)
- shakapacker (9.3.0)
+ shakapacker (9.4.0)
activesupport (>= 5.2)
package_json
rack-proxy (>= 0.6.1)
@@ -461,7 +461,7 @@ DEPENDENCIES
sass-rails (~> 6.0)
sdoc
selenium-webdriver (= 4.9.0)
- shakapacker (= 9.3.0)
+ shakapacker (= 9.4.0)
spring (~> 4.0)
sprockets (~> 4.0)
sqlite3 (~> 1.6)
diff --git a/react_on_rails/spec/dummy/client/app-react16/startup/ReduxApp.client.jsx b/react_on_rails/spec/dummy/client/app-react16/startup/ReduxApp.client.jsx
index b7ab770286..a96f624be7 100644
--- a/react_on_rails/spec/dummy/client/app-react16/startup/ReduxApp.client.jsx
+++ b/react_on_rails/spec/dummy/client/app-react16/startup/ReduxApp.client.jsx
@@ -5,7 +5,7 @@
import React from 'react';
import { combineReducers, applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux';
-import thunkMiddleware from 'redux-thunk';
+import { thunk } from 'redux-thunk';
import ReactDOM from 'react-dom';
import reducers from '../../app/reducers/reducersIndex';
@@ -29,7 +29,7 @@ export default (props, railsContext, domNodeId) => {
// This is where we'll put in the middleware for the async function. Placeholder.
// store will have helloWorldData as a top level property
- const store = createStore(combinedReducer, combinedProps, applyMiddleware(thunkMiddleware));
+ const store = createStore(combinedReducer, combinedProps, applyMiddleware(thunk));
// renderApp is a function required for hot reloading. see
// https://github.com/retroalgic/react-on-rails-hot-minimal/blob/master/client/src/entry.js
diff --git a/react_on_rails/spec/dummy/client/app/components/ReactHelmet.jsx b/react_on_rails/spec/dummy/client/app/components/ReactHelmet.jsx
index bba49492df..ca82611935 100644
--- a/react_on_rails/spec/dummy/client/app/components/ReactHelmet.jsx
+++ b/react_on_rails/spec/dummy/client/app/components/ReactHelmet.jsx
@@ -1,7 +1,10 @@
import React from 'react';
-import { Helmet } from 'react-helmet';
+import { Helmet } from '@dr.pogodin/react-helmet';
import HelloWorld from '../startup/HelloWorld';
+// Note: This component expects to be wrapped in a HelmetProvider by its parent.
+// For client-side rendering, wrap in HelmetProvider at the app root.
+// For server-side rendering, the server entry point provides the HelmetProvider.
const ReactHelmet = (props) => (
diff --git a/react_on_rails/spec/dummy/client/app/startup/ReactHelmetApp.client.jsx b/react_on_rails/spec/dummy/client/app/startup/ReactHelmetApp.client.jsx
index 2c5f342d92..4dd6c26570 100644
--- a/react_on_rails/spec/dummy/client/app/startup/ReactHelmetApp.client.jsx
+++ b/react_on_rails/spec/dummy/client/app/startup/ReactHelmetApp.client.jsx
@@ -1,11 +1,17 @@
// Top level component for simple client side only rendering
import React from 'react';
+import { HelmetProvider } from '@dr.pogodin/react-helmet';
import ReactHelmet from '../components/ReactHelmet';
// This works fine, React functional component:
// export default (props) => ;
-export default (props) => ;
+// HelmetProvider is required by @dr.pogodin/react-helmet for both client and server rendering
+export default (props) => (
+
+
+
+);
// Note, the server side has to be a Render-Function
diff --git a/react_on_rails/spec/dummy/client/app/startup/ReactHelmetApp.server.jsx b/react_on_rails/spec/dummy/client/app/startup/ReactHelmetApp.server.jsx
index f7c77e8a73..5c9d10cb2b 100644
--- a/react_on_rails/spec/dummy/client/app/startup/ReactHelmetApp.server.jsx
+++ b/react_on_rails/spec/dummy/client/app/startup/ReactHelmetApp.server.jsx
@@ -1,7 +1,7 @@
// Top level component for simple client side only rendering
import React from 'react';
import { renderToString } from 'react-dom/server';
-import { Helmet } from 'react-helmet';
+import { HelmetProvider } from '@dr.pogodin/react-helmet';
import ReactHelmet from '../components/ReactHelmet';
/*
@@ -16,12 +16,21 @@ import ReactHelmet from '../components/ReactHelmet';
* the function could get the property of `.renderFunction = true` added to it.
*/
export default (props, _railsContext) => {
- const componentHtml = renderToString();
- const helmet = Helmet.renderStatic();
+ // For server-side rendering with @dr.pogodin/react-helmet, we pass a context object
+ // to HelmetProvider to capture the helmet data per-request (thread-safe)
+ const helmetContext = {};
+
+ const componentHtml = renderToString(
+
+
+ ,
+ );
+
+ const { helmet } = helmetContext;
const renderedHtml = {
componentHtml,
- title: helmet.title.toString(),
+ title: helmet ? helmet.title.toString() : '',
};
// Note that this function returns an Object for server rendering.
diff --git a/react_on_rails/spec/dummy/client/app/startup/ReactHelmetAppBroken.client.jsx b/react_on_rails/spec/dummy/client/app/startup/ReactHelmetAppBroken.client.jsx
index 2c5f342d92..4dd6c26570 100644
--- a/react_on_rails/spec/dummy/client/app/startup/ReactHelmetAppBroken.client.jsx
+++ b/react_on_rails/spec/dummy/client/app/startup/ReactHelmetAppBroken.client.jsx
@@ -1,11 +1,17 @@
// Top level component for simple client side only rendering
import React from 'react';
+import { HelmetProvider } from '@dr.pogodin/react-helmet';
import ReactHelmet from '../components/ReactHelmet';
// This works fine, React functional component:
// export default (props) => ;
-export default (props) => ;
+// HelmetProvider is required by @dr.pogodin/react-helmet for both client and server rendering
+export default (props) => (
+
+
+
+);
// Note, the server side has to be a Render-Function
diff --git a/react_on_rails/spec/dummy/client/app/startup/ReactHelmetAppBroken.server.jsx b/react_on_rails/spec/dummy/client/app/startup/ReactHelmetAppBroken.server.jsx
index d1c72af50d..f0d68d3e18 100644
--- a/react_on_rails/spec/dummy/client/app/startup/ReactHelmetAppBroken.server.jsx
+++ b/react_on_rails/spec/dummy/client/app/startup/ReactHelmetAppBroken.server.jsx
@@ -3,7 +3,7 @@
// function. The point of this is to provide a good error.
import React from 'react';
import { renderToString } from 'react-dom/server';
-import { Helmet } from 'react-helmet';
+import { HelmetProvider } from '@dr.pogodin/react-helmet';
import ReactHelmet from '../components/ReactHelmet';
/*
@@ -18,12 +18,17 @@ import ReactHelmet from '../components/ReactHelmet';
* Alternately, the function could get the property of `.renderFunction = true` added to it.
*/
export default (props) => {
- const componentHtml = renderToString();
- const helmet = Helmet.renderStatic();
+ const helmetContext = {};
+ const componentHtml = renderToString(
+
+
+ ,
+ );
+ const { helmet } = helmetContext;
const renderedHtml = {
componentHtml,
- title: helmet.title.toString(),
+ title: helmet ? helmet.title.toString() : '',
};
return { renderedHtml };
};
diff --git a/react_on_rails/spec/dummy/client/app/startup/ReduxApp.client.jsx b/react_on_rails/spec/dummy/client/app/startup/ReduxApp.client.jsx
index 3e8ddc211b..cfe9e24a93 100644
--- a/react_on_rails/spec/dummy/client/app/startup/ReduxApp.client.jsx
+++ b/react_on_rails/spec/dummy/client/app/startup/ReduxApp.client.jsx
@@ -5,7 +5,7 @@
import React from 'react';
import { combineReducers, applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux';
-import thunkMiddleware from 'redux-thunk';
+import { thunk } from 'redux-thunk';
import ReactDOMClient from 'react-dom/client';
import reducers from '../reducers/reducersIndex';
@@ -34,7 +34,7 @@ export default (props, railsContext, domNodeId) => {
// This is where we'll put in the middleware for the async function. Placeholder.
// store will have helloWorldData as a top level property
- const store = createStore(combinedReducer, combinedProps, applyMiddleware(thunkMiddleware));
+ const store = createStore(combinedReducer, combinedProps, applyMiddleware(thunk));
// renderApp is a function required for hot reloading. see
// https://github.com/retroalgic/react-on-rails-hot-minimal/blob/master/client/src/entry.js
diff --git a/react_on_rails/spec/dummy/client/app/startup/ReduxApp.server.jsx b/react_on_rails/spec/dummy/client/app/startup/ReduxApp.server.jsx
index bdcc317962..a26b2969f0 100644
--- a/react_on_rails/spec/dummy/client/app/startup/ReduxApp.server.jsx
+++ b/react_on_rails/spec/dummy/client/app/startup/ReduxApp.server.jsx
@@ -6,7 +6,7 @@
import React from 'react';
import { combineReducers, applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux';
-import middleware from 'redux-thunk';
+import { thunk } from 'redux-thunk';
// Uses the index
import reducers from '../reducers/reducersIndex';
@@ -28,7 +28,7 @@ export default (props, railsContext) => {
// This is where we'll put in the middleware for the async function. Placeholder.
// store will have helloWorldData as a top level property
- const store = applyMiddleware(middleware)(createStore)(combinedReducer, combinedProps);
+ const store = applyMiddleware(thunk)(createStore)(combinedReducer, combinedProps);
// Provider uses the this.props.children, so we're not typical React syntax.
// This allows redux to add additional props to the HelloWorldContainer.
diff --git a/react_on_rails/spec/dummy/client/app/stores/SharedReduxStore.jsx b/react_on_rails/spec/dummy/client/app/stores/SharedReduxStore.jsx
index 33dcc680ed..3d463ff891 100644
--- a/react_on_rails/spec/dummy/client/app/stores/SharedReduxStore.jsx
+++ b/react_on_rails/spec/dummy/client/app/stores/SharedReduxStore.jsx
@@ -1,5 +1,5 @@
import { combineReducers, applyMiddleware, createStore } from 'redux';
-import middleware from 'redux-thunk';
+import { thunk } from 'redux-thunk';
import reducers from '../reducers/reducersIndex';
@@ -12,5 +12,5 @@ export default (props, railsContext) => {
delete props.prerender;
const combinedReducer = combineReducers(reducers);
const newProps = { ...props, railsContext };
- return applyMiddleware(middleware)(createStore)(combinedReducer, newProps);
+ return applyMiddleware(thunk)(createStore)(combinedReducer, newProps);
};
diff --git a/react_on_rails/spec/dummy/package.json b/react_on_rails/spec/dummy/package.json
index e9998a52db..84c0436be3 100644
--- a/react_on_rails/spec/dummy/package.json
+++ b/react_on_rails/spec/dummy/package.json
@@ -18,14 +18,14 @@
"node-libs-browser": "^2.2.1",
"null-loader": "^4.0.0",
"prop-types": "^15.7.2",
- "react": "18.0.0",
- "react-dom": "18.0.0",
- "react-helmet": "^6.1.0",
+ "@dr.pogodin/react-helmet": "^3.0.4",
+ "react": "19.0.0",
+ "react-dom": "19.0.0",
"react-on-rails": "link:.yalc/react-on-rails",
- "react-redux": "^8.0.2",
+ "react-redux": "^9.2.0",
"react-router-dom": "^6.0.0",
- "redux": "^4.0.1",
- "redux-thunk": "^2.2.0",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
"regenerator-runtime": "^0.13.4"
},
"devDependencies": {
@@ -38,7 +38,6 @@
"@rescript/react": "^0.13.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
- "@types/react-helmet": "^6.1.5",
"babel-loader": "8.2.4",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"compression-webpack-plugin": "9",
diff --git a/react_on_rails/spec/react_on_rails/git_utils_spec.rb b/react_on_rails/spec/react_on_rails/git_utils_spec.rb
index 86d70a5a0e..baeaf95981 100644
--- a/react_on_rails/spec/react_on_rails/git_utils_spec.rb
+++ b/react_on_rails/spec/react_on_rails/git_utils_spec.rb
@@ -8,6 +8,14 @@ module ReactOnRails
context "with uncommitted git changes" do
let(:message_handler) { instance_double("MessageHandler") } # rubocop:disable RSpec/VerifiedDoubleReference
+ around do |example|
+ # Temporarily unset CI env var to test actual uncommitted changes behavior
+ original_ci = ENV.fetch("CI", nil)
+ ENV.delete("CI")
+ example.run
+ ENV["CI"] = original_ci if original_ci
+ end
+
it "returns true" do
allow(described_class).to receive(:`).with("git status --porcelain").and_return("M file/path")
expect(message_handler).to receive(:add_error)
@@ -22,9 +30,37 @@ module ReactOnRails
end
end
+ context "when CI environment variable is set" do
+ let(:message_handler) { instance_double("MessageHandler") } # rubocop:disable RSpec/VerifiedDoubleReference
+
+ around do |example|
+ original_ci = ENV.fetch("CI", nil)
+ ENV["CI"] = "true"
+ example.run
+ ENV["CI"] = original_ci
+ ENV.delete("CI") unless original_ci
+ end
+
+ it "returns false without checking git status" do
+ # Should not call git status at all
+ expect(described_class).not_to receive(:`)
+ expect(message_handler).not_to receive(:add_error)
+
+ expect(described_class.uncommitted_changes?(message_handler, git_installed: true)).to be(false)
+ end
+ end
+
context "with clean git status" do
let(:message_handler) { instance_double("MessageHandler") } # rubocop:disable RSpec/VerifiedDoubleReference
+ around do |example|
+ # Temporarily unset CI env var to test actual clean git behavior
+ original_ci = ENV.fetch("CI", nil)
+ ENV.delete("CI")
+ example.run
+ ENV["CI"] = original_ci if original_ci
+ end
+
it "returns false" do
allow(described_class).to receive(:`).with("git status --porcelain").and_return("")
expect(message_handler).not_to receive(:add_error)
@@ -36,6 +72,14 @@ module ReactOnRails
context "with git not installed" do
let(:message_handler) { instance_double("MessageHandler") } # rubocop:disable RSpec/VerifiedDoubleReference
+ around do |example|
+ # Temporarily unset CI env var to test actual git not installed behavior
+ original_ci = ENV.fetch("CI", nil)
+ ENV.delete("CI")
+ example.run
+ ENV["CI"] = original_ci if original_ci
+ end
+
it "returns true" do
allow(described_class).to receive(:`).with("git status --porcelain").and_return(nil)
expect(message_handler).to receive(:add_error)
diff --git a/react_on_rails_pro/Gemfile.development_dependencies b/react_on_rails_pro/Gemfile.development_dependencies
index 926c07ec3a..6519f1ced0 100644
--- a/react_on_rails_pro/Gemfile.development_dependencies
+++ b/react_on_rails_pro/Gemfile.development_dependencies
@@ -7,7 +7,7 @@ ruby '3.3.7'
gem "react_on_rails", path: "../"
-gem "shakapacker", "9.3.0"
+gem "shakapacker", "9.4.0"
gem "bootsnap", require: false
gem "rails", "~> 7.1"
gem "puma", "~> 6"
diff --git a/react_on_rails_pro/docs/code-splitting-loadable-components.md b/react_on_rails_pro/docs/code-splitting-loadable-components.md
index bd06ea4a59..6adb0547ac 100644
--- a/react_on_rails_pro/docs/code-splitting-loadable-components.md
+++ b/react_on_rails_pro/docs/code-splitting-loadable-components.md
@@ -1,12 +1,14 @@
# Server-side rendering with code-splitting using Loadable/Components
+
by ShakaCode
-*Last updated September 19, 2022*
+_Last updated September 19, 2022_
## Introduction
+
The [React library recommends](https://loadable-components.com/docs/getting-started/) the use of React.lazy for code splitting with dynamic imports except
when using server-side rendering. In that case, as of February 2020, they recommend [Loadable Components](https://loadable-components.com)
-for server-side rendering with dynamic imports.
+for server-side rendering with dynamic imports.
Note, in 2019 and prior, the code-splitting feature was implemented using `react-loadable`. The React
team no longer recommends that library. The new way is far preferable.
@@ -18,7 +20,8 @@ yarn add @loadable/babel-plugin @loadable/component @loadable/server @loadable/
```
### Summary
-- [`@loadable/babel-plugin`](https://loadable-components.com/docs/getting-started/) - The plugin transforms your code to be ready for Server Side Rendering.
+
+- [`@loadable/babel-plugin`](https://loadable-components.com/docs/getting-started/) - The plugin transforms your code to be ready for Server Side Rendering.
- `@loadable/component` - Main library for creating loadable components.
- `@loadable/server` - Has functions for collecting chunks and provide style, script, link tags for the server.
- `@loadable/webpack-plugin` - The plugin to create a stats file with all chunks, assets information.
@@ -35,15 +38,16 @@ See example of server configuration differences in the loadable-components [exam
for server-side rendering](https://github.com/gregberge/loadable-components/blob/master/examples/server-side-rendering/webpack.config.babel.js)
You need to configure 3 things:
-1. `target`
- a. client-side: `web`
- b. server-side: `node`
+
+1. `target`
+ a. client-side: `web`
+ b. server-side: `node`
2. `output.libraryTarget`
- a. client-side: `undefined`
- b. server-side: `commonjs2`
-3. babel-loader options.caller = 'node' or 'web'
-3. `plugins`
- a. server-side: `new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })`
+ a. client-side: `undefined`
+ b. server-side: `commonjs2`
+3. babel-loader options.caller = 'node' or 'web'
+4. `plugins`
+ a. server-side: `new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })`
```js
{
@@ -58,14 +62,15 @@ You need to configure 3 things:
Explanation:
- `target: 'node'` is required to be able to run the server bundle with the dynamic import logic on nodejs.
-If that is not done, webpack will add and invoke browser-specific functions to fetch the chunks into the bundle, which throws an error on server-rendering.
+ If that is not done, webpack will add and invoke browser-specific functions to fetch the chunks into the bundle, which throws an error on server-rendering.
- `new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })`
-The react_on_rails_pro node-renderer expects only one single server-bundle. In other words, we cannot and do not want to split the server bundle.
+ The react_on_rails_pro node-renderer expects only one single server-bundle. In other words, we cannot and do not want to split the server bundle.
#### Client config
For the client config we only need to add the plugin:
+
```js
{
plugins: [
@@ -74,30 +79,33 @@ For the client config we only need to add the plugin:
]
}
```
+
This plugin collects all the information about entrypoints, chunks, and files, that have these chunks and creates a stats file during client bundle build.
This stats file is used later to map rendered components to file assets. While you can use any filename, our documentation will use the default name.
### Babel
Per [the docs](https://loadable-components.com/docs/babel-plugin/#transformation):
+
> The plugin transforms your code to be ready for Server Side Rendering
Add this to `babel.config.js`:
+
```js
{
"plugins": ["@loadable/babel-plugin"]
}
```
-https://loadable-components.com/docs/babel-plugin/
+https://loadable-components.com/docs/babel-plugin/
### Convert components into loadable components
Instead of importing the component directly, use a dynamic import:
```js
-import load from '@loadable/component'
-const MyComponent = load(() => import('./MyComponent'))
+import load from '@loadable/component';
+const MyComponent = load(() => import('./MyComponent'));
```
### Resolving issue with ChunkLoadError
@@ -118,22 +126,25 @@ const consoleDebug = (fn) => {
console.debug(fn());
}
};
-const retry = (fn, retryMessage = '', retriesLeft = 3, interval = 500) => new Promise((resolve, reject) => {
- fn()
- .then(resolve)
- .catch(() => {
- setTimeout(() => {
- if (retriesLeft === 1) {
- console.warn(`Maximum retries exceeded, retryMessage: ${retryMessage}. Reloading page...`);
- window.location.reload();
- return;
- }
- // Passing on "reject" is the important part
- consoleDebug(() => `Trying request, retryMessage: ${retryMessage}, retriesLeft: ${retriesLeft - 1}`);
- retry(fn, retryMessage, retriesLeft - 1, interval).then(resolve, reject);
- }, interval);
- });
-});
+const retry = (fn, retryMessage = '', retriesLeft = 3, interval = 500) =>
+ new Promise((resolve, reject) => {
+ fn()
+ .then(resolve)
+ .catch(() => {
+ setTimeout(() => {
+ if (retriesLeft === 1) {
+ console.warn(`Maximum retries exceeded, retryMessage: ${retryMessage}. Reloading page...`);
+ window.location.reload();
+ return;
+ }
+ // Passing on "reject" is the important part
+ consoleDebug(
+ () => `Trying request, retryMessage: ${retryMessage}, retriesLeft: ${retriesLeft - 1}`,
+ );
+ retry(fn, retryMessage, retriesLeft - 1, interval).then(resolve, reject);
+ }, interval);
+ });
+ });
export default retry;
```
@@ -152,21 +163,21 @@ const HomePage = loadable(() => retry(() => import('./HomePage')));
In the client bundle, we need to wrap the `hydrateRoot` call into a `loadableReady` function.
So, hydration will be fired only after all necessary chunks preloads. In this example below,
-`ClientApp` is registering as `App`.
+`ClientApp` is registering as `App`.
```js
import React from 'react';
import ReactOnRails from 'react-on-rails';
-import { hydrateRoot } from 'react-dom/client'
-import { loadableReady } from '@loadable/component'
+import { hydrateRoot } from 'react-dom/client';
+import { loadableReady } from '@loadable/component';
import App from './App';
const ClientApp = (props, railsContext, domId) => {
loadableReady(() => {
- const root = document.getElementById(domId)
+ const root = document.getElementById(domId);
hydrateRoot(root, );
- })
-}
+ });
+};
ReactOnRails.register({
App: ClientApp,
@@ -175,20 +186,20 @@ ReactOnRails.register({
#### Server
-The purpose of the server function is to collect all rendered chunks and pass them as script, link,
-style tags to the Rails view. In this example below, `ServerApp` is registering as `App`.
+The purpose of the server function is to collect all rendered chunks and pass them as script, link,
+style tags to the Rails view. In this example below, `ServerApp` is registering as `App`.
```js
import React from 'react';
import ReactOnRails from 'react-on-rails';
-import { ChunkExtractor } from '@loadable/server'
-import App from './App'
-import path from 'path'
+import { ChunkExtractor } from '@loadable/server';
+import App from './App';
+import path from 'path';
const ServerApp = (props, railsContext) => {
// This loadable-stats file was generated by `LoadablePlugin` in client webpack config.
// You must configure the path to resolve per your setup. If you are copying the file to
- // a remote server, the file should be a sibling of this file.
+ // a remote server, the file should be a sibling of this file.
// __dirname is going to be the directory where the server-bundle.js exists
// Note, React on Rails Pro automatically copies the loadable-stats.json to the same place as the
// server-bundle.js. Thus, the __dirname of this code is where we can find loadable-stats.json.
@@ -198,10 +209,10 @@ const ServerApp = (props, railsContext) => {
// This object is used to search filenames by corresponding chunk names.
// See https://loadable-components.com/docs/api-loadable-server/#chunkextractor
// for the entryPoints, pass an array of all your entryPoints using dynamic imports
- const extractor = new ChunkExtractor({ statsFile, entrypoints: ['client-bundle'] })
+ const extractor = new ChunkExtractor({ statsFile, entrypoints: ['client-bundle'] });
// It creates the wrapper `ChunkExtractorManager` around `App` to collect chunk names of rendered components.
- const jsx = extractor.collectChunks()
+ const jsx = extractor.collectChunks();
const componentHtml = renderToString(jsx);
@@ -211,8 +222,8 @@ const ServerApp = (props, railsContext) => {
// Returns all the files with rendered chunks for furture insert into rails view.
linkTags: extractor.getLinkTags(),
styleTags: extractor.getStyleTags(),
- scriptTags: extractor.getScriptTags()
- }
+ scriptTags: extractor.getScriptTags(),
+ },
};
};
@@ -224,6 +235,7 @@ ReactOnRails.register({
## Configure react_on_rails_pro
### React on Rails Pro
+
You must set `config.assets_top_copy` so that the node-renderer will have access to the loadable-stats.json.
```ruby
@@ -233,15 +245,16 @@ You must set `config.assets_top_copy` so that the node-renderer will have access
Your server rendering code, per the above, will find this file like this:
```js
- const statsFile = path.resolve(__dirname, 'loadable-stats.json');
-```
+const statsFile = path.resolve(__dirname, 'loadable-stats.json');
+```
Note, if `__dirname` is not working in your webpack build, that's because you didn't set `node: false`
in your webpack configuration. That turns off the polyfills for things like `__dirname`.
-
### Node Renderer
+
In your `node-renderer.js` file which runs node renderer, you need to specify `supportModules` options as follows:
+
```js
const path = require('path');
const env = process.env;
@@ -261,7 +274,7 @@ reactOnRailsProNodeRenderer(config);
```erb
<% res = react_component_hash("App", props: {}, prerender: true) %>
<%= content_for :link_tags, res['linkTags'] %>
-<%= content_for :style_tags, res['styleTags'] %>
+<%= content_for :style_tags, res['styleTags'] %>
<%= res['componentHtml'].html_safe %>
@@ -269,6 +282,7 @@ reactOnRailsProNodeRenderer(config);
```
## Making HMR Work
+
To make HMR work, it's best to disable loadable-components when using the Dev Server.
Note: you will need access to our **private** React on Rails Pro repository to open the following links.
@@ -277,9 +291,11 @@ Take a look at the code searches for ['imports-loadable'](https://github.com/sha
The general concept is that we have a non-loadable, HMR-ready, file that substitutes for the loadable-enabled one, with the suffixes `imports-hmr.js` instead of `imports-loadable.js`
### Webpack configuration
+
Use the [NormalModuleReplacement plugin](https://webpack.js.org/plugins/normal-module-replacement-plugin/):
[code](https://github.com/shakacode/react_on_rails_pro/blob/a361f4e163b9170f180ae07ee312fb9b4c719fc3/spec/dummy/config/webpack/environment.js#L81-L91)
+
```js
if (isWebpackDevServer) {
environment.plugins.append(
@@ -305,7 +321,7 @@ Note: you will need access to our **private** React on Rails Pro repository to o
### Client-Side Startup
- [spec/dummy/client/app/loadable/loadable-client.imports-hmr.js](https://github.com/shakacode/react_on_rails_pro/blob/master/spec/dummy/client/app/loadable/loadable-client.imports-hmr.js)
-- [spec/dummy/client/app/loadable/loadable-client.imports-loadable.js](https://github.com/shakacode/react_on_rails_pro/blob/master/spec/dummy/client/app/loadable/loadable-client.imports-loadable.js)
+- [spec/dummy/client/app/loadable/loadable-client.imports-loadable.jsx](https://github.com/shakacode/react_on_rails_pro/blob/master/spec/dummy/client/app/loadable/loadable-client.imports-loadable.jsx)
### Server-Side Startup
diff --git a/react_on_rails_pro/spec/dummy/Gemfile.lock b/react_on_rails_pro/spec/dummy/Gemfile.lock
index f865a0a92f..fa6aa67d8f 100644
--- a/react_on_rails_pro/spec/dummy/Gemfile.lock
+++ b/react_on_rails_pro/spec/dummy/Gemfile.lock
@@ -114,9 +114,9 @@ GEM
io-event (~> 1.11)
metrics (~> 0.12)
traces (~> 0.18)
- base64 (0.2.0)
- benchmark (0.4.0)
- bigdecimal (3.1.9)
+ base64 (0.3.0)
+ benchmark (0.5.0)
+ bigdecimal (3.3.1)
bindex (0.8.1)
bootsnap (1.18.3)
msgpack (~> 1.2)
@@ -137,7 +137,7 @@ GEM
childprocess (5.0.0)
coderay (1.1.3)
concurrent-ruby (1.3.5)
- connection_pool (2.5.0)
+ connection_pool (2.5.5)
console (1.34.2)
fiber-annotation
fiber-local (~> 1.1)
@@ -154,12 +154,13 @@ GEM
crass (1.0.6)
csso-rails (1.0.0)
execjs (>= 1)
- date (3.4.1)
+ date (3.5.0)
diff-lcs (1.5.1)
docile (1.4.0)
- drb (2.2.1)
+ drb (2.2.3)
equivalent-xml (0.6.0)
nokogiri (>= 1.4.3)
+ erb (6.0.0)
erubi (1.13.1)
execjs (2.9.1)
fakefs (2.8.0)
@@ -197,9 +198,9 @@ GEM
http-2 (>= 1.0.0)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
- io-console (0.8.0)
+ io-console (0.8.1)
io-event (1.14.2)
- irb (1.15.1)
+ irb (1.15.3)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
@@ -219,8 +220,8 @@ GEM
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
- logger (1.6.6)
- loofah (2.24.0)
+ logger (1.7.0)
+ loofah (2.24.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@@ -233,8 +234,8 @@ GEM
method_source (1.1.0)
metrics (0.15.0)
mini_mime (1.1.5)
- mini_portile2 (2.8.8)
- minitest (5.25.4)
+ mini_portile2 (2.8.9)
+ minitest (5.26.2)
mize (0.4.1)
protocol (~> 2.0)
msgpack (1.7.2)
@@ -250,24 +251,24 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
- nokogiri (1.18.8)
+ nokogiri (1.18.10)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
- nokogiri (1.18.8-aarch64-linux-gnu)
+ nokogiri (1.18.10-aarch64-linux-gnu)
racc (~> 1.4)
- nokogiri (1.18.8-aarch64-linux-musl)
+ nokogiri (1.18.10-aarch64-linux-musl)
racc (~> 1.4)
- nokogiri (1.18.8-arm-linux-gnu)
+ nokogiri (1.18.10-arm-linux-gnu)
racc (~> 1.4)
- nokogiri (1.18.8-arm-linux-musl)
+ nokogiri (1.18.10-arm-linux-musl)
racc (~> 1.4)
- nokogiri (1.18.8-arm64-darwin)
+ nokogiri (1.18.10-arm64-darwin)
racc (~> 1.4)
- nokogiri (1.18.8-x86_64-darwin)
+ nokogiri (1.18.10-x86_64-darwin)
racc (~> 1.4)
- nokogiri (1.18.8-x86_64-linux-gnu)
+ nokogiri (1.18.10-x86_64-linux-gnu)
racc (~> 1.4)
- nokogiri (1.18.8-x86_64-linux-musl)
+ nokogiri (1.18.10-x86_64-linux-musl)
racc (~> 1.4)
package_json (0.2.0)
parallel (1.25.1)
@@ -275,7 +276,7 @@ GEM
ast (~> 2.4.1)
racc
pg (1.5.6)
- pp (0.6.2)
+ pp (0.6.3)
prettyprint
prettyprint (0.2.0)
prism-rails (1.5.0)
@@ -292,17 +293,17 @@ GEM
pry (>= 0.13.0)
pry-theme (1.3.1)
coderay (~> 1.1)
- psych (5.2.3)
+ psych (5.2.6)
date
stringio
public_suffix (6.0.0)
puma (6.5.0)
nio4r (~> 2.0)
racc (1.8.1)
- rack (3.1.12)
+ rack (3.1.19)
rack-proxy (0.7.7)
rack
- rack-session (2.1.0)
+ rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
@@ -323,7 +324,7 @@ GEM
activesupport (= 7.2.2.1)
bundler (>= 1.15.0)
railties (= 7.2.2.1)
- rails-dom-testing (2.2.0)
+ rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
@@ -339,20 +340,22 @@ GEM
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
- rake (13.2.1)
+ rake (13.3.1)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
rbs (3.9.5)
logger
- rdoc (6.12.0)
+ rdoc (6.16.0)
+ erb
psych (>= 4.0.0)
+ tsort
redis (5.4.0)
redis-client (>= 0.22.0)
redis-client (0.24.0)
connection_pool
regexp_parser (2.9.2)
- reline (0.6.0)
+ reline (0.6.3)
io-console (~> 0.5)
rexml (3.3.9)
rspec-core (3.13.0)
@@ -422,7 +425,7 @@ GEM
websocket (~> 1.0)
semantic_range (3.1.0)
sexp_processor (4.17.1)
- shakapacker (9.3.0)
+ shakapacker (9.4.0)
activesupport (>= 5.2)
package_json
rack-proxy (>= 0.6.1)
@@ -452,18 +455,19 @@ GEM
sqlite3 (1.7.3-x86-linux)
sqlite3 (1.7.3-x86_64-darwin)
sqlite3 (1.7.3-x86_64-linux)
- stringio (3.1.2)
+ stringio (3.1.8)
sync (0.5.0)
term-ansicolor (1.10.2)
mize
tins (~> 1.0)
- thor (1.3.2)
+ thor (1.4.0)
tilt (2.4.0)
timeout (0.4.3)
tins (1.33.0)
bigdecimal
sync
traces (0.18.2)
+ tsort (0.2.0)
turbolinks (5.2.1)
turbolinks-source (~> 5.2)
turbolinks-source (5.2.0)
@@ -495,7 +499,7 @@ GEM
xpath (3.2.0)
nokogiri (~> 1.8)
yard (0.9.36)
- zeitwerk (2.7.1)
+ zeitwerk (2.7.3)
PLATFORMS
aarch64-linux
@@ -556,7 +560,7 @@ DEPENDENCIES
sass-rails
scss_lint
selenium-webdriver (= 4.9.0)
- shakapacker (= 9.3.0)
+ shakapacker (= 9.4.0)
spring
spring-watcher-listen
sprockets
diff --git a/react_on_rails_pro/spec/dummy/app/views/pages/pro/loadable_component.html.erb b/react_on_rails_pro/spec/dummy/app/views/pages/pro/loadable_component.html.erb
index d59d793283..c1f8366d9e 100644
--- a/react_on_rails_pro/spec/dummy/app/views/pages/pro/loadable_component.html.erb
+++ b/react_on_rails_pro/spec/dummy/app/views/pages/pro/loadable_component.html.erb
@@ -23,7 +23,7 @@
Client code
-<%= ApplicationHelper::include_code "loadable/loadable-client.imports-loadable.js" %>
+<%= ApplicationHelper::include_code "loadable/loadable-client.imports-loadable.jsx" %>
diff --git a/react_on_rails_pro/spec/dummy/client/app/components/Loadable/Header.jsx b/react_on_rails_pro/spec/dummy/client/app/components/Loadable/Header.jsx
index 954dfed2b7..d1798aae96 100644
--- a/react_on_rails_pro/spec/dummy/client/app/components/Loadable/Header.jsx
+++ b/react_on_rails_pro/spec/dummy/client/app/components/Loadable/Header.jsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Helmet } from 'react-helmet';
+import { Helmet } from '@dr.pogodin/react-helmet';
import ActiveLink from './ActiveLink';
const Header = () => (
diff --git a/react_on_rails_pro/spec/dummy/client/app/components/Loadable/pages/A.jsx b/react_on_rails_pro/spec/dummy/client/app/components/Loadable/pages/A.jsx
index a35a4e3aa5..e5bf43bbaa 100644
--- a/react_on_rails_pro/spec/dummy/client/app/components/Loadable/pages/A.jsx
+++ b/react_on_rails_pro/spec/dummy/client/app/components/Loadable/pages/A.jsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Helmet } from 'react-helmet';
+import { Helmet } from '@dr.pogodin/react-helmet';
export default () => (
<>
diff --git a/react_on_rails_pro/spec/dummy/client/app/components/Loadable/pages/B.jsx b/react_on_rails_pro/spec/dummy/client/app/components/Loadable/pages/B.jsx
index c31b3cd3d1..70ca174de6 100644
--- a/react_on_rails_pro/spec/dummy/client/app/components/Loadable/pages/B.jsx
+++ b/react_on_rails_pro/spec/dummy/client/app/components/Loadable/pages/B.jsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Helmet } from 'react-helmet';
+import { Helmet } from '@dr.pogodin/react-helmet';
export default () => (
<>
diff --git a/react_on_rails_pro/spec/dummy/client/app/components/ReactHelmet.jsx b/react_on_rails_pro/spec/dummy/client/app/components/ReactHelmet.jsx
index e4b8c36589..7067cb7fd3 100644
--- a/react_on_rails_pro/spec/dummy/client/app/components/ReactHelmet.jsx
+++ b/react_on_rails_pro/spec/dummy/client/app/components/ReactHelmet.jsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Helmet } from 'react-helmet';
+import { Helmet } from '@dr.pogodin/react-helmet';
import HelloWorld from '../ror-auto-load-components/HelloWorld';
import { consistentKeysReplacer } from '../utils/json';
diff --git a/react_on_rails_pro/spec/dummy/client/app/loadable/loadable-client.imports-hmr.jsx b/react_on_rails_pro/spec/dummy/client/app/loadable/loadable-client.imports-hmr.jsx
index 226eb45b52..3479440963 100644
--- a/react_on_rails_pro/spec/dummy/client/app/loadable/loadable-client.imports-hmr.jsx
+++ b/react_on_rails_pro/spec/dummy/client/app/loadable/loadable-client.imports-hmr.jsx
@@ -1,6 +1,12 @@
import React from 'react';
+import { HelmetProvider } from '@dr.pogodin/react-helmet';
import Loadable from './LoadableApp';
-const WrappedLoadable = (props, railsContext) => () => ;
+// HelmetProvider is required by @dr.pogodin/react-helmet for both client and server rendering
+const WrappedLoadable = (props, railsContext) => () => (
+
+
+
+);
export default WrappedLoadable;
diff --git a/react_on_rails_pro/spec/dummy/client/app/loadable/loadable-client.imports-loadable.js b/react_on_rails_pro/spec/dummy/client/app/loadable/loadable-client.imports-loadable.jsx
similarity index 59%
rename from react_on_rails_pro/spec/dummy/client/app/loadable/loadable-client.imports-loadable.js
rename to react_on_rails_pro/spec/dummy/client/app/loadable/loadable-client.imports-loadable.jsx
index da7b0046ec..b630825756 100644
--- a/react_on_rails_pro/spec/dummy/client/app/loadable/loadable-client.imports-loadable.js
+++ b/react_on_rails_pro/spec/dummy/client/app/loadable/loadable-client.imports-loadable.jsx
@@ -2,13 +2,19 @@ import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import { loadableReady } from '@loadable/component';
+import { HelmetProvider } from '@dr.pogodin/react-helmet';
import ClientApp from './LoadableApp';
const App = (props, railsContext, domNodeId) => {
loadableReady(() => {
const el = document.getElementById(domNodeId);
- hydrateRoot(el, React.createElement(ClientApp, { ...props, path: railsContext.pathname }));
+ hydrateRoot(
+ el,
+
+ {React.createElement(ClientApp, { ...props, path: railsContext.pathname })}
+ ,
+ );
});
};
diff --git a/react_on_rails_pro/spec/dummy/client/app/loadable/loadable-server.imports-hmr.jsx b/react_on_rails_pro/spec/dummy/client/app/loadable/loadable-server.imports-hmr.jsx
index 8cd4c5f6d6..bac3c1461a 100644
--- a/react_on_rails_pro/spec/dummy/client/app/loadable/loadable-server.imports-hmr.jsx
+++ b/react_on_rails_pro/spec/dummy/client/app/loadable/loadable-server.imports-hmr.jsx
@@ -1,21 +1,28 @@
import React from 'react';
import { renderToString } from 'react-dom/server';
-import { Helmet } from 'react-helmet';
+import { HelmetProvider } from '@dr.pogodin/react-helmet';
import App from './LoadableApp';
// Version of the consumer app to use without loadable components to enable HMR
const hmrApp = (props, railsContext) => {
- const componentHtml = renderToString(React.createElement(App, { ...props, path: railsContext.pathname }));
- const helmet = Helmet.renderStatic();
+ // For server-side rendering with @dr.pogodin/react-helmet, we pass a context object
+ // to HelmetProvider to capture the helmet data per-request (thread-safe)
+ const helmetContext = {};
+ const componentHtml = renderToString(
+
+ {React.createElement(App, { ...props, path: railsContext.pathname })}
+ ,
+ );
+ const { helmet } = helmetContext;
return {
renderedHtml: {
componentHtml,
- link: helmet.link.toString(),
- meta: helmet.meta.toString(),
- style: helmet.style.toString(),
- title: helmet.title.toString(),
+ link: helmet ? helmet.link.toString() : '',
+ meta: helmet ? helmet.meta.toString() : '',
+ style: helmet ? helmet.style.toString() : '',
+ title: helmet ? helmet.title.toString() : '',
},
};
};
diff --git a/react_on_rails_pro/spec/dummy/client/app/loadable/loadable-server.imports-loadable.jsx b/react_on_rails_pro/spec/dummy/client/app/loadable/loadable-server.imports-loadable.jsx
index 095d236ea4..19b50d0a58 100644
--- a/react_on_rails_pro/spec/dummy/client/app/loadable/loadable-server.imports-loadable.jsx
+++ b/react_on_rails_pro/spec/dummy/client/app/loadable/loadable-server.imports-loadable.jsx
@@ -4,7 +4,7 @@ import path from 'path';
import React from 'react';
import { ChunkExtractor } from '@loadable/server';
import { renderToString } from 'react-dom/server';
-import { Helmet } from 'react-helmet';
+import { HelmetProvider } from '@dr.pogodin/react-helmet';
import App from './LoadableApp';
@@ -14,19 +14,28 @@ const loadableApp = (props, railsContext) => {
const statsFile = path.resolve(__dirname, 'loadable-stats.json');
const extractor = new ChunkExtractor({ entrypoints: ['client-bundle'], statsFile });
const { pathname } = railsContext;
- const componentHtml = renderToString(extractor.collectChunks());
- const helmet = Helmet.renderStatic();
+ // For server-side rendering with @dr.pogodin/react-helmet, we pass a context object
+ // to HelmetProvider to capture the helmet data per-request (thread-safe)
+ const helmetContext = {};
+ const componentHtml = renderToString(
+ extractor.collectChunks(
+
+
+ ,
+ ),
+ );
+ const { helmet } = helmetContext;
return {
renderedHtml: {
componentHtml,
- link: helmet.link.toString(),
+ link: helmet ? helmet.link.toString() : '',
linkTags: extractor.getLinkTags(),
styleTags: extractor.getStyleTags(),
- meta: helmet.meta.toString(),
+ meta: helmet ? helmet.meta.toString() : '',
scriptTags: extractor.getScriptTags(),
- style: helmet.style.toString(),
- title: helmet.title.toString(),
+ style: helmet ? helmet.style.toString() : '',
+ title: helmet ? helmet.title.toString() : '',
},
};
};
diff --git a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/ReactHelmetApp.client.jsx b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/ReactHelmetApp.client.jsx
index 4597810353..b24a1fa073 100644
--- a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/ReactHelmetApp.client.jsx
+++ b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/ReactHelmetApp.client.jsx
@@ -2,10 +2,14 @@
// Top level component for simple client side only rendering
import React from 'react';
+import { HelmetProvider } from '@dr.pogodin/react-helmet';
import ReactHelmet from '../components/ReactHelmet';
const stubbedResponse = { name: 'ReactOnRails', country: [], count: 0 };
+// HelmetProvider is required by @dr.pogodin/react-helmet for both client and server rendering
export default (props, _railsContext) => () => (
-
+
+
+
);
diff --git a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/ReactHelmetApp.server.jsx b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/ReactHelmetApp.server.jsx
index a60264919b..8b6e2106ee 100644
--- a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/ReactHelmetApp.server.jsx
+++ b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/ReactHelmetApp.server.jsx
@@ -5,7 +5,7 @@ import 'cross-fetch/polyfill';
// Top level component for simple client side only rendering
import React from 'react';
import { renderToString } from 'react-dom/server';
-import { Helmet } from 'react-helmet';
+import { HelmetProvider } from '@dr.pogodin/react-helmet';
import ReactHelmet from '../components/ReactHelmet';
/*
@@ -28,12 +28,19 @@ export default async (props, _railsContext) => {
console.error(`There was an error doing an API request during server rendering: ${error}`),
);
- const componentHtml = renderToString();
- const helmet = Helmet.renderStatic();
+ // For server-side rendering with @dr.pogodin/react-helmet, we pass a context object
+ // to HelmetProvider to capture the helmet data per-request (thread-safe)
+ const helmetContext = {};
+ const componentHtml = renderToString(
+
+
+ ,
+ );
+ const { helmet } = helmetContext;
const promiseObject = {
componentHtml,
- title: helmet.title.toString(),
+ title: helmet ? helmet.title.toString() : '',
};
return promiseObject;
};
diff --git a/react_on_rails_pro/spec/dummy/package.json b/react_on_rails_pro/spec/dummy/package.json
index bfe782fc95..bec8906f88 100644
--- a/react_on_rails_pro/spec/dummy/package.json
+++ b/react_on_rails_pro/spec/dummy/package.json
@@ -50,7 +50,7 @@
"react": "19.0.0",
"react-dom": "19.0.0",
"react-error-boundary": "^4.1.2",
- "react-helmet": "^6.0.0-beta.2",
+ "@dr.pogodin/react-helmet": "^3.0.4",
"react-on-rails-pro": "link:.yalc/react-on-rails-pro",
"react-on-rails-rsc": "^19.0.2",
"react-proptypes": "^1.0.0",
@@ -63,7 +63,7 @@
"sass": "^1.43.4",
"sass-loader": "^12.3.0",
"sass-resources-loader": "^2.0.0",
- "shakapacker": "9.3.0",
+ "shakapacker": "9.4.0",
"style-loader": "^3.3.1",
"tailwindcss": "^3.2.7",
"terser-webpack-plugin": "5",
diff --git a/script/convert b/script/convert
index b99413323f..c3fcc39b01 100755
--- a/script/convert
+++ b/script/convert
@@ -47,6 +47,19 @@ gsub_file_content("../package.json", /"react": "[^"]*",/, '"react": "18.0.0",')
gsub_file_content("../package.json", /"react-dom": "[^"]*",/, '"react-dom": "18.0.0",')
gsub_file_content("../react_on_rails/spec/dummy/package.json", /"react": "[^"]*",/, '"react": "18.0.0",')
gsub_file_content("../react_on_rails/spec/dummy/package.json", /"react-dom": "[^"]*",/, '"react-dom": "18.0.0",')
+
+# Switch from @dr.pogodin/react-helmet (React 19+) to react-helmet-async (React 18 compatible)
+# Both have the same HelmetProvider API, but different package names
+gsub_file_content("../react_on_rails/spec/dummy/package.json", /"@dr\.pogodin\/react-helmet": "[^"]*",/, '"react-helmet-async": "^1.3.0",')
+# Update import statements in all client files
+Dir.glob(File.expand_path("../react_on_rails/spec/dummy/client/**/*.{js,jsx,ts,tsx}", __dir__)).each do |file|
+ content = File.binread(file)
+ if content.include?("@dr.pogodin/react-helmet")
+ content.gsub!(%r{from ['"]@dr\.pogodin/react-helmet['"]}, 'from "react-helmet-async"')
+ content.gsub!(%r{['"]@dr\.pogodin/react-helmet['"]}, '"react-helmet-async"')
+ File.binwrite(file, content)
+ end
+end
gsub_file_content(
"../packages/react-on-rails-pro/package.json",
/"test:non-rsc": "(?:\\"|[^"])*",/,