react_router_rails_spa 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +215 -2
- data/README.md +117 -17
- data/documents/introduction.md +293 -0
- data/lib/react_router_rails_spa/csrf/csrf_cookie_enabled.rb +20 -0
- data/lib/react_router_rails_spa/generators/install_generator.rb +41 -7
- data/lib/react_router_rails_spa/generators/templates/csrf.ts +6 -2
- data/lib/react_router_rails_spa/generators/templates/csrf_cookie_enabled.rb +18 -0
- data/lib/react_router_rails_spa/generators/templates/proxy.ts +17 -10
- data/lib/react_router_rails_spa/generators/templates/react-router.config.ts +4 -5
- data/lib/react_router_rails_spa/generators/templates/react.rake +18 -26
- data/lib/react_router_rails_spa/generators/templates/vite.config.ts +18 -16
- data/lib/react_router_rails_spa/generators/templates/welcome/welcome.tsx +17 -7
- data/lib/react_router_rails_spa/version.rb +1 -1
- data/lib/react_router_rails_spa.rb +1 -0
- metadata +8 -19
- data/.idea/.name +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 95855ba78c935cea1d1c666a75769bf23dca4b3593b6837f29ceb2d97c3259b2
|
4
|
+
data.tar.gz: b4f53bab792448b9a8a1f7df9d6b02a37680425ab51fcdde40cc90c3b3d7dc50
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 653a9a898765b134d1154e33169be82cd31d8143c775da3216e2106b7dad3b705a26d4fb94f07e30e200e478fd7d1398ae0308c883919e8a704d7a989ef53c6c
|
7
|
+
data.tar.gz: 813669a3b2801ddfad534b8ca1135f3f7dae018b8329f5bb112ced8fdf67d5367203c23f6f229b4b277ae645c6434e1e75c58f80cfbbb06c560fb4dea40133c9
|
data/.rubocop.yml
CHANGED
@@ -1,8 +1,221 @@
|
|
1
1
|
AllCops:
|
2
2
|
TargetRubyVersion: 3.0
|
3
|
+
NewCops: enable
|
3
4
|
|
5
|
+
# Below taken from rubocop-rails-omakase
|
6
|
+
|
7
|
+
# Align `when` with `end`.
|
8
|
+
Layout/CaseIndentation:
|
9
|
+
Enabled: true
|
10
|
+
EnforcedStyle: end
|
11
|
+
|
12
|
+
# Align comments with method definitions.
|
13
|
+
Layout/CommentIndentation:
|
14
|
+
Enabled: true
|
15
|
+
|
16
|
+
Layout/ElseAlignment:
|
17
|
+
Enabled: true
|
18
|
+
|
19
|
+
Layout/EmptyLineAfterMagicComment:
|
20
|
+
Enabled: true
|
21
|
+
|
22
|
+
Layout/EmptyLinesAroundBlockBody:
|
23
|
+
Enabled: true
|
24
|
+
|
25
|
+
# In a regular class definition, no empty lines around the body.
|
26
|
+
Layout/EmptyLinesAroundClassBody:
|
27
|
+
Enabled: true
|
28
|
+
|
29
|
+
# In a regular method definition, no empty lines around the body.
|
30
|
+
Layout/EmptyLinesAroundMethodBody:
|
31
|
+
Enabled: true
|
32
|
+
|
33
|
+
# In a regular module definition, no empty lines around the body.
|
34
|
+
Layout/EmptyLinesAroundModuleBody:
|
35
|
+
Enabled: true
|
36
|
+
|
37
|
+
# Align `end` with the matching keyword or starting expression except for
|
38
|
+
# assignments, where it should be aligned with the LHS.
|
39
|
+
Layout/EndAlignment:
|
40
|
+
Enabled: true
|
41
|
+
EnforcedStyleAlignWith: variable
|
42
|
+
|
43
|
+
# Method definitions after `private` or `protected` isolated calls need one
|
44
|
+
# extra level of indentation.
|
45
|
+
#
|
46
|
+
# We break this rule in context, though, e.g. for private-only concerns,
|
47
|
+
# so we leave it disabled.
|
48
|
+
Layout/IndentationConsistency:
|
49
|
+
Enabled: false
|
50
|
+
EnforcedStyle: indented_internal_methods
|
51
|
+
|
52
|
+
# Detect hard tabs, no hard tabs.
|
53
|
+
Layout/IndentationStyle:
|
54
|
+
Enabled: true
|
55
|
+
|
56
|
+
# Two spaces, no tabs (for indentation).
|
57
|
+
#
|
58
|
+
# Doesn't behave properly with private-only concerns, so it's disabled.
|
59
|
+
Layout/IndentationWidth:
|
60
|
+
Enabled: false
|
61
|
+
|
62
|
+
Layout/LeadingCommentSpace:
|
63
|
+
Enabled: true
|
64
|
+
|
65
|
+
Layout/SpaceAfterColon:
|
66
|
+
Enabled: true
|
67
|
+
|
68
|
+
Layout/SpaceAfterComma:
|
69
|
+
Enabled: true
|
70
|
+
|
71
|
+
Layout/SpaceAroundEqualsInParameterDefault:
|
72
|
+
Enabled: true
|
73
|
+
|
74
|
+
Layout/SpaceAroundKeyword:
|
75
|
+
Enabled: true
|
76
|
+
|
77
|
+
# Use `foo {}` not `foo{}`.
|
78
|
+
Layout/SpaceBeforeBlockBraces:
|
79
|
+
Enabled: true
|
80
|
+
|
81
|
+
Layout/SpaceBeforeComma:
|
82
|
+
Enabled: true
|
83
|
+
|
84
|
+
Layout/SpaceBeforeFirstArg:
|
85
|
+
Enabled: true
|
86
|
+
|
87
|
+
# Use `->(x, y) { x + y }` not `-> (x, y) { x + y }`
|
88
|
+
Layout/SpaceInLambdaLiteral:
|
89
|
+
Enabled: true
|
90
|
+
|
91
|
+
# Use `[ a, [ b, c ] ]` not `[a, [b, c]]`
|
92
|
+
# Use `[]` not `[ ]`
|
93
|
+
Layout/SpaceInsideArrayLiteralBrackets:
|
94
|
+
Enabled: true
|
95
|
+
EnforcedStyle: space
|
96
|
+
EnforcedStyleForEmptyBrackets: no_space
|
97
|
+
|
98
|
+
# Use `%w[ a b ]` not `%w[ a b ]`.
|
99
|
+
Layout/SpaceInsideArrayPercentLiteral:
|
100
|
+
Enabled: true
|
101
|
+
|
102
|
+
# Use `foo { bar }` not `foo {bar}`.
|
103
|
+
# Use `foo { }` not `foo {}`.
|
104
|
+
Layout/SpaceInsideBlockBraces:
|
105
|
+
Enabled: true
|
106
|
+
EnforcedStyleForEmptyBraces: space
|
107
|
+
|
108
|
+
# Use `{ a: 1 }` not `{a:1}`.
|
109
|
+
# Use `{}` not `{ }`.
|
110
|
+
Layout/SpaceInsideHashLiteralBraces:
|
111
|
+
Enabled: true
|
112
|
+
EnforcedStyle: space
|
113
|
+
EnforcedStyleForEmptyBraces: no_space
|
114
|
+
|
115
|
+
# Use `foo(bar)` not `foo( bar )`
|
116
|
+
Layout/SpaceInsideParens:
|
117
|
+
Enabled: true
|
118
|
+
|
119
|
+
# Requiring a space is not yet supported as of 0.59.2
|
120
|
+
# Use `%w[ foo ]` not `%w[foo]`
|
121
|
+
Layout/SpaceInsidePercentLiteralDelimiters:
|
122
|
+
Enabled: false
|
123
|
+
#EnforcedStyle: space
|
124
|
+
|
125
|
+
# Use `hash[:key]` not `hash[ :key ]`
|
126
|
+
Layout/SpaceInsideReferenceBrackets:
|
127
|
+
Enabled: true
|
128
|
+
|
129
|
+
# Blank lines should not have any spaces.
|
130
|
+
Layout/TrailingEmptyLines:
|
131
|
+
Enabled: true
|
132
|
+
|
133
|
+
# No trailing whitespace.
|
134
|
+
Layout/TrailingWhitespace:
|
135
|
+
Enabled: true
|
136
|
+
|
137
|
+
Lint/RedundantStringCoercion:
|
138
|
+
Enabled: true
|
139
|
+
|
140
|
+
# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
|
141
|
+
Lint/RequireParentheses:
|
142
|
+
Enabled: true
|
143
|
+
|
144
|
+
Lint/UriEscapeUnescape:
|
145
|
+
Enabled: true
|
146
|
+
|
147
|
+
# We generally prefer &&/|| but like low-precedence and/or in context
|
148
|
+
Style/AndOr:
|
149
|
+
Enabled: false
|
150
|
+
|
151
|
+
# Prefer Foo.method over Foo::method
|
152
|
+
Style/ColonMethodCall:
|
153
|
+
Enabled: true
|
154
|
+
|
155
|
+
Style/DefWithParentheses:
|
156
|
+
Enabled: true
|
157
|
+
|
158
|
+
# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
|
159
|
+
Style/HashSyntax:
|
160
|
+
Enabled: true
|
161
|
+
EnforcedShorthandSyntax: either
|
162
|
+
|
163
|
+
# Defining a method with parameters needs parentheses.
|
164
|
+
Style/MethodDefParentheses:
|
165
|
+
Enabled: true
|
166
|
+
|
167
|
+
Style/ParenthesesAroundCondition:
|
168
|
+
Enabled: true
|
169
|
+
|
170
|
+
Style/PercentLiteralDelimiters:
|
171
|
+
Enabled: true
|
172
|
+
PreferredDelimiters:
|
173
|
+
default: "()"
|
174
|
+
"%i": "[]"
|
175
|
+
"%I": "[]"
|
176
|
+
"%r": "{}"
|
177
|
+
"%w": "[]"
|
178
|
+
"%W": "[]"
|
179
|
+
|
180
|
+
# Use quotes for string literals when they are enough.
|
181
|
+
Style/RedundantPercentQ:
|
182
|
+
Enabled: false
|
183
|
+
|
184
|
+
Style/RedundantReturn:
|
185
|
+
Enabled: true
|
186
|
+
AllowMultipleReturnValues: true
|
187
|
+
|
188
|
+
Style/Semicolon:
|
189
|
+
Enabled: true
|
190
|
+
AllowAsExpressionSeparator: true
|
191
|
+
|
192
|
+
Style/StabbyLambdaParentheses:
|
193
|
+
Enabled: true
|
194
|
+
|
195
|
+
# Use `"foo"` not `'foo'` unless escaping is required
|
4
196
|
Style/StringLiterals:
|
197
|
+
Enabled: true
|
5
198
|
EnforcedStyle: double_quotes
|
6
199
|
|
7
|
-
Style/
|
8
|
-
|
200
|
+
Style/TrailingCommaInArrayLiteral:
|
201
|
+
Enabled: true
|
202
|
+
|
203
|
+
Style/TrailingCommaInHashLiteral:
|
204
|
+
Enabled: true
|
205
|
+
|
206
|
+
# Other cops
|
207
|
+
|
208
|
+
Style/Documentation:
|
209
|
+
Enabled: false
|
210
|
+
|
211
|
+
Style/FrozenStringLiteralComment:
|
212
|
+
Enabled: false
|
213
|
+
|
214
|
+
Metrics/BlockLength:
|
215
|
+
Enabled: true
|
216
|
+
Exclude:
|
217
|
+
- "**/*.gemspec"
|
218
|
+
- "**/*.rake"
|
219
|
+
|
220
|
+
Gemspec/RequireMFA:
|
221
|
+
Enabled: true
|
data/README.md
CHANGED
@@ -1,38 +1,138 @@
|
|
1
|
-
|
1
|
+
## React Router SPA Framework mode integration for Ruby on Rails
|
2
2
|
|
3
|
-
|
3
|
+
The react_router_rails_spa gem integrates [React Router in SPA Framework mode](https://reactrouter.com/how-to/spa) with your Ruby on Rails application.
|
4
4
|
|
5
|
-
|
5
|
+
The React app is built as a static SPA.
|
6
|
+
Static assets will be built in your Rails `public` folder,
|
7
|
+
and will be deployed on your current production server with minimal, if any, configuration changes.
|
8
|
+
|
9
|
+
With a single gem and a single command,
|
10
|
+
this gem sets up all that you need in ["Omakase"](https://dhh.dk/2012/rails-is-omakase.html)-style – An integrated client-side router,
|
11
|
+
per-route code-splitting, the loader data-fetch pattern, Rails controllers and routes –
|
12
|
+
All of this will be automatically set up for you.
|
13
|
+
|
14
|
+
[Read the introduction](https://github.com/naofumi/react_router_rails_spa/blob/main/documents/introduction.md) for more background:
|
15
|
+
|
16
|
+
## Who is it for?
|
17
|
+
|
18
|
+
Consider trying out this gem if any of the below apply to you.
|
19
|
+
|
20
|
+
- You want an out-of-the-box solution.
|
21
|
+
- You do not enjoy installing React, React Router, Tailwind, configuring code-splitting, and deciding the data-loading scheme that you will use throughout your application. You want ["Omakase"](https://dhh.dk/2012/rails-is-omakase.html) on the front-end as well as your Rails back-end.
|
22
|
+
- You do not need SEO, at least not for the React pages.
|
23
|
+
- You can always use ERB views for the pages that need SEO.
|
24
|
+
- You are tired of managing multiple servers for your frontend and your backend.
|
25
|
+
- You do not want to incur the additional costs, complexity, and authentication concerns that are inherent when dealing with multiple servers, for no concrete benefit.
|
26
|
+
- You want to simply deploy your React frontend as static assets on a single server, inside your Ruby on Rails `public` folder.
|
27
|
+
- You have many pages, and you want to reduce the initial JavaScript payload size by using automatic code-splitting and lazy-loading, but without sacrificing performance due to request waterfalls.
|
6
28
|
|
7
29
|
## Installation
|
8
30
|
|
9
|
-
|
31
|
+
We assume that you already have an existing Ruby on Rails application.
|
32
|
+
|
33
|
+
Add this line to your application's Gemfile:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
gem 'react_router_rails_spa'
|
37
|
+
```
|
38
|
+
|
39
|
+
Then, run:
|
40
|
+
|
41
|
+
```shell
|
42
|
+
bundle install
|
43
|
+
```
|
44
|
+
|
45
|
+
followed by:
|
46
|
+
|
47
|
+
```shell
|
48
|
+
bin/rails generate react_router_rails_spa:install
|
49
|
+
```
|
50
|
+
|
51
|
+
This will create a new directory called `frontend` inside the project root.
|
52
|
+
This is where your React application is.
|
53
|
+
|
54
|
+
It will also create a React bootstrap endpoint in your Rails routes for all paths starting with `/react`.
|
55
|
+
The endpoint will be handled by `ReactController#show`.
|
56
|
+
|
57
|
+
## Running the React Router development server
|
10
58
|
|
11
|
-
|
59
|
+
React Router is built with Vite and uses the Vite development server to provide a Hot Module Replacement (HMR) capability.
|
12
60
|
|
13
|
-
|
14
|
-
|
61
|
+
Start the Vite development server with the following command
|
62
|
+
(we assume that the Ruby on Rails server is already running (with either the `bin/rails s` or the `bin/dev` command).
|
63
|
+
|
64
|
+
```shell
|
65
|
+
bin/rails react_router:dev
|
15
66
|
```
|
16
67
|
|
17
|
-
|
68
|
+
Note that the development server will not fully represent the application's behavior in production.
|
69
|
+
In particular, the development server is only partially integrated with Rails.
|
70
|
+
We therefore strongly recommend that you build the React Router assets into the `public` folder and preview it before deploying into production.
|
18
71
|
|
19
|
-
|
20
|
-
|
72
|
+
To preview the production build, run the following command.
|
73
|
+
```shell
|
74
|
+
bin/rails react_router:preview
|
21
75
|
```
|
22
76
|
|
23
|
-
|
77
|
+
This will build the React Router application into the Rails `public` folder.
|
78
|
+
The React Router application will be available from the Rails development server
|
79
|
+
(puma) at `http://localhost:3000/react`.
|
80
|
+
This preview will be representative of the production app's behavior with Rails integration.
|
81
|
+
|
82
|
+
## Deployment
|
83
|
+
|
84
|
+
This gem integrates with the Ruby on Rails Asset pipeline, and the React application is automatically built whenever `bin/rails assets:precompile` is run.
|
85
|
+
No changes are required on your deployment script, since your Ruby on Rails application should run this command.
|
86
|
+
|
87
|
+
If your deployment pipeline does not already install Node (for example, you have a "no-build" deployment for Rails),
|
88
|
+
then install it in your CI/CD environment since building the React Router application will require it.
|
89
|
+
|
90
|
+
## Background
|
91
|
+
|
92
|
+
Ruby on Rails has officially supported bundling of complex JavaScript frontend libraries,
|
93
|
+
first with [Webpacker](https://github.com/rails/webpacker),
|
94
|
+
and currently with [jsbundling-rails](https://github.com/rails/jsbundling-rails).
|
95
|
+
We also have [Vite Ruby](https://github.com/ElMassimo/vite_ruby) with an integrated dev server, HMR and other goodies.
|
96
|
+
|
97
|
+
However, on February 14th, 2025,
|
98
|
+
the React team announced the [official deprecation of Create React App (CRA)](https://react.dev/blog/2025/02/14/sunsetting-create-react-app).
|
99
|
+
Instead, they suggested developers should use a framework that builds single-page apps
|
100
|
+
(SPA) deployable to a CDN or a static hosting service –
|
101
|
+
In other words, SPA frameworks such as Next.js or React Router.
|
102
|
+
Importantly,
|
103
|
+
they advised
|
104
|
+
against [building a React app from scratch](https://react.dev/learn/build-a-react-app-from-scratch)
|
105
|
+
unless your app has constraints not well-served by existing SPA frameworks.
|
106
|
+
|
107
|
+
This approach poses challenges to the traditional Rails integration solutions.
|
108
|
+
They all compile the React applications to a single JavaScript file
|
109
|
+
that is then loaded from an ERB file (the React bootstrap HTML) containing helper functions such as
|
110
|
+
`javascript_include_tag` (jsbundling-rails), `javascript_pack_tag` (webpacker), or `vite_javascript_tag` (vite-rails).
|
111
|
+
However, SPA frameworks do not just compile to a single JavaScript file.
|
112
|
+
Instead,
|
113
|
+
they additionally generate their own React bootstrap HTML file which includes many framework-specific optimizations
|
114
|
+
(React Router v7 generates this using SSG from `app/root.tsx`).
|
115
|
+
|
116
|
+
This gem uses the bootstrap HTML file
|
117
|
+
generated by the SPA framework instead of an ERB file.
|
118
|
+
This is served through an ActionController endpoint,
|
119
|
+
enabling you to use cookies to send session-specific information on the first HTML load
|
120
|
+
and is used in this gem to send CSRF tokens.
|
24
121
|
|
25
|
-
|
122
|
+
## Notes when using Rails in API mode
|
26
123
|
|
27
|
-
|
124
|
+
Ruby on Rails provides an API mode that removes many frontend features.
|
125
|
+
Importantly, API mode implies a stateless API server that does not support cookies.
|
126
|
+
It removes the middleware for cookie handling and also for CSRF protection.
|
28
127
|
|
29
|
-
|
128
|
+
To fully benefit from hosting your React app inside Rails' `public` folder, we recommend that you avoid API-mode and instead use cookies for authentication.
|
30
129
|
|
31
|
-
|
130
|
+
If you want to convert your API-mode application to use cookies, make sure to also restore CSRF features.
|
131
|
+
Otherwise, your app will be vulnerable to CSRF attacks.
|
32
132
|
|
33
133
|
## Contributing
|
34
134
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
135
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/naofumi/react_router_rails_spa. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/naofumi/react_router_rails_spa/blob/main/CODE_OF_CONDUCT.md).
|
36
136
|
|
37
137
|
## License
|
38
138
|
|
@@ -40,4 +140,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
40
140
|
|
41
141
|
## Code of Conduct
|
42
142
|
|
43
|
-
Everyone interacting in the
|
143
|
+
Everyone interacting in the ReactRouterRailSpa project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/naofumi/react_router_rails_spa/blob/main/CODE_OF_CONDUCT.md).
|
@@ -0,0 +1,293 @@
|
|
1
|
+
# Integrating Ruby on Rails with Modern SPAs
|
2
|
+
|
3
|
+
## TL;DR;
|
4
|
+
|
5
|
+
Integrating React onto a Ruby on Rails application is unnecessarily challenging.
|
6
|
+
We have to install jsbundling-rails, install multiple packages, configure propshaft, create an ERB endpoint, etc.
|
7
|
+
|
8
|
+
Why can't we just run `rails new`,
|
9
|
+
install a single gem, and instantly have a state-of-the-art React setup with client-side routing,
|
10
|
+
code-splitting, and effective data-loading patterns built-in?
|
11
|
+
|
12
|
+
This article introduces the [react_router_rails_spa gem](https://github.com/naofumi/react_router_rails_spa),
|
13
|
+
which integrates a React Router SPA framework application into your existing Ruby on Rails project.
|
14
|
+
With just a few commands, you will have a fully functioning scaffold on which to build your React app.
|
15
|
+
|
16
|
+
To jump to the installation steps, go to the ["Using the gem" section](#using-the-gem).
|
17
|
+
|
18
|
+
## Who is this for?
|
19
|
+
|
20
|
+
If any of the following describes yourself, then this gem might be for you.
|
21
|
+
|
22
|
+
### You want to create an SPA with a React frontend and a Rails backend.
|
23
|
+
|
24
|
+
* You want a simple setup that is ["omakase"](https://dhh.dk/2012/rails-is-omakase.html). You don't want to install packages and configure them on the React side. On the Rails side, you don't want to manually add routes and controllers. Everything should be a single gem and a single command.
|
25
|
+
* You want something that is easy to deploy, and cost-effective. You don't want to pay for an extra server that you don't strictly need.
|
26
|
+
* You don't need SEO for the React pages.
|
27
|
+
* If SEO is necessary, you can just serve ERB pages or static HTML files.
|
28
|
+
|
29
|
+
### You tried Next.js, but you did not really need SSR nor RSCs. You were only interested in Next.js because you thought it was easy.
|
30
|
+
|
31
|
+
* You're only using Next.js because you thought it was ["omakase"](https://dhh.dk/2012/rails-is-omakase.html) and simple to set up.
|
32
|
+
* After a while, you've found that integrating the Next.js server and the Rails server is harder than you bargained for. You've had headaches around authentication schemes, cross-domains, subdomains, CORS settings, samesite cookies, CSRF mitigation, and reverse proxies, etc. You've realized that running separate frontend and backend servers is hard.
|
33
|
+
* You are not happy with the extra money you have to pay to Next.js.
|
34
|
+
|
35
|
+
The [react_router_rails_spa gem](https://github.com/naofumi/react_router_rails_spa) gives you the simplicity of an opinionated SPA framework,
|
36
|
+
without the complexity of a mulit-server setup.
|
37
|
+
|
38
|
+
### You want to use cutting-edge React
|
39
|
+
|
40
|
+
* You want to use cutting-edge React capabilities like code-splitting, loader-based data fetching and more, but you're not sure how to [set this up from scratch](https://react.dev/learn/build-a-react-app-from-scratch).
|
41
|
+
* You don't want to create a slow, bloated, legacy React app.
|
42
|
+
|
43
|
+
## Who is this NOT for?
|
44
|
+
|
45
|
+
### You want to embedd some React components on top of your ERB-rendered pages
|
46
|
+
|
47
|
+
This is [how React was originally used](https://react.dev/learn/add-react-to-an-existing-project#using-react-for-a-part-of-your-existing-page).
|
48
|
+
React was built for this, and it's generally much simpler to set up than a multi-page SPA.
|
49
|
+
|
50
|
+
The current gem does not help you with this.
|
51
|
+
If you wish to take this approach,
|
52
|
+
you can build your own system or use Gems like [react-rails](https://github.com/reactjs/react-rails) and [turbo-mount](https://github.com/skryukov/turbo-mount).
|
53
|
+
Turbo Mount uses Stimulus to mount components, and is more robust if you are using Hotwire in your ERB views.
|
54
|
+
|
55
|
+
## How does this compare to [...]?
|
56
|
+
|
57
|
+
Compared to gems like [React on Rails](https://github.com/shakacode/react_on_rails) or [Intertia Rails](https://inertia-rails.dev/),
|
58
|
+
the current gem is just an installer and does virtually nothing to modify or add features to React Router.
|
59
|
+
This is a great advantage and ensures that frontend developers will feel right at home.
|
60
|
+
|
61
|
+
It also means that you can easily understand how it works. You can customize accordingly.
|
62
|
+
|
63
|
+
## Background
|
64
|
+
|
65
|
+
On February 14th, 2025,
|
66
|
+
the React team published a blog post
|
67
|
+
titled [Sunsetting Create React App](https://react.dev/blog/2025/02/14/sunsetting-create-react-app). They strongly recommended
|
68
|
+
that **developers should now use an SPA framework instead**.
|
69
|
+
|
70
|
+
Importantly, and often lost in the public discourse, they were **NOT** recommending an **SSR** framework.
|
71
|
+
Instead, they were advocating for creating SPAs with [**SPA** frameworks](https://react.dev/blog/2025/02/14/sunsetting-create-react-app#how-to-migrate-to-a-framework)
|
72
|
+
that could be deployed on a CDN, a static hosting service, or the `public` folder of a Ruby on Rails application.
|
73
|
+
|
74
|
+
In the following, we will call SPAs built with an SPA framework, **"Modern React SPAs"**
|
75
|
+
to highlight that this is the current officially recommended approach.
|
76
|
+
To contrast, we will call the ones that the React team is actively discouraging, **"Legacy React SPAs"**.
|
77
|
+
|
78
|
+
> **"I have no interest nor use for SSR!
|
79
|
+
I don't need SEO.
|
80
|
+
> An SPA is all that I need.
|
81
|
+
> Instead of Create React App, I'll just use Vite!"**
|
82
|
+
|
83
|
+
This was the most common response to the blog post.
|
84
|
+
However IMO, it misses the point that the authors were repeatedly trying to make.
|
85
|
+
|
86
|
+
The React team is strongly recommending that **even if you only need an SPA, you should be creating a Modern SPA**.
|
87
|
+
[They carefully go through some of the features](https://react.dev/learn/build-a-react-app-from-scratch) like code-splitting
|
88
|
+
and loader-based routing
|
89
|
+
that you will need to add if you are building a state-of-the-art React SPA from scratch.
|
90
|
+
These features are often challenging and require expertise to correctly implement,
|
91
|
+
but are essential for modern React applications.
|
92
|
+
|
93
|
+
Without these features, your React SPA is a Legacy SPA.
|
94
|
+
It will suffer from the same performance issues that plagued old SPAs a decade ago –
|
95
|
+
namely huge initial bundle size, data-fetch waterfalls, flickering, and very slow load times.
|
96
|
+
|
97
|
+
In the above article,
|
98
|
+
the React team went out of its way
|
99
|
+
to tell us
|
100
|
+
that we should not simply replace the deprecated Create React App with a newer but nonetheless still architecturally Legacy SPA.
|
101
|
+
Instead, they strongly urge us to embrace Modern React SPAs and avoid these issues.
|
102
|
+
|
103
|
+
We should note that Vite is essentially a bundler and a development server, with a plugin system that allows us to easily install various packages.
|
104
|
+
It is agnostic to the Legacy vs. Modern SPA debate.
|
105
|
+
You can build a Legacy SPA using Vite, and you can also create a Modern SPA.
|
106
|
+
Vite does not care either way, and the installer command `npm create vite@latest` gives you both templates.
|
107
|
+
|
108
|
+
> **"I want to integrate my React app with a Ruby on Rails backend. I'll just add a `javascript_include_tag` to my bootstrap ERB template. That will load React just fine!"**
|
109
|
+
|
110
|
+
This is also a common response from people who prefer a no-fuss solution for Rails.
|
111
|
+
However, note that this is exactly the approach that the React team strongly discourages.
|
112
|
+
The above solution is a Legacy SPA and will suffer from the same legacy issues.
|
113
|
+
|
114
|
+
Instead, the React team is recommending that you integrate a Modern SPA framework using ...
|
115
|
+
|
116
|
+
Well, actually, they don't have a concrete recommendation yet for Rails.
|
117
|
+
As far as we know, nothing currently exists to easily integrate a Modern SPA with Rails.
|
118
|
+
|
119
|
+
We hope to address this with this [react_router_rails_spa gem](https://github.com/naofumi/react_router_rails_spa).
|
120
|
+
|
121
|
+
## Why we need a different approach for Rails integration
|
122
|
+
|
123
|
+
Historically,
|
124
|
+
the way to integrate React with Ruby on Rails was to create an ERB endpoint that served as the initial HTML
|
125
|
+
(the bootstrap HTML) for React.
|
126
|
+
This ERB template would have either a `javascript_include_tag` (jsbundling-rails) or a
|
127
|
+
`javascript_pack_tag` (webpacker) to load the React application build artifact.
|
128
|
+
Newer gems like [Vite Rails](https://vite-ruby.netlify.app/guide/rails.html#tag-helpers-🏷) have also adopted the same approach.
|
129
|
+
|
130
|
+
However, this is exactly what the React team is discouraging, and it seems unwise to continue down this path.
|
131
|
+
|
132
|
+
The problem is that Modern SPAs build their own bootstrap HTML templates with SSG
|
133
|
+
(the first HTML that the browser loads).
|
134
|
+
Modern SPA frameworks are not just JavaScript.
|
135
|
+
Instead, the bootstrap HTML and the JavaScript are tightly integrated.
|
136
|
+
|
137
|
+
Therefore, to take advantage of Modern SPA features,
|
138
|
+
Rails has
|
139
|
+
to give up
|
140
|
+
on generating its own bootstrap HTML from an ERB template with an embedded `javascript_include_tag`.
|
141
|
+
Instead, we have to take the bootstrap HTML generated by the SPA framework, and wrap Rails around this.
|
142
|
+
|
143
|
+
This is how the react_router_rails_spa gem works.
|
144
|
+
|
145
|
+
## Outline of how the gem works
|
146
|
+
|
147
|
+
It is important to note that the [react_router_rails_spa gem](https://github.com/naofumi/react_router_rails_spa) does nothing more than a stock React Router installation with some custom configuration,
|
148
|
+
paired with the generation of a single Rails controller.
|
149
|
+
|
150
|
+
**There is very little custom code, and this is actually a huge advantage**.
|
151
|
+
This is a very thin wrapper around the official React Router installer and resilient against future changes.
|
152
|
+
If you wish, you can easily update and customize your NPM packages independently of this gem. The generated code is also heavily commented
|
153
|
+
to help you understand the internals for yourself.
|
154
|
+
|
155
|
+
### React Router SPA framework mode
|
156
|
+
|
157
|
+
We install and use React Router in SPA framework mode, [configured to generate an SPA build](https://reactrouter.com/how-to/spa).
|
158
|
+
It is a true SPA and will build static files that can be served from any static hosting provider.
|
159
|
+
These are transferred to the `public` folder in Rails for deployment.
|
160
|
+
|
161
|
+
The command for building the react application is integrated into the `rake assets:precompile` command.
|
162
|
+
Therefore, you do not need any additional configuration in your CI/CD scripts.
|
163
|
+
|
164
|
+
### Integration with Ruby on Rails through cookies
|
165
|
+
|
166
|
+
Previously, you would integrate Ruby on Rails using a bootstrap HTML template generated by Rails using ERB templates.
|
167
|
+
This allowed you, for example, to embed CSRF-mitigation token tags.
|
168
|
+
As mentioned above, this is incompatible with Modern SPA frameworks.
|
169
|
+
|
170
|
+
Otherwise,
|
171
|
+
you could build your SPA independently of Rails
|
172
|
+
and send extra requests from the browser to retrieve the CSRF token and other integration information.
|
173
|
+
This requires otherwise unnecessary network requests.
|
174
|
+
|
175
|
+
Neither approach is ideal.
|
176
|
+
|
177
|
+
Instead,
|
178
|
+
the [react_router_rails_spa gem](https://github.com/naofumi/react_router_rails_spa) sends Rails-generated cookies and/or headers alongside the SPA framework-generated bootstrap HTML file.
|
179
|
+
It give you the best of both worlds.
|
180
|
+
|
181
|
+
For example, the `ReactRouterRailsSpa::CsrfCookieEnabled` module
|
182
|
+
sends session-specific CSRF tokens via cookies to integrate Rails'
|
183
|
+
CSRF protection with React.
|
184
|
+
The SPA framework-generated HTML is untouched.
|
185
|
+
|
186
|
+
### Rake files for automation
|
187
|
+
|
188
|
+
We provide rake tasks for starting up the development server and building the React Router application.
|
189
|
+
|
190
|
+
Note that the build task is attached to the `assets:precompile` task.
|
191
|
+
This means
|
192
|
+
that you do not need
|
193
|
+
to add extra configuration to your CI/CD scripts
|
194
|
+
to build the React Router app since it should normally call this task already.
|
195
|
+
|
196
|
+
If your CI/CD already installs Node (which is required for building), then you probably won't have to touch your CI/CD scripts at all.
|
197
|
+
|
198
|
+
### Additional React Router and Vite Configuration
|
199
|
+
|
200
|
+
We currently serve the React application from the `/react/*` paths.
|
201
|
+
All other paths are handled by Rails.
|
202
|
+
The current gem adds minor configurations for this.
|
203
|
+
If you want a different setup, you can change the configurations.
|
204
|
+
|
205
|
+
## Demo and Source code
|
206
|
+
|
207
|
+
* We have a [demo application running on Kamal on a VPS server](https://rrrails.castle104.com/react/). It has simple, session-based authentication and basic CRUD. Mutations are secured by integration with Rails CSRF protection.
|
208
|
+
* In the demo application, we have intentionally added a 0.5 to 1.5-second random delay on all server requests. Even the most bloated and inefficient web technologies will look great on a high-performance device with a fast network. Unless your demo intentionally simulates non-ideal situations, it is meaningless.
|
209
|
+
* The source-code for this demo application is [available on GitHub](https://github.com/naofumi/react-router-vite-rails).
|
210
|
+
|
211
|
+
The source code is heavily commented. We recommend that you read through it to understand the setup in more detail.
|
212
|
+
|
213
|
+
## Using the gem
|
214
|
+
|
215
|
+
### Install Ruby on Rails
|
216
|
+
|
217
|
+
This gem works with a pre-existing installation of Rails. Create a new Rails application if you haven't already.
|
218
|
+
|
219
|
+
```shell
|
220
|
+
rails new [project name]
|
221
|
+
```
|
222
|
+
|
223
|
+
Note that this gem works even with a no-build Rails setup (which is the Rails default).
|
224
|
+
However, you will need Node.js in your CI/CD for deployment.
|
225
|
+
If you are unsure how to do this,
|
226
|
+
we recommend that you generate a new Rails application with jsbundling-rails pre-installed.
|
227
|
+
This will create a ready-made Dockerfile that installs Node.js.
|
228
|
+
(This gem won't use esbuild. We're only doing this for Node.js.)
|
229
|
+
|
230
|
+
```shell
|
231
|
+
rails new [project name] --javascript esbuild
|
232
|
+
```
|
233
|
+
|
234
|
+
### Install the `react_router_rails_spa` gem
|
235
|
+
|
236
|
+
Add the following line to your `Gemfile`.
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
gem "react_router_rails_spa"
|
240
|
+
```
|
241
|
+
|
242
|
+
Install the gem
|
243
|
+
|
244
|
+
```shell
|
245
|
+
bundle install
|
246
|
+
```
|
247
|
+
|
248
|
+
We recommend committing your changes at this point before the following generator adds and modifies your files.
|
249
|
+
|
250
|
+
Run the generator
|
251
|
+
|
252
|
+
```shell
|
253
|
+
bin/rails generate react_router_rails_spa:install
|
254
|
+
```
|
255
|
+
|
256
|
+
This will install the latest version of React Router
|
257
|
+
and generate the routes and all the necessary files and configurations.
|
258
|
+
|
259
|
+
### Run the development server
|
260
|
+
|
261
|
+
Run the following command to start the frontend development server with HMR (Hot Module Replacement).
|
262
|
+
|
263
|
+
```shell
|
264
|
+
bin/rails react_router:dev
|
265
|
+
```
|
266
|
+
|
267
|
+
Point your browser to http://localhost:5173/react/ to see the welcome page.
|
268
|
+
|
269
|
+
Note that the frontend development server will not truly represent
|
270
|
+
how the React application and Rails server interact in production.
|
271
|
+
It is important to test with the following preview command before deploying.
|
272
|
+
|
273
|
+
### Preview and build the React Router application
|
274
|
+
|
275
|
+
Run the following command to build the React Router application
|
276
|
+
and store the static files into the Rails `public` directory.
|
277
|
+
|
278
|
+
```shell
|
279
|
+
bin/rails react_router:build
|
280
|
+
```
|
281
|
+
|
282
|
+
This command is also aliased as:
|
283
|
+
|
284
|
+
```shell
|
285
|
+
bin/rails react_router:preview
|
286
|
+
```
|
287
|
+
|
288
|
+
Start your Rails application if it is not already running and point your browser to http://localhost:3000/react/ to see the welcome page.
|
289
|
+
|
290
|
+
### Read the code
|
291
|
+
|
292
|
+
We have added numerous comments to the code generated by this gem. Please read it to understand how the integration works.
|
293
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# This concern sets the CSRF token inside the "X-CSRF-Token" cookie,
|
2
|
+
# allowing you to easily use the robust CSRF protection that Ruby on Rails provides
|
3
|
+
# inside your React Router app.
|
4
|
+
#
|
5
|
+
# Refer to `frontend/app/utilities/csrf.ts` to see
|
6
|
+
# how the client side is implemented.
|
7
|
+
module ReactRouterRailsSpa
|
8
|
+
module CsrfCookieEnabled
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
included do
|
11
|
+
before_action :set_csrf_cookie
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def set_csrf_cookie
|
17
|
+
cookies["X-CSRF-Token"] = form_authenticity_token
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -6,19 +6,54 @@ module ReactRouterRailsSpa
|
|
6
6
|
source_root File.expand_path("templates", __dir__)
|
7
7
|
|
8
8
|
def create_react_router_app
|
9
|
-
say "
|
9
|
+
say "Downloading React Router v7 ..."
|
10
10
|
inside Rails.root do
|
11
11
|
run "npx create-react-router@latest frontend --yes --no-git-init"
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
15
|
def setup_rails_routes
|
16
|
-
|
16
|
+
say "Setting up Rails routes ..."
|
17
|
+
route <<~RUBY
|
18
|
+
# When we use React Router inside a subdirectory, it works better if we
|
19
|
+
# use a trailing slash for the root path.
|
20
|
+
# This redirects from "/react-router" to "/react-router/".
|
21
|
+
get "react", to: redirect("/react/"), constraints: ->(req) {
|
22
|
+
req.original_url.last != "/"
|
23
|
+
}
|
24
|
+
|
25
|
+
# All requests to `/react/*` are handled by ReactController#show.
|
26
|
+
match "react", to: "react#show", via: :all
|
27
|
+
get "react/*path", to: "react#show"
|
28
|
+
RUBY
|
17
29
|
end
|
18
30
|
|
19
31
|
def create_react_controller
|
20
|
-
|
32
|
+
say "Creating the React bootstrap endpoint controller ..."
|
33
|
+
create_file "app/controllers/react_controller.rb", <<~RUBY
|
34
|
+
# This controller provides the catch-all action for React Router.
|
35
|
+
# This will render the bootstrap HTML file
|
36
|
+
# for all React Router requests.
|
37
|
+
#
|
38
|
+
# Unlike typical webpack or esbuild setups, we do not generate the bootstrap HTML file from ERB templates which include
|
39
|
+
# `javascript_include_tag` (propshaft, sprockets) or `javascript_pack_tag` (webpack).
|
40
|
+
# Instead, we take the index.html file that was generated by the React Router build
|
41
|
+
# and rename it to "react-router-rails-spa-index.html".
|
42
|
+
# We then serve this from the controller action in response to the bootstrap HTML file request.
|
43
|
+
#
|
44
|
+
# Benefits:
|
45
|
+
#
|
46
|
+
# * We can use the index.html file that React Router generates using SSG,
|
47
|
+
# from the `frontend/app/root.tsx` file.
|
48
|
+
# This file contains optimizations that would be challenging to recreate inside Rails using ERB templates.
|
49
|
+
# * By going through the Rails controller, we can adjust the cache and cookie headers to
|
50
|
+
# improve performance, reliability, and integration with Rails.
|
51
|
+
#
|
52
|
+
# The included ReactRouterRailsSpa::CsrfCookieEnabled module will
|
53
|
+
# send the CSRF token inside the "X-CSRF-Token" cookie for use inside your React app.
|
21
54
|
class ReactController < ApplicationController
|
55
|
+
include ReactRouterRailsSpa::CsrfCookieEnabled
|
56
|
+
|
22
57
|
def show
|
23
58
|
render file: Rails.public_path.join("react/react-router-rails-spa-index.html"), layout: false
|
24
59
|
end
|
@@ -26,20 +61,19 @@ module ReactRouterRailsSpa
|
|
26
61
|
RUBY
|
27
62
|
end
|
28
63
|
|
29
|
-
def create_public_folders
|
30
|
-
empty_directory Rails.root.join("public/react")
|
31
|
-
end
|
32
|
-
|
33
64
|
def copy_rake_task
|
65
|
+
say "Copying Rake tasks ..."
|
34
66
|
template "react.rake", "lib/tasks/react.rake"
|
35
67
|
end
|
36
68
|
|
37
69
|
def copy_react_router_configs
|
70
|
+
say "Copying React Router configurations ..."
|
38
71
|
template "react-router.config.ts", "frontend/react-router.config.ts"
|
39
72
|
template "vite.config.ts", "frontend/vite.config.ts"
|
40
73
|
end
|
41
74
|
|
42
75
|
def copy_react_app_files
|
76
|
+
say "Copying React Router application pages and utilities ..."
|
43
77
|
template "home.tsx", "frontend/app/routes/home.tsx"
|
44
78
|
directory "welcome", "frontend/app/welcome"
|
45
79
|
template "csrf.ts", "frontend/app/utilities/csrf.ts"
|
@@ -14,13 +14,13 @@
|
|
14
14
|
*
|
15
15
|
* How it works.
|
16
16
|
*
|
17
|
-
* 1. The Ruby on Rails server sends the session-specific
|
17
|
+
* 1. The Ruby on Rails server creates and sends the session-specific
|
18
18
|
* CSRF token in a Cookie named "X-CSRF-Token"
|
19
19
|
* app/controllers/concerns/csrf_cookie_enabled.rb
|
20
20
|
* 2. Retrieve this token with the `getCSRFToken()` function
|
21
21
|
* and use its value to send in your fetch request headers.
|
22
22
|
* 3. On receiving the request, Rails will validate that the
|
23
|
-
* 'X-CSRF-Token' header value
|
23
|
+
* 'X-CSRF-Token' header value is identical to the session-specific token.
|
24
24
|
* 4. Note that you only need to do this for non-GET requests assuming that
|
25
25
|
* you are following best practices and not mutating data in GET requests.
|
26
26
|
*
|
@@ -36,6 +36,10 @@
|
|
36
36
|
* }
|
37
37
|
* )
|
38
38
|
*
|
39
|
+
* Note that Axios has a feature that automatically does this for you.
|
40
|
+
* Read the XSRF configuration comments on the linked page.
|
41
|
+
* https://axios-http.com/docs/req_config
|
42
|
+
*
|
39
43
|
* */
|
40
44
|
|
41
45
|
export function getCSRFToken() {
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# This concern sets the CSRF token inside the "X-CSRF-Token" cookie,
|
2
|
+
# allowing you to easily use the robust CSRF protection that Ruby on Rails provides
|
3
|
+
# inside your React Router app.
|
4
|
+
#
|
5
|
+
# Refer to `frontend/app/utilities/csrf.ts` to see
|
6
|
+
# how the client side is implemented.
|
7
|
+
module CsrfCookieEnabled
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
included do
|
10
|
+
before_action :set_csrf_cookie
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def set_csrf_cookie
|
16
|
+
cookies["X-CSRF-Token"] = form_authenticity_token
|
17
|
+
end
|
18
|
+
end
|
@@ -1,17 +1,24 @@
|
|
1
1
|
/*
|
2
|
-
*
|
3
|
-
*
|
2
|
+
* The `baseApiPath()` is useful when you are using the React Router development server
|
3
|
+
* and the JSON API endpoints on your Rails server do not have specific prefixes.
|
4
|
+
* It is not used in production or preview builds.
|
4
5
|
*
|
5
|
-
*
|
6
|
+
*
|
7
|
+
* In the example application, the Ruby on Rails API endpoints are like
|
8
|
+
* `GET /posts` or `GET /users` and do not have any specific prefixes like `/api*`.
|
9
|
+
*
|
10
|
+
* However, during development, the port for the React Router development server (typically 5173) is different from
|
6
11
|
* the Rails development server (typically 3000).
|
7
|
-
* This means that the
|
12
|
+
* This means that the React Router server needs to distinguish
|
8
13
|
* between the requests it should send to the Rails server on port 3000 (JSON API requests) and the
|
9
|
-
* ones that it should handle itself on port 5173 (e.g., the
|
14
|
+
* ones that it should handle itself on port 5173 (e.g., the React Router assets).
|
10
15
|
*
|
11
|
-
* Therefore, when running on the
|
12
|
-
* requests intended for the Rails API server with "/api" to tell
|
16
|
+
* Therefore, when running on the React Router development server, we prefix
|
17
|
+
* requests intended for the Rails API server with "/api" to tell the development server to send them
|
13
18
|
* to port 3000.
|
14
|
-
*
|
19
|
+
* The all other requests will be sent to port 5173.
|
20
|
+
*
|
21
|
+
* See the `proxy:` section in `frontend/vite.config.ts` for
|
15
22
|
* the Vite side of this configuration.
|
16
23
|
*
|
17
24
|
* Using this function, you would write a request to the Rails API as follows.
|
@@ -20,8 +27,8 @@
|
|
20
27
|
* headers: { "Accept": "application/json" }
|
21
28
|
* }).then(...)....
|
22
29
|
*
|
23
|
-
* Note that if your API server's endpoints are like `/api/posts`,
|
24
|
-
* then you
|
30
|
+
* Note that if your JSON API server's endpoints are like `/api/posts`,
|
31
|
+
* then you don't need the `baseApiPath()` function.
|
25
32
|
*
|
26
33
|
* fetch(`/api/posts`, {
|
27
34
|
* headers: { "Accept": "application/json" }
|
@@ -2,11 +2,10 @@ import type { Config } from "@react-router/dev/config";
|
|
2
2
|
|
3
3
|
export default {
|
4
4
|
// Config options...
|
5
|
-
// For our React Router app, we will turn off Server-side render
|
6
|
-
// use SPA mode!!
|
5
|
+
// For our React Router app, we will turn off Server-side render and use SPA mode.
|
7
6
|
ssr: false,
|
8
|
-
//
|
9
|
-
// The basename
|
7
|
+
// We serve the React Router app from the "/react/" path.
|
8
|
+
// The basename option tells React Router to manage this when generating
|
10
9
|
// Link tags, for example.
|
11
|
-
basename: "/react
|
10
|
+
basename: "/react/"
|
12
11
|
} satisfies Config;
|
@@ -1,17 +1,4 @@
|
|
1
|
-
|
2
|
-
desc "Build the React application"
|
3
|
-
task :build do
|
4
|
-
puts "Building React Router v7 app..."
|
5
|
-
system("cd frontend && npm install && npm run build")
|
6
|
-
|
7
|
-
puts "Moving build files to public/react-router..."
|
8
|
-
system("rm -rf public/react-router/*")
|
9
|
-
system("mv frontend/dist/* public/react-router/")
|
10
|
-
system("mv public/react-router/index.html public/react-router/react-router-index.html")
|
11
|
-
|
12
|
-
puts "✅ React app successfully built and deployed!"
|
13
|
-
end
|
14
|
-
end
|
1
|
+
# frozen_string_literal: true
|
15
2
|
|
16
3
|
namespace :react_router do
|
17
4
|
# For convenience, npm packages do not have to be explicitly installed.
|
@@ -30,9 +17,9 @@ namespace :react_router do
|
|
30
17
|
# this task in the Procfile.
|
31
18
|
#
|
32
19
|
# bin/rails react_router:dev
|
33
|
-
desc "Start React Router
|
20
|
+
desc "Start React Router Development Server with Hot Module Reloading"
|
34
21
|
task dev: [ :npm_install ] do
|
35
|
-
puts "Starting React Router v7 app
|
22
|
+
puts "Starting React Router v7 app development server..."
|
36
23
|
Dir.chdir("#{Dir.pwd}/frontend") do
|
37
24
|
system("npm", "run", "dev")
|
38
25
|
end
|
@@ -54,21 +41,26 @@ namespace :react_router do
|
|
54
41
|
# Running bin/rails assets:precompile will also run this task.
|
55
42
|
#
|
56
43
|
# bin/rails react_router:build
|
57
|
-
desc "Build React Router App"
|
44
|
+
desc "Build React Router App and move to the public folder"
|
58
45
|
task build: [ :npm_install ] do
|
59
|
-
|
60
|
-
|
61
|
-
system("npm", "run", "build")
|
46
|
+
puts "Building React Router v7 app..."
|
47
|
+
`cd frontend && npm run build`
|
62
48
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
49
|
+
puts "Moving build files to public/react..."
|
50
|
+
`rm -rf public/react`
|
51
|
+
`mv frontend/build/client public/react`
|
52
|
+
`mv public/react/index.html public/react/react-router-rails-spa-index.html`
|
67
53
|
|
68
|
-
|
69
|
-
end
|
54
|
+
puts "✅ React app successfully built and deployed!"
|
70
55
|
end
|
71
56
|
|
57
|
+
# Run bin/rails react_router:preview to create a preview build.
|
58
|
+
#
|
59
|
+
# This is identical to running bin/rails react_router: build
|
60
|
+
# and is provided solely to align better with intent.
|
61
|
+
desc "Preview your React Router App from the Rails development server (typically port 3000)"
|
62
|
+
task preview: [ :build ]
|
63
|
+
|
72
64
|
# Run bin/rails react_router:clobber to remove the build files.
|
73
65
|
# Running bin/rails assets:clobber will also run this task.
|
74
66
|
task :clobber do
|
@@ -3,32 +3,32 @@ import tailwindcss from "@tailwindcss/vite";
|
|
3
3
|
import { defineConfig } from "vite";
|
4
4
|
import tsconfigPaths from "vite-tsconfig-paths";
|
5
5
|
|
6
|
+
const railsEnv = process.env.RAILS_ENV || 'development'
|
7
|
+
|
6
8
|
export default defineConfig({
|
7
9
|
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
|
8
10
|
build: {
|
9
11
|
assetsDir: "assets",
|
10
|
-
// Set to true if you want sourcemaps
|
11
|
-
sourcemap: true,
|
12
|
+
// Set to true if you want sourcemaps in your build.
|
13
|
+
sourcemap: (["test", "development"].indexOf(railsEnv) !== -1) ? true : false,
|
12
14
|
},
|
13
|
-
//
|
14
|
-
// the app is served from the "/react
|
15
|
-
|
16
|
-
base: "/react-router/",
|
15
|
+
// As also defined in react-router.config.ts, this tells Vite that
|
16
|
+
// the app is served from the "/react/" sub-path.
|
17
|
+
base: "/react/",
|
17
18
|
|
18
19
|
// These are settings for the dev server.
|
19
20
|
server: {
|
20
|
-
// In production, the API server (Rails) and the assets
|
21
|
-
// will be served from the same port number (typically 80).
|
21
|
+
// In production and preview mode, the API server (Rails) and the assets
|
22
|
+
// will be served from the same port number (typically 80 for production and 3000 for preview).
|
22
23
|
//
|
23
|
-
// However,
|
24
|
-
//
|
25
|
-
//
|
26
|
-
// The use of different ports can complicate CORS setting and cookie handling.
|
24
|
+
// However, when using the vite development server, assets will be served from port 5173,
|
25
|
+
// and the API server will be on port 3000 (Rails default).
|
26
|
+
// The use of different ports complicates CORS settings and cookie handling.
|
27
27
|
//
|
28
|
-
// To
|
29
|
-
// on port (5173) and forward
|
30
|
-
//
|
31
|
-
//
|
28
|
+
// To work around this, Vite provides a proxy mode that can receive API requests
|
29
|
+
// on port (5173) and forward them to the Rails server (port 3000).
|
30
|
+
// From the browser's viewpoint, all communications including API requests to the Rails server
|
31
|
+
// will now happen on port 5173, so CORS will no longer be necessary.
|
32
32
|
proxy: {
|
33
33
|
// Any requests starting with "/api" will be forwarded according to
|
34
34
|
// the following rules.
|
@@ -44,6 +44,8 @@ export default defineConfig({
|
|
44
44
|
// Note that if the Rails server has a dedicated namespace for APIs,
|
45
45
|
// then you can just use that instead of "/api", and the following
|
46
46
|
// rewrite rule will not be necessary.
|
47
|
+
//
|
48
|
+
// Also see `frontend/app/utilities/proxy.ts` for how we handle this inside React.
|
47
49
|
rewrite: (path) => path.replace(/^\/api/, ""),
|
48
50
|
},
|
49
51
|
},
|
@@ -5,7 +5,7 @@ import railsLogo from "./rails-logo.svg";
|
|
5
5
|
export function Welcome() {
|
6
6
|
return (
|
7
7
|
<main className="flex items-center justify-center pt-16 pb-4">
|
8
|
-
<div className="flex-1 flex flex-col items-center
|
8
|
+
<div className="flex-1 flex flex-col items-center min-h-0">
|
9
9
|
<header className="flex flex-row items-baseline gap-9 max-w-[100vw]">
|
10
10
|
<div className="w-[300px]">
|
11
11
|
<img
|
@@ -28,16 +28,18 @@ export function Welcome() {
|
|
28
28
|
/>
|
29
29
|
</div>
|
30
30
|
</header>
|
31
|
-
<div className="
|
31
|
+
<div className="mt-12 text-2xl text-gray-600">with the</div>
|
32
|
+
<h1 className="mt-6 text-3xl font-bold">react_router_rails_spa gem</h1>
|
33
|
+
<div className="mt-12 max-w-[300px] w-full space-y-6 px-4">
|
32
34
|
<nav className="space-y-4">
|
33
|
-
<p className="leading-6 text-gray-700 dark:text-gray-200 text-center">
|
35
|
+
<p className="text-2xl leading-6 text-gray-700 dark:text-gray-200 text-center">
|
34
36
|
What's next?
|
35
37
|
</p>
|
36
38
|
<div className="text-center">
|
37
39
|
{resources.map(({ href, text }) => (
|
38
40
|
<a
|
39
41
|
key={href}
|
40
|
-
className="block mb-
|
42
|
+
className="block mb-4 leading-tight text-blue-700 hover:underline dark:text-blue-500"
|
41
43
|
href={href}
|
42
44
|
target="_blank"
|
43
45
|
rel="noreferrer"
|
@@ -54,12 +56,20 @@ export function Welcome() {
|
|
54
56
|
}
|
55
57
|
|
56
58
|
const resources = [
|
59
|
+
{
|
60
|
+
href: "https://github.com/naofumi/react_router_rails_spa",
|
61
|
+
text: "Read the README for the react_router_rails_spa gem",
|
62
|
+
},
|
63
|
+
{
|
64
|
+
href: "https://github.com/naofumi/react_router_rails_spa/blob/main/documents/introduction.md",
|
65
|
+
text: "Read the Introduction for the react_router_rails_spa gem",
|
66
|
+
},
|
57
67
|
{
|
58
68
|
href: "https://reactrouter.com/docs",
|
59
|
-
text: "React Router Docs",
|
69
|
+
text: "Read the React Router Docs",
|
60
70
|
},
|
61
71
|
{
|
62
|
-
href: "https://
|
63
|
-
text: "
|
72
|
+
href: "https://rubyonrails.org/",
|
73
|
+
text: "Read the Ruby on Rails Docs",
|
64
74
|
},
|
65
75
|
];
|
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: react_router_rails_spa
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Naofumi Kagami
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-04-
|
12
|
-
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: rails
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: 8.0.2
|
20
|
-
type: :development
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: 8.0.2
|
11
|
+
date: 2025-04-22 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
27
13
|
description:
|
28
14
|
email:
|
29
15
|
- naofumi@mac.com
|
@@ -32,7 +18,6 @@ extensions: []
|
|
32
18
|
extra_rdoc_files: []
|
33
19
|
files:
|
34
20
|
- ".idea/.gitignore"
|
35
|
-
- ".idea/.name"
|
36
21
|
- ".idea/inspectionProfiles/Project_Default.xml"
|
37
22
|
- ".idea/misc.xml"
|
38
23
|
- ".idea/modules.xml"
|
@@ -44,9 +29,12 @@ files:
|
|
44
29
|
- LICENSE.txt
|
45
30
|
- README.md
|
46
31
|
- Rakefile
|
32
|
+
- documents/introduction.md
|
47
33
|
- lib/react_router_rails_spa.rb
|
34
|
+
- lib/react_router_rails_spa/csrf/csrf_cookie_enabled.rb
|
48
35
|
- lib/react_router_rails_spa/generators/install_generator.rb
|
49
36
|
- lib/react_router_rails_spa/generators/templates/csrf.ts
|
37
|
+
- lib/react_router_rails_spa/generators/templates/csrf_cookie_enabled.rb
|
50
38
|
- lib/react_router_rails_spa/generators/templates/home.tsx
|
51
39
|
- lib/react_router_rails_spa/generators/templates/proxy.ts
|
52
40
|
- lib/react_router_rails_spa/generators/templates/react-router.config.ts
|
@@ -65,6 +53,7 @@ metadata:
|
|
65
53
|
homepage_uri: https://github.com/naofumi/react_router_rails_spa
|
66
54
|
source_code_uri: https://github.com/naofumi/react_router_rails_spa
|
67
55
|
changelog_uri: https://github.com/naofumi/react_router_rails_spa
|
56
|
+
rubygems_mfa_required: 'true'
|
68
57
|
post_install_message:
|
69
58
|
rdoc_options: []
|
70
59
|
require_paths:
|
@@ -83,5 +72,5 @@ requirements: []
|
|
83
72
|
rubygems_version: 3.4.10
|
84
73
|
signing_key:
|
85
74
|
specification_version: 4
|
86
|
-
summary:
|
75
|
+
summary: Integrate React Router v7 SPA framework with your Ruby on Rails backend.
|
87
76
|
test_files: []
|
data/.idea/.name
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
re_rou_ra
|