Skip to content

Commit f24b667

Browse files
committed
Documentation update
1 parent 0116db5 commit f24b667

File tree

11 files changed

+109
-59
lines changed

11 files changed

+109
-59
lines changed

README.md

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,48 @@
11
# React-Router-Vite-Rails
22

3-
This is an example web application proposing a way to integrate React Router Framework/SPA mode and Ruby on Rails.
3+
This is an example web application that proposes a way to integrate React Router Framework/SPA mode and Ruby on Rails.
44

5-
Consider using this if you wish to easily create a better React SPA integrated with Ruby on Rails.
5+
Consider using the methods in this example
6+
to explore a simple way to create a better React SPA integration for Ruby on Rails.
67

78
[Jump to how to build it](#how-it-is-built)
89
[See this example app deployed using Kamal](https://rrrails.castle104.com)
910

1011
## The problem
1112

1213
Historically,
13-
integrating React with Ruby on Rails was done by creating a dedicated Rails route and ERB file as the bootstrap file
14+
integrating React with Ruby on Rails was done by creating a dedicated Rails route from which you served a static ERB file as the bootstrap file
1415
(the first HTML file that the browser loads).
1516

16-
You would use `javascript_include_tag` (jsbundling or sprockets)
17-
or `javascript_pack_tag` (webpacker) to load the JavaScript bundle that contains your React application.
18-
If you wish to use Vite,
17+
Inside this ERB file, you would typically use `javascript_include_tag` ([jsbundling](https://github.com/rails/jsbundling-rails) or [sprockets](https://github.com/rails/sprockets))
18+
or `javascript_pack_tag` ([webpacker](https://github.com/rails/webpacker)) to load the JavaScript bundle that contained your React application.
19+
If you wished to use Vite,
1920
you might use [Vite Rails](https://github.com/ElMassimo/vite_ruby/tree/main/vite_rails)
20-
and then use the `vite_javascript_tag`.
21+
and then use the corresponding `vite_javascript_tag` in exactly the same way.
2122

22-
However, the traditional approach treats React as a library.
23-
You are responsible for installing the client-side router, implementing code-splitting, avoiding fetch waterfalls, etc.
24-
Although this approach may have been adequate in the past, the React team has recommended against it and suggested that [even for SPAs, developers should use a framework](https://react.dev/blog/2025/02/14/sunsetting-create-react-app).
23+
However, this traditional approach treats React as a library.
24+
You are responsible for installing the client-side router,
25+
implementing code-splitting, avoiding fetch waterfalls, etc.,
26+
all of which have significant implications for performance.
27+
Although this approach may have been adequate in the past,
28+
the React team has recommended against it and suggested that [even if you are only interested in a SPA,
29+
developers should use a framework](https://react.dev/blog/2025/02/14/sunsetting-create-react-app).
2530

26-
With this in mind, I propose a solution that allows you to easily integrate an SPA framework with Ruby on Rails.
31+
With this in mind, I propose a simple solution that allows you to integrate an SPA framework with Ruby on Rails.
2732

2833
## The proposal
2934

3035
The current proposal uses React Router version 7 in SPA/Framework mode.
3136

3237
* Instead of creating a bootstrap file (the HTML file that is initially loaded by the browser) in ERB, we use the one that React Router builds in SPA mode (using SSG).
33-
* We send the static bootstrap file through Rails controllers instead of serving it in the `public` directory. This allows us to set caching headers separately and manage cookies efficiently, simplifying authentication and CSRF protection.
38+
* We send this static bootstrap file to the browser through Rails controllers instead of serving it in the `public` directory. This allows us to set caching headers separately and manage cookies efficiently, simplifying authentication and CSRF protection.
3439

3540
### Compared to the traditional method
3641

3742
* Compared to the traditional approach where React is treated as a library, an SPA framework will integrate a client-side routing library.
38-
* It will also give you automatic code-splitting together with data-fetching parallelization and other benefits.
43+
* It will also give you automatic code-splitting together with data-fetching parallelization and other benefits that are important for performance and UX.
3944

40-
Putting it simply, it should make it easier to create a better SPA.
45+
Putting it simply, the current method should make it easier to create a better SPA.
4146

4247
### Compared to hosting static files on a separate server
4348

@@ -50,31 +55,35 @@ Putting it simply, it should make it easier to create a better SPA.
5055
* Hosting a separate Next.js SSR server will typically require you to re-implement authentication inside Next.js. The current approach can simply use Rails' authentication as is.
5156
* With the current approach, cross-site requests and CORS will no longer be a concern.
5257

53-
Note that you can use Next.js as an SPA
54-
and [use static export](https://nextjs.org/docs/app/building-your-application/upgrading/single-page-applications#static-export-optional).
55-
This will also allow you
56-
to host static files in the Ruby on Rails `public` folder although there are difficulties with dynamic routes.
58+
Note that you can use Next.js [with static exports](https://nextjs.org/docs/app/building-your-application/upgrading/single-page-applications#static-export-optional)
59+
to generate files that can be statically hosted as an SPA.
60+
You could put these on a Ruby on Rails `public` folder to achieve simplicity that is similar to the current approach.
61+
However, Next.js static exports have difficulties, particularly with dynamic routes,
62+
which make it a less straightforward than React Router v7 in SPA mode.
63+
Hence, my choice of React Router v7 instead.
5764

5865
## Building the Integration
5966

60-
I have heavily added comments to each file. Please look through these to see how the application is configured.
67+
I have heavily added comments to each file in this repository.
68+
Please read these to understand how the application is configured.
6169

6270
The step-by-step setup is as follows.
6371

6472
### Install Ruby on Rails
6573

6674
Just run `rails new` to set up the Ruby on Rails server.
67-
A no-build setup will suffice since Rails is not responsible for building the React application.
68-
If you want, you can use jsbundling for additional JavaScript.
69-
Note that any build using jsbundling will be completely independent of the React app.
75+
The default, no-build setup will suffice since Rails is not responsible for building the React application.
76+
If you want, you can use [jsbundling](https://github.com/rails/jsbundling-rails) for additional JavaScript.
77+
Note that the React Router app will be completely independent of jsbundling.
7078

7179
```shell
7280
rails new [react-router-vite-rails]
7381
```
7482

7583
\[react-router-vite-rails] is the name of the project.
7684

77-
Prepare a route and a controller action to serve the bootstrap HTML template. View the following files in the current directory for guidance.
85+
Prepare a route and a controller action to serve the bootstrap HTML template.
86+
View the comments in the following files for guidance.
7887

7988
* `config/routes.rb`
8089
* `app/controllers/react_controller.rb`
@@ -99,7 +108,7 @@ npx create-react-router@latest [frontend-react-router]
99108

100109
Configure the following files to run in SPA mode.
101110
This will set up the development server proxy, and move files to Rails' `public` directory on build.
102-
Also, set up the Ruby rake tasks for running the development server and building assets.
111+
You will additionally need to set up the Ruby rake tasks for running the development server and building assets.
103112
The comments in the following files inside the current repository should guide you.
104113

105114
* `frontend-react-router/react-router.config.ts`
@@ -135,7 +144,8 @@ The example application simulates a slow server by adding a 2-second delay to al
135144
This gives us a more realistic experience similar to what users might see in the real world.
136145
Note that routes that don't need server connections will be instantaneous.
137146

138-
Any fast website will have a good UI/UX, even with ancient technology, and we need a slower one to demonstrate the benefits of using an SPA framework.
147+
A fast server and a good internet connection will mask any deficiencies of a poorly built SPA.
148+
To understand the benefits of using an SPA framework, I believe that you need to simulate real-world conditions with a slower network.
139149

140150
### Authentication
141151

@@ -156,21 +166,28 @@ The server sends the CSRF token in a cookie, which can be added to the header of
156166

157167
Note that because we serve the bootstrap HTML template from a controller action,
158168
the React application is guaranteed to have access to the CSRF token from the first load onwards.
159-
This is convenient when the first page contains a form.
169+
This is convenient when the first loaded page contains a form
170+
and needs a valid CSRF token from the onset to allow immediate submission.
160171

161172
### Cache Control
162173

163174
The bootstrap HTML template is served from a Rails controller action.
164175
As a result, the default Cache-Control header is set to the same value as other ERB files – `Cache-Control: max-age=0, private, must-revalidate`.
165176
On the other hand, assets are served from the `public` folder and have `Cache-Control: public, max-age=172800`.
166177

167-
This ensures that assets use the browser cache effectively, whereas the HTML template is always fresh and users get the most recent version of the app.
178+
This ensures that assets served from `public` use the browser cache effectively,
179+
whereas the HTML template served from the Rails controller is always fresh and always provides the most recent version of the app.
180+
181+
Note that assets served from the `public` folder will have hash-digests to bust caching.
182+
However, since the bootstrap HTML template is the first file to be loaded,
183+
it always has to have the same URL, and this precludes the use of hash-digests on this one.
184+
Therefore, we need separate caching configurations for each.
168185

169186
### Deployment
170187

171-
Yarn installation and the React Router build step are integrated into the `bin/rails assets:precompile` task.
188+
NPM installation and the React Router build step are integrated into the `bin/rails assets:precompile` task.
172189
Artifacts are stored inside the Ruby on Rails `public` folder.
173-
Note that we don't use Propshaft for the React Router build, and therefore artifacts are not saved into `app/assets/builds`.
174190

175-
Your Dockerfile and CI setup can stay the same with the exception that you will need to install Node.js.
176-
See the `Dockerfile` for an example.
191+
Since we tap into the asset pipeline commands,
192+
your Dockerfile and CI setup can stay the same with the exception that you may need to install Node.js.
193+
See the `Dockerfile` for an example of this.

app/controllers/concerns/csrf_cookie_enabled.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
# This allows you to use Ruby on Rails' robust CSRF protection
2-
# in your React Router app.
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.
34
#
45
# Refer to `frontend-react-router/app/utilities/csrf.ts` to see
56
# how the client side is implemented.

app/controllers/react_controller.rb

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
class ReactController < ApplicationController
22
layout false
33

4-
# This is the catch-all action for React Router. This will render the index.html file
4+
# This is the catch-all action for React Router.
5+
# This will render the bootstrap HTML file
56
# for all React Router requests.
67
#
7-
# Unlike typical webpack or esbuild setups, we do not render ERB files which include
8-
# `javascript_include_tag` (propshaft, sprockets) or `javascript_pack_tag` (webpack)
9-
# to load the React app.
10-
# Instead, take the index.html file that was generated by the React Router build
8+
# Unlike typical webpack or esbuild setups, we do not generate the bootstrap HTML file from ERB templates which include
9+
# `javascript_include_tag` (propshaft, sprockets) or `javascript_pack_tag` (webpack).
10+
# Instead, we take the index.html file that was generated by the React Router build
1111
# and rename it to "react-router-index.html".
12-
# This is served as a file from the controller in response to the bootstrap file request.
12+
# We then serve this from a controller action in response to the bootstrap HTML file request.
1313
#
1414
# Benefits:
1515
#
16-
# * We can use the index.html file that React Router generates as is.
17-
# This file contains optimizations that would be challenging to recreate on the Rails-side.
16+
# * We can use the index.html file that React Router generates using SSG,
17+
# from the `frontend-react-router/app/root.tsx` file.
18+
# This file contains optimizations that would be challenging to recreate inside Rails using ERB templates.
1819
# * By going through the Rails controller, we can adjust cache and cookie headers to
1920
# improve performance, reliability, and integration with Rails.
2021
def show

app/controllers/sessions_controller.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
# illustrate how easy it is to add authentication when done on Rails,
33
# thanks to the robust session framework that it provides.
44
#
5-
# Obviously lacking is a password hashing mechanism which Rails provides out of the box
6-
# as the `use_secure_password` method on ActiveRecord.
7-
# We use simple clear text passwords here.
5+
# To focus on the simplicity of authentication, I have opted to use passwords in clear text.
6+
# This is a bad idea for production, but it emphasizes that authentication is merely checking
7+
# the provided credentials and then handing them the keys (via the session).
8+
#
9+
# To use this in production, use the `has_secure_password` method that ActiveRecord provides
10+
# to check the validity of the email - password combination.
11+
#
812
class SessionsController < ApplicationController
913
# GET /users/new
1014
def new

app/helpers/application_helper.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
module ApplicationHelper
2+
# I often use the `safe_join` technique to write slightly complex
3+
# HTML from View Helpers.
4+
# This helper allows me to make the `safe_join` syntax a bit more pleasant.
25
def sj(*args)
36
safe_join([ args.flatten ])
47
end

app/helpers/buttons_helper.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
module ButtonsHelper
2+
# I like to use View helpers instead of partials to create simple components like buttons.
3+
# View helpers can become cumbersome for complex components but are a great fit
4+
# for Atom- or Molecule-level components, as defined in the Atomic Design methodology.
5+
#
6+
# Sending in the `user` attributes (with a default value of `current_user`) is my
7+
# preferred way of making the view helpers easy to test even without mocks.
8+
# I typically test authorization-dependent view logic like this.
9+
#
10+
# See `test/helpers/buttons_helper_test.rb` for a testing example.
211
def profile_button(user = current_user)
312
if user
413
form_with url: session_path(user.id), method: :delete do |f|

frontend-react-router/app/models/me.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,22 @@ export async function getMeUnlessLoaded() {
1919
}
2020
}
2121

22+
/*
23+
* Since we send the bootstrap HTML file from Rails controllers,
24+
* it is feasible to attach information related to the user and their preferences
25+
* inside a cookie that is immediately available to the browser when the
26+
* bootstrap HTML file is received.
27+
*
28+
* This would allow flicker-free rendering of dark/light themes and other user
29+
* related UI elements.
30+
*
31+
* However, sending personal data via cookies should be avoided since they are
32+
* easily viewable.
33+
* Instead, we use a `/me` endpoint and rely on the React Router loader mechanism
34+
* to enable flicker-free rendering.
35+
*
36+
*
37+
* */
2238
async function getMe() {
2339
try {
2440
let data

frontend-react-router/app/utilities/csrf.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
* and use its value to send in your fetch request headers.
2222
* 3. On receiving the request, Rails will validate that the
2323
* 'X-CSRF-Token' header value matches what was sent via the cookie.
24-
* 4. Note that you only need to do this for non-GET requests.
24+
* 4. Note that you only need to do this for non-GET requests assuming that
25+
* you are following best practices and not mutating data in GET requests.
2526
*
2627
* Your code should look like this.
2728
* const res = await fetch(`${baseApiPath()}/posts`, {

frontend-react-router/app/utilities/proxy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
* In this example application, the Ruby on Rails APIs endpoints are like
33
* `GET /posts` or `GET /users` and do not have any prefixes like `/api*`.
44
*
5-
* However, the port for the Vite development server (typically 5173) is different from
5+
* However, during development, the port for the Vite development server (typically 5173) is different from
66
* the Rails development server (typically 3000).
77
* This means that the Vite server needs to distinguish
8-
* between the requests it should send to the Rails server on port 3000 and the
8+
* between the requests it should send to the Rails server on port 3000 (JSON API requests) and the
99
* ones that it should handle itself on port 5173 (e.g., the Vite assets).
1010
*
1111
* Therefore, when running on the Vite development server, we prefix

frontend-react-router/react-router.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export default {
2424
await rename("build/client", "../public/react-router")
2525
await rm("build", { recursive: true, force: true })
2626
},
27-
// In the above, we decided to serve the React Router app from "/react-router/".
27+
// In the above, we have decided to serve the React Router app from "/react-router/".
2828
// The basename options tell React Router to manage this when generating
29-
// link tags, for example.
29+
// Link tags, for example.
3030
basename: "/react-router/"
3131
} satisfies Config;

0 commit comments

Comments
 (0)