react_router_rails_spa 0.1.0 → 0.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 261fa2216af64ecdaff4a8d74850b7ccebd05f4c3d0fbd8af986fd0658faf873
4
- data.tar.gz: 262c1899faf8d7b574d7c6523fb864139c640a1faf274c581f07139d68ae68bd
3
+ metadata.gz: a8b4be06b2470f7ece193c99c21dca1af7ae3856591399c8e16b6170bcc54c9e
4
+ data.tar.gz: b845de80454767f463266504cff5028f098d9de7bb4837ff3de0d310c08ef5a5
5
5
  SHA512:
6
- metadata.gz: dec2a2a6262c9b3cbff0fa5f8f301d55cef2433b0f2b29350a610874240b735ca7d6b265f271df8dbd91e33cd922c7a41c038417ed22dd3f195a3dea77b671ac
7
- data.tar.gz: 6419659c7dd675ea3e531efc15a6dfaef38597db12d420f84eb9ac61c0bb088121998c4e333bf2b2801d552a34010f0d802ca3aa494fe44cfc0534726e83791c
6
+ metadata.gz: 17b47b7846d437d18bc48ff589f183b5a7b1a8d7e9406cd34ff9a3e19eab5d388facdbcb1685bf3df56a52a81e3300eb50fe6fbcbd627e73233f7eb1028d7633
7
+ data.tar.gz: e781941c2404d2e5f7e41be2249cb3ed17fc56d3975d1a6d23324b02d1cf2b7fc05f9f3ed0784de29319dbcc18c9f7c2e9fa5e46445b40bb555d2a99c1e2937f
data/README.md CHANGED
@@ -1,38 +1,139 @@
1
- # ReactRouterRails
1
+ ## React Router SPA Framework mode integration for Ruby on Rails
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
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
+ The React app is built as static assets in your Rails `public` folder, and so it can be deployed on your current production server with minimal, if any, configuration changes.
4
5
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/react_router_rails_spa`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+ Benefit from state-of-the-art React SPA technlologies
7
+ the integrated client-side router, loader data-fetching pattern,
8
+ automatic code-splitting, and development server with HMR,
9
+ without the costs and complexities of running a separate front-end server.
10
+
11
+ If you are using Next.js, not for SEO
12
+ but only for convenience,
13
+ if you are not using SSR but fetching data on the client-side,
14
+ then this gem will provide you with a similarly convenient but simpler, cheaper and more performant solution.
15
+
16
+ You may be considering the traditional approaches to React and Rails integration such as [Webpacker](https://github.com/rails/webpacker),
17
+ [jsbundling-rails](https://github.com/rails/jsbundling-rails), [Vite Ruby](https://github.com/ElMassimo/vite_ruby)
18
+ but concerned about installing and configuring additional packages, and avoiding SPA pitfalls.
19
+ With a single gem and a single command, this gem sets up all that you need in "Omakase"-style – Rails route and controller setup, an integrated client-side router, automatic per-route code-splitting, and the loader data-fetch pattern to eliminate data-fetch waterfalls.
20
+
21
+ ## Who is it for?
22
+
23
+ Consider trying out this gem if you fit any of the following descriptions.
24
+
25
+ - You want to integrate React into a Ruby on Rails application.
26
+ - You want an out-of-the-box solution for a state-of-the-art multipage React application. 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" on the front-end as well as your Rails back-end.
27
+ - Your Ruby on Rails application already has ERB (Hotwire) pages.
28
+ - You do not need SEO for the React generated pages (you don't need SSR/SSG).
29
+ - You can always use ERB views for the pages that neww SEO.
30
+ - You do not want to host multiple servers for your frontend and your backend.
31
+ - You do not want to incur the additional costs, complexity, and authentication concerns that are inherent when dealing with multiple servers.
32
+ - You want to simply deploy your React frontend as static assets on a single server, inside your Ruby on Rails `public` folder.
33
+ - 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
34
 
7
35
  ## Installation
8
36
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
37
+ The React Router application will be installed inside the `frontend` directory. We assume that you have an existing Ruby on Rails application.
38
+
39
+ Add this line to your application's Gemfile:
40
+
41
+ ```ruby
42
+ gem 'react_router_rails_spa'
43
+ ```
44
+
45
+ Then, run:
46
+
47
+ ```shell
48
+ bundle install
49
+ bin/rails generate react_router_rails_spa:install
50
+ ```
51
+
52
+ This will create a new directory called `frontend` inside the project root.
53
+ It will also create a React bootstrap endpoint for all paths starting with `/react`.
54
+ The endpoint will be handled by `ReactController#show`.
10
55
 
11
- Install the gem and add to the application's Gemfile by executing:
56
+ You will also have rake tasks for starting the dev server and building/previewing the React app.
12
57
 
13
- ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
58
+ As part of the integration, we provide utilities for using the robust CSRF protection built into Ruby on Rails from your React application.
59
+
60
+ ## Running the React Router development server
61
+
62
+ React Router is built with Vite and uses the Vite development server to provide a Hot Module Replacement (HMR) capability, that is very helpful for routing editing of pages.
63
+ However, the development server is not used in production and will not widely represent the application's behavior in production.
64
+ We therefore strongly recommend that you build the React Router assets into the `public` folder and preview it before deploying into production.
65
+
66
+ Start the Vite development server with HMR with the following command (we assume that the Ruby on Rails server is already running (with either `bin/rails s` or `bin/dev`).
67
+
68
+ ```shell
69
+ bin/rails react_router:dev
15
70
  ```
16
71
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
72
+ Access the development server from the URL outputted from this command (Typically `http://localhost:5173/react`)
18
73
 
19
- ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
74
+ To preview the production build, run the following command.
75
+ ```shell
76
+ bin/rails react_router:preview
21
77
  ```
22
78
 
23
- ## Usage
79
+ This will build the React Router application into the Rails `public` folder, and the React Router application will be available from the Rails development server (puma) at `http://localhost:3000/react`.
80
+ The preview will be representative of the production app's behavior.
81
+
82
+ ## Deployment
83
+
84
+ This gem integrates with the Ruby on Rails Asset pipeline.
85
+
86
+ The React Router application is automatically built whenever `bin/rails assets:precompile` is run
87
+ and therefore no changes should be required since your Ruby on Rails application should already do this.
88
+
89
+ If your deployment pipeline does not already install Node (for example, you have a "no-build" deployment for Rails),
90
+ then install it in your CI/CD environment since building the React Router application will require it.
91
+
92
+ ## Background
93
+
94
+ Ruby on Rails has officially supported bundling of complex JavaScript frontend libraries,
95
+ first with [Webpacker](https://github.com/rails/webpacker),
96
+ and currently with [jsbundling-rails](https://github.com/rails/jsbundling-rails).
97
+ We also have [Vite Ruby](https://github.com/ElMassimo/vite_ruby) with an integrated dev server, HMR and other goodies.
98
+
99
+ However, on February 14th, 2025,
100
+ the React team announced the [official deprecation of Create React App (CRA)](https://react.dev/blog/2025/02/14/sunsetting-create-react-app).
101
+ Instead, they suggested developers should use a framework that builds single-page apps
102
+ (SPA) deployable to a CDN or a static hosting service –
103
+ In other words, SPA frameworks such as Next.js or React Router.
104
+ Importantly,
105
+ they advised
106
+ against [building a React app from scratch](https://react.dev/learn/build-a-react-app-from-scratch)
107
+ unless your app has constraints not well-served by existing SPA frameworks.
108
+
109
+ This approach poses challenges to the traditional Rails integration solutions.
110
+ They all compile the React applications to a single JavaScript file
111
+ that is then loaded from an ERB file (the React bootstrap HTML) containing helper functions such as
112
+ `javascript_include_tag` (jsbundling-rails), `javascript_pack_tag` (webpacker), or `vite_javascript_tag` (vite-rails).
113
+ However, SPA frameworks do not just compile to a single JavaScript file.
114
+ Instead,
115
+ they additionally generate their own React bootstrap HTML file which includes many framework-specific optimizations
116
+ (React Router v7 generates this using SSG from `app/root.tsx`).
24
117
 
25
- TODO: Write usage instructions here
118
+ This gem uses the bootstrap HTML file
119
+ generated by the SPA framework instead of an ERB file.
120
+ This is served through an ActionController endpoint,
121
+ enabling you to use cookies to send session-specific information on the first HTML load
122
+ and is used in this gem to send CSRF tokens.
26
123
 
27
- ## Development
124
+ ## Notes when using Rails in API mode
28
125
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
126
+ Ruby on Rails provides an API mode that removes many frontend features.
127
+ Importantly, API mode implies a stateless API server that does not support cookies.
128
+ It removes the middleware for cookie handling and also for CSRF protection.
30
129
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
130
+ 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.
131
+ If you want to convert your API-mode application to use cookies, make sure to also restore CSRF features.
132
+ Otherwise, your app will be vulnerable to CSRF attacks.
32
133
 
33
134
  ## Contributing
34
135
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/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/[USERNAME]/re_rou_ra/blob/main/CODE_OF_CONDUCT.md).
136
+ 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
137
 
37
138
  ## License
38
139
 
@@ -40,4 +141,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
40
141
 
41
142
  ## Code of Conduct
42
143
 
43
- Everyone interacting in the ReactRouterRails project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/react_router_rails_spa/blob/main/CODE_OF_CONDUCT.md).
144
+ 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,290 @@
1
+ # Integrating Ruby on Rails with Modern SPAs
2
+
3
+ ## TL;DR;
4
+
5
+ This article describes the [react_router_rails_spa gem](https://github.com/naofumi/react_router_rails_spa), which allows you to integrate a React Router SPA framework application into you existing Ruby on Rails project.
6
+
7
+ To jump to the installation steps, go to the "Steps to Integrate React Router into your Rails Application" section.
8
+
9
+ ## Who is this for?
10
+
11
+ The react_router_rails_spa gem was designed for the following situations.
12
+
13
+ ### You want to create a web application with a React frontend and a Rails backend.
14
+
15
+ * 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. You don't want to manually add routes and controllers on the Rails side. Everything should be a single gem and a single command.
16
+ * You want something that is easy to deploy, and cost-effective. You don't want to worry about server charges.
17
+ * You don't need SEO for the React pages.
18
+ * If SEO is necessary, you can just serve ERB pages or static HTML files.
19
+
20
+ ### You are considering Next.js, but you do not need SSR nor RSCs. You are only considering Next.js because it is easy to set up.
21
+
22
+ * You're only using Next.js because you thought it was ["omakase"](https://dhh.dk/2012/rails-is-omakase.html). You actually found Rails integration harder than you bargained for.
23
+ * You are worried about deployment. Specifically, you are not happy with Vercel pricing or the extra cost of an additional AWS ECS instance to host Next.js.
24
+
25
+ The react_router_rails_spa gem satisfies the above requirements and more.
26
+ It will give you an ["omakase"](https://dhh.dk/2012/rails-is-omakase.html) Modern React SPA with a single command.
27
+
28
+ ### You want to use React because you believe you can create better UIs
29
+
30
+ * You're using React because you think you can create a better UI/UX compared to Hotwire (I actually think that this is untrue and Hotwire can create great UI/UXs, but that's a different discussion)
31
+ * You want to use cutting-edge React capabilities like code-splitting, loader-based data fetching and more. You don't want to create a slow, bloated, legacy React app.
32
+
33
+ ## Who is this NOT for?
34
+
35
+ ### You want to embedd some React components on top of your ERB-rendered pages
36
+
37
+ This is [the original way that React was used](https://react.dev/learn/add-react-to-an-existing-project#using-react-for-a-part-of-your-existing-page).
38
+ If you wish to take this approach,
39
+ 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).
40
+ Turbo Mount uses Stimulus to mount components, and is therefore more robust if you are using Hotwire in your ERB views.
41
+
42
+ ## Background
43
+
44
+ On February 14th, 2025,
45
+ the React team published a blog post
46
+ titled [Sunsetting Create React App](https://react.dev/blog/2025/02/14/sunsetting-create-react-app). They strongly recommended
47
+ that **developers should now use an SPA framework instead**.
48
+
49
+ Importantly, and often lost in the public discourse, they were **NOT** recommending an **SSR** framework.
50
+ 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)
51
+ that could be deployed on a CDN, a static hosting service, or the `public` folder of a Ruby on Rails application.
52
+
53
+ In the following, I will call SPAs built with an SPA framework, **"Modern React SPAs"**
54
+ to highlight that this is the current officially recommended approach.
55
+ To contrast, I will call the ones that the React team is actively discouraging, **"Legacy React SPAs"**.
56
+
57
+ > **"I have no interest nor use for SSR!
58
+ I don't need SEO.
59
+ > An SPA is all that I need.
60
+ > Instead of Create React App, I'll just use Vite!"**
61
+
62
+ This was the most common response to the blog post.
63
+ However IMO, it misses the point that the authors were repeatedly trying to make.
64
+
65
+ The React team is strongly recommending that **even if you only need an SPA, you should be creating a Modern SPA**.
66
+ [They carefully go through some of the features](https://react.dev/learn/build-a-react-app-from-scratch) like code-splitting
67
+ and loader-based routing
68
+ that you will need to add if you are building a state-of-the-art React SPA from scratch.
69
+ These features are often challenging and require expertise to correctly implement,
70
+ but are essential for modern React applications.
71
+
72
+ Without these features, your React SPA is a Legacy SPA.
73
+ It will suffer from the same performance issues that plagued old SPAs a decade ago –
74
+ namely huge initial bundle size, data-fetch waterfalls, flickering, and very slow load times.
75
+
76
+ In the above article,
77
+ the React team went out of its way
78
+ to tell us
79
+ that we should not simply replace the deprecated Create React App with a newer but nonetheless still architecturally Legacy SPA.
80
+ Instead, they strongly urge us to embrace Modern React SPAs and avoid these issues.
81
+
82
+ I should note that Vite is essentially a bundler and a development server, with a plugin system that allows us to easily install various packages.
83
+ It is agnostic to the Legacy vs. Modern SPA debate.
84
+ You can build a Legacy SPA using Vite, and you can also create a Modern SPA.
85
+ Vite does not care either way, and the installer command `npm create vite@latest` gives you both templates.
86
+
87
+ > **"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!"**
88
+
89
+ This is also a common response from people who prefer a no-fuss solution for Rails.
90
+ However, note that this is exactly the approach that the React team strongly discourages.
91
+ The above solution is a Legacy SPA and will suffer from the same legacy issues.
92
+
93
+ Instead, the React team is recommending that you integrate a Modern SPA framework using ...
94
+
95
+ Well, actually, they don't have a concrete recommendation yet for Rails.
96
+ As far as I know, nothing currently exists to easily integrate a Modern SPA with Rails.
97
+
98
+ I hope to address this with this [react_router_rails_spa gem](https://github.com/naofumi/react_router_rails_spa).
99
+
100
+ ## Why we need a different approach for Rails integration
101
+
102
+ Historically,
103
+ the way to integrate React with Ruby on Rails was to create an ERB endpoint that served as the initial HTML
104
+ (the bootstrap HTML) for React.
105
+ This ERB template would have either a `javascript_include_tag` (jsbundling-rails) or a
106
+ `javascript_pack_tag` (webpacker) to load the React application build artifact.
107
+ Newer gems like [Vite Rails](https://vite-ruby.netlify.app/guide/rails.html#tag-helpers-🏷) have also adopted the same approach.
108
+
109
+ However, this is exactly what the React team is discouraging, and it seems unwise to continue down this path.
110
+
111
+ The problem is that Modern SPAs build their own bootstrap HTML templates with SSG
112
+ (the first HTML that the browser loads).
113
+ Modern SPA frameworks are not just JavaScript.
114
+ Instead, the bootstrap HTML and the JavaScript are tightly integrated.
115
+
116
+ Therefore, to take advantage of Modern SPA features,
117
+ Rails has
118
+ to give up
119
+ on generating its own bootstrap HTML from an ERB template with an embedded `javascript_include_tag`.
120
+ Instead, we have to take the bootstrap HTML generated by the SPA framework, and wrap Rails around this.
121
+
122
+ This is how the react_router_rails_spa gem works.
123
+
124
+ ## Outline of how the gem works
125
+
126
+ 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,
127
+ paired with the generation of a single Rails controller.
128
+
129
+ There is very little custom code, and you can easily update your NPM packages independently of this gem. The generated code is also heavily commented to help you understand the internals for yourself.
130
+
131
+ ### React Router
132
+
133
+ We install and use React Router in framework mode, [configured to generate an SPA build](https://reactrouter.com/how-to/spa).
134
+ This will build static files that can be served from any static hosting provider, including the `public` folder in Rails.
135
+
136
+ One of these static files is the bootstrap HTML template (the root `index.html` file).
137
+ We change the name of this file so that it will not be directly served from the `public` folder.
138
+ Instead,
139
+ we use a dedicated Rails controller to add Rails-generated HTTP headers and cookies
140
+ and serve the file as the response body.
141
+
142
+ After building, these static files will be transferred to the Rails `public` folder from which they can be deployed like any other static asset.
143
+
144
+ ### Rails routes.rb and the ReactController
145
+
146
+ We generate a `ReactController` that serves the bootstrap HTML template.
147
+ The body of the response is the exact contents of the React Router-generated `index.html` file, but
148
+ by passing it through the `ReactController`,
149
+ we can customize the headers and add session-specific information as cookies.
150
+
151
+ For example, `ReactController`
152
+ includes the `ReactRouterRailsSpa::CsrfCookieEnabled` module
153
+ which sends session-specific CSRF tokens via cookies to integrate Rails'
154
+ CSRF protection with React.
155
+
156
+ `ReactController` will also add session cookies so that you can take advantage of session information from the bootstrap HTML file onwards.
157
+
158
+ Finally, the `ReactController` allows you to set cache headers separately from assets served directly from the `assets` folder.
159
+
160
+ Rails uses the [ActionDispatch::Static middleware](https://api.rubyonrails.org/classes/ActionDispatch/Static.html)
161
+ to serve assets from the `public` folder,
162
+ and this sets the HTTP caching headers aggressively to allow extensive caching for long periods of time.
163
+ While this is great for JavaScript, CSS and image assets,
164
+ this is usually undesirable for HTML files since we cannot attach cache-busting digests to them.
165
+ Serving the bootstrap HTML template through `ReactController` allows us
166
+ to easily change the cache headers to values that are suitable for HTML responses.
167
+ Currently, the [react_router_rails_spa gem](https://github.com/naofumi/react_router_rails_spa) uses the same cache headers as other ERB files,
168
+ but this can be customized in the controller.
169
+
170
+ Another way to look at this integration is like this;
171
+
172
+ * The traditional approach taken by webpack, esbuild and vite-rails, is to communicate between the React application and Rails via a Rails generated ERB bootstrap HTML. For example, Rails-generated CSRF tokens were provided as `meta` tags embedded in HTML.
173
+ * The current approach is to use the React Router generated bootstrap HTML and to add Rails integration through HTTP headers (including cookies) only. That's why this gem sends Rails-generated CSRF tokens to the React application using cookies.
174
+
175
+ ### Rake files for automation
176
+
177
+ We provide rake tasks for starting up the development server and building the React Router application.
178
+
179
+ Note that the build task is attached to the `assets:precompile` task.
180
+ This means
181
+ that you do not need
182
+ to add extra configuration to your CI/CD scripts
183
+ to build the React Router app since it should normally call this task already.
184
+
185
+ 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.
186
+
187
+ ### Additional React Router and Vite Configuration
188
+
189
+ We currently serve the React application from the `/react/*` paths.
190
+ All other paths are handled by Rails.
191
+ The current gem adds minor configurations for this.
192
+ If you want a different setup, you can change the configurations.
193
+
194
+ Note that configurations for the Vite development server are tricky.
195
+ We have provided settings
196
+ to compensate for the fact that the development server runs on port 5173 while the Rails application runs on port 3000.
197
+ However, this will not allow you to test integration between Rails and React.
198
+
199
+ * The React app runs on port 5173 while the ERB files are on port 3000. Links between the two will not work on the development server, even if they are fine in production.
200
+ * The React app running on the development server will not bootstrap from the Ruby on Rails endpoint on the `ReactController#show` action. Instead, the development server will directly serve the React Router generated bootstrap HTML. This means that the bootstrap file will not contain Rails integrations. This is only an issue on the development server and not in production.
201
+
202
+ As a solution, you can use the development server with HMR for small fixes, but for larger changes,
203
+ you will need to use a "preview" build.
204
+ You should also always check with a "preview" build before deploying to production.
205
+
206
+ We provide a rake task for building a "preview".
207
+
208
+ ```shell
209
+ bin/rails react_router:preview
210
+ ```
211
+
212
+ ## Demo and Source code
213
+
214
+ * I have a [demo application based on the current proposal running on Kamal on a VPS server](https://rrrails.castle104.com/react-router/). It has simple, session-based authentication and basic CRUD. Mutations are secured by integration with Rails CSRF protection.
215
+ * In the demo application, I have intentionally added a 0.5 to 1.5-second 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.
216
+ * The source-code for this demo application is [available on GitHub](https://github.com/naofumi/react-router-vite-rails).
217
+
218
+ The source code is heavily commented. I recommend that you read through it to understand the setup in more detail.
219
+
220
+ ## Using the gem
221
+
222
+ ### Install Ruby on Rails
223
+
224
+ This gem works with a pre-existing installation of Rails. Create a new Rails application if you haven't already.
225
+
226
+ ```shell
227
+ rails new [project name]
228
+ ```
229
+
230
+ Note that this gem works even with a no-build Rails setup (which is the Rails default),
231
+ but you will need Node on your machine to install React Router.
232
+
233
+ ### Install the `react_router_rails_spa` gem
234
+
235
+ Add the following line to your `Gemfile`.
236
+
237
+ ```ruby
238
+ gem "react_router_rails_spa"
239
+ ```
240
+
241
+ Install the gem
242
+
243
+ ```shell
244
+ bundle install
245
+ ```
246
+
247
+ We recommend committing your changes at this point before the following generator adds and modifies your files.
248
+
249
+ Run the generator
250
+
251
+ ```shell
252
+ bin/rails generate react_router_rails_spa:install
253
+ ```
254
+
255
+ This will install the latest version of React Router
256
+ and generate the routes and all the necessary files and configurations.
257
+
258
+ ### Run the development server
259
+
260
+ Run the following command to start the development server. This comes with HMR (Hot Module Replacement).
261
+
262
+ ```shell
263
+ bin/rails react_router:dev
264
+ ```
265
+
266
+ Point your browser to http://localhost:5173/react/ to see the welcome page.
267
+
268
+ ### Build the React Router application
269
+
270
+ Run the following command to build the React Router application
271
+ and store the static files into the Rails `public` directory.
272
+
273
+ ```shell
274
+ bin/rails react_router:build
275
+ ```
276
+
277
+ Start your Rails application if it is not already running.
278
+
279
+ Point your browser to http://localhost:3000/react/ to see the welcome page.
280
+
281
+ This command is also aliased as `preview`.
282
+
283
+ ```shell
284
+ bin/rails react_router:preview
285
+ ```
286
+
287
+ ### Read the added code
288
+
289
+ I have added numerous comments to the code generated by this gem. Please read it to understand how the integration works.
290
+
@@ -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 "Setting up React Router v7 SBA mode in Rails..."
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
- route 'get "react/*path", to: "react#show"'
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
- create_file "app/controllers/react_controller.rb", <<-RUBY
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 matches what was sent via the cookie.
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
- * In this example application, the Ruby on Rails APIs endpoints are like
3
- * `GET /posts` or `GET /users` and do not have any prefixes like `/api*`.
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
- * However, during development, the port for the Vite development server (typically 5173) is different from
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 Vite server needs to distinguish
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 Vite assets).
14
+ * ones that it should handle itself on port 5173 (e.g., the React Router assets).
10
15
  *
11
- * Therefore, when running on the Vite development server, we prefix
12
- * requests intended for the Rails API server with "/api" to tell Vite to send them
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
- * See the `proxy:` section in `frontend-react-router/vite.config.ts` for
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 can write like the following, without the `baseApiPath()` function.
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
- // In the above, we have decided to serve the React Router app from "/react-router/".
9
- // The basename options tell React Router to manage this when generating
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-router/"
10
+ basename: "/react/"
12
11
  } satisfies Config;
@@ -1,17 +1,4 @@
1
- namespace :react do
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 Dev Server"
34
- task dev: [ :npm_install ] do
35
- puts "Starting React Router v7 app dev server..."
20
+ desc "Start React Router Development Server with Hot Module Reloading"
21
+ task dev: [:npm_install] do
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
@@ -40,7 +27,7 @@ namespace :react_router do
40
27
 
41
28
  # bin/rails react_router:typecheck
42
29
  desc "Check Typescript for the React Router App"
43
- task typecheck: [ :npm_install ] do
30
+ task typecheck: [:npm_install] do
44
31
  puts "Check Typescript for React Router v7 app..."
45
32
  Dir.chdir("#{Dir.pwd}/frontend") do
46
33
  system("npm", "run", "typecheck")
@@ -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"
58
- task build: [ :npm_install ] do
59
- Dir.chdir("#{Dir.pwd}/frontend") do
60
- puts "Building React Router v7 app..."
61
- system("npm", "run", "build")
44
+ desc "Build React Router App and move to the public folder"
45
+ task build: [:npm_install] do
46
+ puts "Building React Router v7 app..."
47
+ `cd frontend && npm run build`
62
48
 
63
- puts "Moving build files to public/react..."
64
- system("rm -rf public/react/*")
65
- system("mv frontend/build/client public/react")
66
- system("mv public/react/index.html public/react/react-router-rails-spa-index.html")
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
- puts "✅ React app successfully built and deployed!"
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
@@ -83,5 +75,5 @@ end
83
75
  # This means that any normal Rails deployment script which
84
76
  # contains rake assets:precompile will also build the
85
77
  # React Router app automatically.
86
- Rake::Task["assets:precompile"].enhance([ "react_router:build" ])
87
- Rake::Task["assets:clobber"].enhance([ "react_router:clobber" ])
78
+ Rake::Task["assets:precompile"].enhance(["react_router:build"])
79
+ Rake::Task["assets:clobber"].enhance(["react_router:clobber"])
@@ -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
- // Like in react-router.config.ts, this tells Vite that
14
- // the app is served from the "/react-router/" sub-path.
15
- // Change this if you want to use a different sub-path name.
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, in development, assets will be served from the
24
- // Vite dev server on port 5173, and the API server will be on
25
- // port 3000 (Rails default).
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 prevent this problem, Vite provides a proxy mode that can receive API requests
29
- // on port (5173) and forward this to the Rails server (port 3000).
30
- // The browser will think that all communication happens on port 5173 so CORS
31
- // is no longer necessary.
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 gap-16 min-h-0">
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="max-w-[300px] w-full space-y-6 px-4">
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&apos;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-2 leading-normal text-blue-700 hover:underline dark:text-blue-500"
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/documentation/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://rmx.as/discord",
63
- text: "Join Discord",
72
+ href: "https://reactrouter.com/docs",
73
+ text: "Read the Ruby on Rails Docs",
64
74
  },
65
75
  ];
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReactRouterRailsSpa
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "react_router_rails_spa/version"
4
4
  require_relative "react_router_rails_spa/generators/install_generator"
5
+ require_relative "react_router_rails_spa/csrf/csrf_cookie_enabled"
5
6
 
6
7
  module ReactRouterRailsSpa
7
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: react_router_rails_spa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
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-07 00:00:00.000000000 Z
11
+ date: 2025-04-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -32,7 +32,6 @@ extensions: []
32
32
  extra_rdoc_files: []
33
33
  files:
34
34
  - ".idea/.gitignore"
35
- - ".idea/.name"
36
35
  - ".idea/inspectionProfiles/Project_Default.xml"
37
36
  - ".idea/misc.xml"
38
37
  - ".idea/modules.xml"
@@ -44,9 +43,12 @@ files:
44
43
  - LICENSE.txt
45
44
  - README.md
46
45
  - Rakefile
46
+ - documents/introduction.md
47
47
  - lib/react_router_rails_spa.rb
48
+ - lib/react_router_rails_spa/csrf/csrf_cookie_enabled.rb
48
49
  - lib/react_router_rails_spa/generators/install_generator.rb
49
50
  - lib/react_router_rails_spa/generators/templates/csrf.ts
51
+ - lib/react_router_rails_spa/generators/templates/csrf_cookie_enabled.rb
50
52
  - lib/react_router_rails_spa/generators/templates/home.tsx
51
53
  - lib/react_router_rails_spa/generators/templates/proxy.ts
52
54
  - lib/react_router_rails_spa/generators/templates/react-router.config.ts
@@ -83,5 +85,5 @@ requirements: []
83
85
  rubygems_version: 3.4.10
84
86
  signing_key:
85
87
  specification_version: 4
86
- summary: Use React Router v7 framework on your Rails-powered SPA.
88
+ summary: Integrate React Router v7 SPA framework with your Ruby on Rails backend.
87
89
  test_files: []
data/.idea/.name DELETED
@@ -1 +0,0 @@
1
- re_rou_ra