Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,13 @@

# Ignore master key for decrypting credentials and more.
/config/master.key

/public/packs
/public/packs-test
/node_modules
/yarn-error.log
yarn-debug.log*
.yarn-integrity

# Generated React on Rails packs
**/generated/**
6 changes: 6 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,9 @@ group :test do
gem "capybara"
gem "selenium-webdriver"
end

# gem "react_on_rails", "= 15.0"

gem "react_on_rails", path: "../../react_on_rails"

gem "shakapacker", path: "/Users/justin/shakacode/shakapacker/shakapacker-popmenu"
Comment on lines +59 to +61
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Absolute path gems will break CI and other machines—gate via ENV and provide fallback

Local paths are non-portable. Add ENV-gated overrides with a default to published gems.

Apply this diff:

-# gem "react_on_rails", "= 15.0"
-
-gem "react_on_rails", path: "../../react_on_rails"
-
-gem "shakapacker", path: "/Users/justin/shakacode/shakapacker/shakapacker-popmenu"
+# Prefer released gems; allow local overrides via ENV for development
+if ENV['REACT_ON_RAILS_PATH']
+  gem "react_on_rails", path: ENV['REACT_ON_RAILS_PATH']
+else
+  gem "react_on_rails"
+end
+
+if ENV['SHAKAPACKER_PATH']
+  gem "shakapacker", path: ENV['SHAKAPACKER_PATH']
+else
+  gem "shakapacker"
+end

Alternatively, keep Gemfile clean and use Bundler locals:
bundle config local.react_on_rails ../../react_on_rails and
bundle config local.shakapacker /path/to/shakapacker.

Please confirm CI can bundle without local paths.


🏁 Script executed:

#!/bin/bash
# Display lines around the local gem definitions in Gemfile
sed -n '50,80p' Gemfile

echo -e "\nSearching for Bundler local override configurations..."
rg -n "bundle config local.react_on_rails" -n .
rg -n "bundle config local.shakapacker" -n .

echo -e "\nChecking for .bundle/config file..."
if [ -f .bundle/config ]; then
  echo ".bundle/config contents:"
  sed -n '1,200p' .bundle/config
else
  echo ".bundle/config not found"
fi

Length of output: 435


Absolute local gem paths will break CI — gate via ENV or use Bundler locals

Gemfile contains local paths for react_on_rails and shakapacker (lines 59–61); .bundle/config not found — CI/other machines will fail to bundle.

Apply this diff:

-# gem "react_on_rails", "= 15.0"
-
-gem "react_on_rails", path: "../../react_on_rails"
-
-gem "shakapacker", path: "/Users/justin/shakacode/shakapacker/shakapacker-popmenu"
+# Prefer released gems; allow local overrides via ENV for development
+if ENV['REACT_ON_RAILS_PATH']
+  gem "react_on_rails", path: ENV['REACT_ON_RAILS_PATH']
+else
+  gem "react_on_rails"
+end
+
+if ENV['SHAKAPACKER_PATH']
+  gem "shakapacker", path: ENV['SHAKAPACKER_PATH']
+else
+  gem "shakapacker"
+end

Alternatively set per-developer bundler locals: bundle config local.react_on_rails ../../react_on_rails and bundle config local.shakapacker /path/to/shakapacker.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
gem "react_on_rails", path: "../../react_on_rails"
gem "shakapacker", path: "/Users/justin/shakacode/shakapacker/shakapacker-popmenu"
# Prefer released gems; allow local overrides via ENV for development
if ENV['REACT_ON_RAILS_PATH']
gem "react_on_rails", path: ENV['REACT_ON_RAILS_PATH']
else
gem "react_on_rails"
end
if ENV['SHAKAPACKER_PATH']
gem "shakapacker", path: ENV['SHAKAPACKER_PATH']
else
gem "shakapacker"
end
🤖 Prompt for AI Agents
Gemfile around lines 59 to 61: the Gemfile currently hardcodes local paths for
react_on_rails and shakapacker which will break CI and other machines; update
the Gemfile to conditionally use local paths only when a developer enables them
via an ENV flag (e.g. if ENV["USE_LOCAL_GEMS"] then use path: "..."), otherwise
depend on the published gems, or remove the path entries and document using
per-developer Bundler locals (bundle config local.react_on_rails
../../react_on_rails and bundle config local.shakapacker /path/to/shakapacker);
ensure CI does not set the ENV flag and add a short comment explaining the
developer-local workflow.

28 changes: 28 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
PATH
remote: ../../react_on_rails
specs:
react_on_rails (16.0.1.rc.2)
addressable
connection_pool
execjs (~> 2.5)
rails (>= 5.2)
rainbow (~> 3.0)
shakapacker (>= 6.0)

PATH
remote: /Users/justin/shakacode/shakapacker/shakapacker-popmenu
specs:
shakapacker (8.4.0)
activesupport (>= 5.2)
package_json
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)

GEM
remote: https://rubygems.org/
specs:
Expand Down Expand Up @@ -110,6 +131,7 @@ GEM
erubi (1.13.1)
et-orbi (1.3.0)
tzinfo
execjs (2.10.0)
fugit (1.11.2)
et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4)
Expand Down Expand Up @@ -185,6 +207,7 @@ GEM
nokogiri (1.18.9-x86_64-linux-musl)
racc (~> 1.4)
ostruct (0.6.3)
package_json (0.1.0)
parallel (1.27.0)
parser (3.3.9.0)
ast (~> 2.4.1)
Expand All @@ -202,6 +225,8 @@ GEM
raabro (1.4.0)
racc (1.8.1)
rack (3.2.1)
rack-proxy (0.7.7)
rack
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
Expand Down Expand Up @@ -284,6 +309,7 @@ GEM
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 4.0)
websocket (~> 1.0)
semantic_range (3.1.0)
solid_cable (3.0.12)
actioncable (>= 7.2)
activejob (>= 7.2)
Expand Down Expand Up @@ -365,8 +391,10 @@ DEPENDENCIES
kamal
puma (>= 5.0)
rails (~> 8.0.2, >= 8.0.2.1)
react_on_rails!
rubocop-rails-omakase
selenium-webdriver
shakapacker!
solid_cable
solid_cache
solid_queue
Expand Down
5 changes: 5 additions & 0 deletions Procfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Procfile for development using HMR (SSR enabled)
# You can run these commands in separate shells
rails: bundle exec rails s -p 3000
wp-client: WEBPACK_SERVE=true bin/shakapacker-dev-server
wp-server: SERVER_BUNDLE_ONLY=true bin/shakapacker --watch
8 changes: 8 additions & 0 deletions Procfile.dev-prod-assets
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Procfile for development with production assets
# Uses production-optimized, precompiled assets with development environment
# Uncomment additional processes as needed for your app

rails: bundle exec rails s -p 3001
# sidekiq: bundle exec sidekiq -C config/sidekiq.yml
# redis: redis-server
# mailcatcher: mailcatcher --foreground
2 changes: 2 additions & 0 deletions Procfile.dev-static-assets
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
web: bin/rails server -p 3000
js: bin/shakapacker --watch
9 changes: 9 additions & 0 deletions app/controllers/hello_world_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

class HelloWorldController < ApplicationController
layout "hello_world"

def index
@hello_world_props = { name: "Stranger" }
end
end
15 changes: 15 additions & 0 deletions app/javascript/packs/application.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* eslint no-console:0 */
// This file is automatically compiled by Webpack, along with any other files
// present in this directory. You're encouraged to place your actual application logic in
// a relevant structure within app/javascript and only use these pack files to reference
// that code so it'll be compiled.
//
// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate
// layout file, like app/views/layouts/application.html.erb

// Uncomment to copy all static images under ./images to the output folder and reference
// them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>)
// or the `imagePath` JavaScript helper below.
//
// const images = require.context('./images', true)
// const imagePath = (name) => images(name, true)
3 changes: 3 additions & 0 deletions app/javascript/packs/server-bundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// import statement added by react_on_rails:generate_packs rake task
import "./../generated/server-bundle-generated.js"
// Placeholder comment - auto-generated imports will be prepended here by react_on_rails:generate_packs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.bright {
color: green;
font-weight: bold;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* eslint-disable import/prefer-default-export */

import { HELLO_WORLD_NAME_UPDATE } from '../constants/helloWorldConstants';

// Action interface
export interface UpdateNameAction {
type: typeof HELLO_WORLD_NAME_UPDATE;
text: string;
}

// Union type for all actions
export type HelloWorldAction = UpdateNameAction;

// Action creator with proper TypeScript typing
export const updateName = (text: string): UpdateNameAction => ({
type: HELLO_WORLD_NAME_UPDATE,
text,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.bright {
color: green;
font-weight: bold;
}
24 changes: 24 additions & 0 deletions app/javascript/src/HelloWorldApp/components/HelloWorld.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import * as style from './HelloWorld.module.css';
import type { PropsFromRedux } from '../containers/HelloWorldContainer';

// Component props are inferred from Redux container
type HelloWorldProps = PropsFromRedux;

const HelloWorld: React.FC<HelloWorldProps> = ({ name, updateName }) => (
<div>
<h3>
Hello,
{name}!
</h3>
<hr />
<form>
<label className={style.bright} htmlFor="name">
Say hello to:
<input id="name" type="text" value={name} onChange={(e) => updateName(e.target.value)} />
</label>
</form>
</div>
);

export default HelloWorld;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* eslint-disable import/prefer-default-export */

export const HELLO_WORLD_NAME_UPDATE = 'HELLO_WORLD_NAME_UPDATE' as const;

// Action type for TypeScript
export type HelloWorldActionType = typeof HELLO_WORLD_NAME_UPDATE;
20 changes: 20 additions & 0 deletions app/javascript/src/HelloWorldApp/containers/HelloWorldContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Simple example of a React "smart" component

import { connect, ConnectedProps } from 'react-redux';
import HelloWorld from '../components/HelloWorld';
import * as actions from '../actions/helloWorldActionCreators';
import type { HelloWorldState } from '../reducers/helloWorldReducer';

// Which part of the Redux global state does our component want to receive as props?
const mapStateToProps = (state: HelloWorldState) => ({ name: state.name });

// Create the connector
const connector = connect(mapStateToProps, actions);

// Infer the props from Redux state and actions
export type PropsFromRedux = ConnectedProps<typeof connector>;

// Don't forget to actually use connect!
// Note that we don't export HelloWorld, but the redux "connected" version of it.
// See https://github.com/reactjs/react-redux/blob/master/docs/api.md#examples
export default connector(HelloWorld);
22 changes: 22 additions & 0 deletions app/javascript/src/HelloWorldApp/reducers/helloWorldReducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { combineReducers } from 'redux';
import { HELLO_WORLD_NAME_UPDATE } from '../constants/helloWorldConstants';
import { HelloWorldAction } from '../actions/helloWorldActionCreators';

// State interface
export interface HelloWorldState {
name: string;
}

// Individual reducer with TypeScript types
const name = (state: string = '', action: HelloWorldAction): string => {
switch (action.type) {
case HELLO_WORLD_NAME_UPDATE:
return action.text;
default:
return state;
}
};

const helloWorldReducer = combineReducers<HelloWorldState>({ name });

export default helloWorldReducer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useMemo, type FC } from 'react';
import { Provider } from 'react-redux';

import configureStore, { type RailsProps } from '../store/helloWorldStore';
import HelloWorldContainer from '../containers/HelloWorldContainer';

// Props interface matches what Rails will pass from the controller
interface HelloWorldAppProps extends RailsProps {}

// See documentation for https://github.com/reactjs/react-redux.
// This is how you get props from the Rails view into the redux store.
// This code here binds your smart component to the redux store.
const HelloWorldApp: FC<HelloWorldAppProps> = (props) => {
const store = useMemo(() => configureStore(props), [props]);

return (
<Provider store={store}>
<HelloWorldContainer />
</Provider>
);
};

export default HelloWorldApp;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import HelloWorldApp from './HelloWorldApp.client';
// This could be specialized for server rendering
// For example, if using React Router, we'd have the SSR setup here.

export default HelloWorldApp;
18 changes: 18 additions & 0 deletions app/javascript/src/HelloWorldApp/store/helloWorldStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createStore } from 'redux';
import type { Store, PreloadedState } from 'redux';
import helloWorldReducer from '../reducers/helloWorldReducer';
import type { HelloWorldState } from '../reducers/helloWorldReducer';

// Rails props interface - customize based on your Rails controller
export interface RailsProps {
name: string;
[key: string]: any; // Allow additional props from Rails
}

// Store type
export type HelloWorldStore = Store<HelloWorldState>;

const configureStore = (railsProps: RailsProps): HelloWorldStore =>
createStore(helloWorldReducer, railsProps as PreloadedState<HelloWorldState>);

export default configureStore;
15 changes: 15 additions & 0 deletions app/javascript/types/css-modules.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// TypeScript definitions for CSS modules
declare module "*.module.css" {
const classes: { [key: string]: string };
export default classes;
}

declare module "*.module.scss" {
const classes: { [key: string]: string };
export default classes;
}

declare module "*.module.sass" {
const classes: { [key: string]: string };
export default classes;
}
2 changes: 2 additions & 0 deletions app/views/hello_world/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<h1>Hello World</h1>
<%= react_component("HelloWorldApp", props: @hello_world_props, prerender: true) %>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Verify component registration for SSR.

Ensure HelloWorldApp is registered with React on Rails in both client and server bundles; otherwise SSR will raise “Component not registered.”


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
rg -nC2 "ReactOnRails\.register\((\s*)\{?.*HelloWorldApp" app/javascript || echo "Registration not found"
rg -n "HelloWorldApp" app/javascript | wc -l

Length of output: 24


Register HelloWorldApp with ReactOnRails for SSR.

No ReactOnRails.register for HelloWorldApp was found under app/javascript (script output: "Registration not found"; "HelloWorldApp" occurrences: 5). app/views/hello_world/index.html.erb uses prerender: true — add the ReactOnRails registration for HelloWorldApp in both the server and client entrypoints under app/javascript so SSR can locate the component.

1 change: 1 addition & 0 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

<%# Includes all stylesheet files in app/assets/stylesheets %>
<%= stylesheet_link_tag "application" %>
<%= javascript_pack_tag "application" %>
</head>

<body>
Expand Down
15 changes: 15 additions & 0 deletions app/views/layouts/hello_world.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>ReactOnRailsWithShakapacker</title>
<%= csrf_meta_tags %>

<!-- Empty pack tags - React on Rails injects component CSS/JS here -->
<%= stylesheet_pack_tag %>
<%= javascript_pack_tag %>
</head>

<body>
<%= yield %>
</body>
</html>
36 changes: 36 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// The source code including full typescript support is available at:
// https://github.com/shakacode/react_on_rails_demo_ssr_hmr/blob/master/babel.config.js

module.exports = function (api) {
const defaultConfigFunc = require('shakapacker/package/babel/preset.js')
const resultConfig = defaultConfigFunc(api)
const isProductionEnv = api.env('production')

const changesOnDefault = {
presets: [
[
'@babel/preset-react',
{
development: !isProductionEnv,
useBuiltIns: true,
runtime: 'automatic'
}
]
].filter(Boolean),
plugins: [
// Enable React Refresh (Fast Refresh) only when webpack-dev-server is running (HMR mode)
// This prevents React Refresh from trying to connect when using static compilation
!isProductionEnv && process.env.WEBPACK_SERVE && 'react-refresh/babel',
isProductionEnv && ['babel-plugin-transform-react-remove-prop-types',
{
removeImport: true
}
]
].filter(Boolean),
}

resultConfig.presets = [...resultConfig.presets, ...changesOnDefault.presets]
resultConfig.plugins = [...resultConfig.plugins, ...changesOnDefault.plugins ]

return resultConfig
}
Loading