react_on_rails_pro 16.2.0.beta.8
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 +7 -0
- data/.controlplane/Dockerfile +49 -0
- data/.controlplane/controlplane.yml +22 -0
- data/.controlplane/gvc.yml +25 -0
- data/.controlplane/postgres.yml +33 -0
- data/.controlplane/rails.yml +49 -0
- data/.controlplane/redis.yml +18 -0
- data/.gitignore +77 -0
- data/.prettierignore +12 -0
- data/.prettierrc +19 -0
- data/.rspec +2 -0
- data/.rubocop.yml +120 -0
- data/.scss-lint.yml +205 -0
- data/CHANGELOG.md +570 -0
- data/CI_SETUP.md +502 -0
- data/CONTRIBUTING.md +376 -0
- data/Dockerfile +63 -0
- data/Gemfile +8 -0
- data/Gemfile.development_dependencies +74 -0
- data/Gemfile.loader +32 -0
- data/Gemfile.lock +527 -0
- data/LICENSE +98 -0
- data/LICENSE_SETUP.md +272 -0
- data/README.md +577 -0
- data/Rakefile +13 -0
- data/app/controllers/react_on_rails_pro/rsc_payload_controller.rb +7 -0
- data/app/helpers/react_on_rails_pro_helper.rb +360 -0
- data/app/views/react_on_rails_pro/rsc_payload.html.erb +1 -0
- data/babel.config.js +4 -0
- data/docs/bundle-caching.md +205 -0
- data/docs/caching.md +234 -0
- data/docs/code-splitting-loadable-components.md +313 -0
- data/docs/code-splitting.md +349 -0
- data/docs/configuration.md +165 -0
- data/docs/contributors-info/onboarding-customers.md +6 -0
- data/docs/contributors-info/releasing.md +40 -0
- data/docs/contributors-info/style.md +33 -0
- data/docs/home-pro.md +146 -0
- data/docs/installation.md +203 -0
- data/docs/js-memory-leaks.md +22 -0
- data/docs/node-renderer/basics.md +92 -0
- data/docs/node-renderer/debugging.md +38 -0
- data/docs/node-renderer/error-reporting-and-tracing.md +160 -0
- data/docs/node-renderer/heroku.md +102 -0
- data/docs/node-renderer/js-configuration.md +91 -0
- data/docs/node-renderer/troubleshooting.md +5 -0
- data/docs/profiling-server-side-rendering-code.md +179 -0
- data/docs/react-server-components/add-streaming-and-interactivity.md +190 -0
- data/docs/react-server-components/create-without-ssr.md +448 -0
- data/docs/react-server-components/glossary.md +102 -0
- data/docs/react-server-components/how-react-server-components-work.md +243 -0
- data/docs/react-server-components/inside-client-components.md +332 -0
- data/docs/react-server-components/purpose-and-benefits.md +243 -0
- data/docs/react-server-components/rendering-flow.md +86 -0
- data/docs/react-server-components/selective-hydration-in-streamed-components.md +75 -0
- data/docs/react-server-components/server-side-rendering.md +72 -0
- data/docs/react-server-components/tutorial.md +19 -0
- data/docs/release-notes/4.0.md +94 -0
- data/docs/release-notes/v4-react-server-components.md +66 -0
- data/docs/ruby-api.md +11 -0
- data/docs/streaming-server-rendering.md +210 -0
- data/docs/troubleshooting.md +24 -0
- data/docs/updating.md +219 -0
- data/eslint.config.mjs +220 -0
- data/lib/react_on_rails_pro/assets_precompile.rb +230 -0
- data/lib/react_on_rails_pro/cache.rb +88 -0
- data/lib/react_on_rails_pro/concerns/rsc_payload_renderer.rb +38 -0
- data/lib/react_on_rails_pro/concerns/stream.rb +103 -0
- data/lib/react_on_rails_pro/configuration.rb +228 -0
- data/lib/react_on_rails_pro/constants.rb +8 -0
- data/lib/react_on_rails_pro/engine.rb +24 -0
- data/lib/react_on_rails_pro/error.rb +14 -0
- data/lib/react_on_rails_pro/license_public_key.rb +30 -0
- data/lib/react_on_rails_pro/license_validator.rb +188 -0
- data/lib/react_on_rails_pro/prepare_node_renderer_bundles.rb +40 -0
- data/lib/react_on_rails_pro/rendering_error.rb +5 -0
- data/lib/react_on_rails_pro/request.rb +318 -0
- data/lib/react_on_rails_pro/routes.rb +13 -0
- data/lib/react_on_rails_pro/server_rendering_js_code.rb +102 -0
- data/lib/react_on_rails_pro/server_rendering_pool/node_rendering_pool.rb +133 -0
- data/lib/react_on_rails_pro/server_rendering_pool/pro_rendering.rb +117 -0
- data/lib/react_on_rails_pro/stream_cache.rb +61 -0
- data/lib/react_on_rails_pro/stream_request.rb +170 -0
- data/lib/react_on_rails_pro/utils.rb +222 -0
- data/lib/react_on_rails_pro/v8_log_processor.rb +50 -0
- data/lib/react_on_rails_pro/version.rb +6 -0
- data/lib/react_on_rails_pro.rb +23 -0
- data/package-scripts.yml +109 -0
- data/package.json +159 -0
- data/rakelib/dummy_apps.rake +22 -0
- data/rakelib/lint.rake +32 -0
- data/rakelib/public_key_management.rake +155 -0
- data/rakelib/rbs.rake +47 -0
- data/rakelib/run_rspec.rake +81 -0
- data/rakelib/task_helpers.rb +45 -0
- data/rakelib/yard.rake +20 -0
- data/react_on_rails_pro.gemspec +47 -0
- data/readme-gen-docs.md +1 -0
- data/script/bootstrap +33 -0
- data/script/preinstall.js +31 -0
- data/script/setup +23 -0
- data/script/test +38 -0
- data/sig/react_on_rails_pro/cache.rbs +13 -0
- data/sig/react_on_rails_pro/configuration.rbs +100 -0
- data/sig/react_on_rails_pro/error.rbs +4 -0
- data/sig/react_on_rails_pro/utils.rbs +7 -0
- data/sig/react_on_rails_pro.rbs +5 -0
- data/yarn.lock +7599 -0
- metadata +319 -0
data/docs/caching.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Caching
|
|
2
|
+
|
|
3
|
+
Caching at the React on Rails level can greatly speed up your app and reduce the load on your servers, allowing more requests for a given level of hardware.
|
|
4
|
+
|
|
5
|
+
Consult the [Rails Guide on Caching](http://guides.rubyonrails.org/caching_with_rails.html#cache-stores) for details on:
|
|
6
|
+
|
|
7
|
+
* [Cache Stores and Configuration](http://guides.rubyonrails.org/caching_with_rails.html#cache-stores)
|
|
8
|
+
* [Determination of Cache Keys](http://guides.rubyonrails.org/caching_with_rails.html#cache-keys)
|
|
9
|
+
* [Caching in Development](http://guides.rubyonrails.org/caching_with_rails.html#caching-in-development): **To toggle caching in development**, run `rails dev:cache`.
|
|
10
|
+
|
|
11
|
+
See the [bottom note on confirming and debugging cache keys](#confirming-and-debugging-cache-keys).
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
React on Rails Pro has caching at 2 levels:
|
|
15
|
+
|
|
16
|
+
1. "Fragment caching" view helpers, `cached_react_component` and `cached_react_component_hash`.
|
|
17
|
+
2. Caching of requests for server rendering.
|
|
18
|
+
|
|
19
|
+
### Tracing
|
|
20
|
+
If tracing is turned on in your config/initializers/react_on_rails_pro.rb, you'll see timing log messages that begin with `[ReactOnRailsPro:1234]: exec_server_render_js` where 1234 is the process id and `exec_server_render_js` could be a different method being traced.
|
|
21
|
+
|
|
22
|
+
* **exec_server_render_js**: Timing of server rendering, which may have the prerender_caching turned on.
|
|
23
|
+
* **cached_react_component** and **cached_react_component_hash**: Timing of the cached view helper which maybe calling server rendering.
|
|
24
|
+
|
|
25
|
+
Here's a sample. Note the second request
|
|
26
|
+
```
|
|
27
|
+
Started GET "/server_side_redux_app_cached" for ::1 at 2018-05-24 22:40:13 -1000
|
|
28
|
+
[ReactOnRailsPro:63422] exec_server_render_js: ReduxApp, 230.7ms
|
|
29
|
+
[ReactOnRailsPro:63422] cached_react_component: ReduxApp, 2483.8ms
|
|
30
|
+
Completed 200 OK in 3613ms (Views: 3407.5ms | ActiveRecord: 0.0ms)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
Started GET "/server_side_redux_app_cached" for ::1 at 2018-05-24 22:40:36 -1000
|
|
34
|
+
Processing by PagesController#server_side_redux_app_cached as HTML
|
|
35
|
+
Rendering pages/server_side_redux_app_cached.html.erb within layouts/application
|
|
36
|
+
[ReactOnRailsPro:63422] cached_react_component: ReduxApp, 1.1ms
|
|
37
|
+
Completed 200 OK in 19ms (Views: 16.4ms | ActiveRecord: 0.0ms)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Prerender (Server Side Rendering) Caching
|
|
41
|
+
|
|
42
|
+
### Why?
|
|
43
|
+
1. Server side rendering is typically done like a stateless functional component, meaning that the result should be idempotent from based on props passed in.
|
|
44
|
+
1. It's much easier than configuring fragment caching. So long as you have some space in your Rails cache, "it should just work."
|
|
45
|
+
|
|
46
|
+
### Why not?
|
|
47
|
+
If you're using regular caching for most componentas (cached_react_component_hash), and you don't want to use caching for other components, then having prerender caching still results in caching for all your rendering calls, increasing the liklihood of premature cache ejection.
|
|
48
|
+
|
|
49
|
+
In the future, React on Rails will allow stateful server rendering. Thus, your server side JavaScript depend on externalities, such as AJAX calls for
|
|
50
|
+
GraphQL. In that case, you will set this caching to false.
|
|
51
|
+
|
|
52
|
+
### When?
|
|
53
|
+
The largest percentage gains will come from saving the time of server rendering. However, even when not doing server rendering, caching can be effective as the caching will prevent the calculation of the props and the conversion to a string of the prop values.
|
|
54
|
+
|
|
55
|
+
### How?
|
|
56
|
+
|
|
57
|
+
To enable caching server rendering requests to the JavaScript calculation engine (ExecJS or Node Renderer), set this config
|
|
58
|
+
value in `config/initializers/react_on_rails_pro.rb` to true (default is false):
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
config.prerender_caching = true
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Server rendering JavaScript evaluation requests are cached by a cache key that considers the following:
|
|
65
|
+
|
|
66
|
+
1. Hash of the server bundle.
|
|
67
|
+
2. The JavaScript code to evaluate.
|
|
68
|
+
|
|
69
|
+
### Diagnostics
|
|
70
|
+
if you're using `react_component_hash`, you'll get 2 extra keys returned:
|
|
71
|
+
|
|
72
|
+
1. RORP_CACHE_KEY: the prerender cache key
|
|
73
|
+
2. RORP_CACHE_HIT: whether or not there was a cache hit.
|
|
74
|
+
|
|
75
|
+
It can be useful to log these to the rendered HTML page to debug caching issues.
|
|
76
|
+
|
|
77
|
+
## React on Rails Fragment Caching
|
|
78
|
+
|
|
79
|
+
This is very similar to Rails fragment caching.
|
|
80
|
+
|
|
81
|
+
From the [Rails docs](http://guides.rubyonrails.org/caching_with_rails.html#fragment-caching):
|
|
82
|
+
|
|
83
|
+
> Fragment Caching allows a fragment of view logic to be wrapped in a cache block and served out of the cache store when the next request comes in.
|
|
84
|
+
|
|
85
|
+
It is similar in that the most important parts that you need to consider are:
|
|
86
|
+
|
|
87
|
+
1. Determining the optimal cache keys that minimize any cost such as database queries.
|
|
88
|
+
2. Clearing the Rails.cache on some deployments.
|
|
89
|
+
|
|
90
|
+
If you're already familiar with Rails fragment caching, the React on Rails implementation should feel familiar.
|
|
91
|
+
|
|
92
|
+
The reasons "why" and "why not" are the same as for basic Rails fragment caching:
|
|
93
|
+
|
|
94
|
+
### Why Use Fragment Caching?
|
|
95
|
+
1. Next to caching at the controller or HTTP level, this is the fastest type of caching.
|
|
96
|
+
2. The additional complexity to add this with React on Rails Pro is minimal.
|
|
97
|
+
3. The performance gains can be huge.
|
|
98
|
+
4. The load on your Rails server can be far lessened.
|
|
99
|
+
|
|
100
|
+
### Why Not Use Fragment Caching?
|
|
101
|
+
1. It's tricky to get all the right cache keys. You have to consider any values that can change and cause the rendering to change. See the [Rails docs for cache keys](http://guides.rubyonrails.org/caching_with_rails.html#cache-keys)
|
|
102
|
+
2. Testing is a bit tricky or just not done for fragment caching.
|
|
103
|
+
3. Some deployments require you to clear caches.
|
|
104
|
+
|
|
105
|
+
### Considerations for Determining Your Cache Key
|
|
106
|
+
1. Consult the [Rails docs for cache keys](http://guides.rubyonrails.org/caching_with_rails.html#cache-keys) for help with cache key definitions.
|
|
107
|
+
2. If your React code depends on any values from the [Rails Context](https://github.com/shakacode/react_on_rails/blob/master/docs/basics/generator-functions-and-railscontext.md#rails-context), such as the `locale` or the URL `location`, then be sure to include such values in your cache key. In other words, if you are using some JavaScript such as `react-router` that depends on your URL, or on a call to `toLocalString(locale)`, then be sure to include such values in your cache key. To find the values that React on Rails uses, use some code like this:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
the_rails_context = rails_context
|
|
111
|
+
i18nLocale = the_rails_context[:i18nLocale]
|
|
112
|
+
location = the_rails_context[:location]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
If you are calling `rails_context` from your controller method, then prefix it like this: `helpers.rails_context` so long as you have react_on_rails > 11.2.2. If less than that, call `helpers.send(:rails_context, server_side: true)`
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
If performance is particulary sensitive, consult the view helper definition for `rails_context`. For example, you can save the cost of calculating the rails_context by directly getting a value:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
i18nLocale = I18n.locale
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### How: API
|
|
125
|
+
Here is the doc for helpers `cached_react_component` and `cached_react_component_hash`. Consult the [docs in React on Rails](https://www.shakacode.com/react-on-rails/docs/api/view-helpers-api/) for the non-cached analogies `react_component` and `react_component_hash`. These docs only show the differences.
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
# Provide caching support for react_component in a manner akin to Rails fragment caching.
|
|
129
|
+
# All the same options as react_component apply with the following difference:
|
|
130
|
+
#
|
|
131
|
+
# 1. You must pass the props as a block. This is so that the evaluation of the props is not done
|
|
132
|
+
# if the cache can be used.
|
|
133
|
+
# 2. Provide the cache_key option
|
|
134
|
+
# cache_key: String or Array (or Proc returning a String or Array) containing your cache keys.
|
|
135
|
+
# If prerender is set to true, the server bundle digest will be included in the cache key.
|
|
136
|
+
# The cache_key value is the same as used for conventional Rails fragment caching.
|
|
137
|
+
# 3. Optionally provide the `:cache_options` key with a value of a hash including as
|
|
138
|
+
# :compress, :expires_in, :race_condition_ttl as documented in the Rails Guides
|
|
139
|
+
# 4. Provide boolean values for `:if` or `:unless` to conditionally use caching.
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
You can find the `:cache_options` documented in the [Rails docs for ActiveSupport cache store](https://guides.rubyonrails.org/caching_with_rails.html#activesupport-cache-store).
|
|
143
|
+
|
|
144
|
+
#### API Usage examples
|
|
145
|
+
|
|
146
|
+
The fragment caching for `react_component`:
|
|
147
|
+
```ruby
|
|
148
|
+
<%= cached_react_component("App", cache_key: [@user, @post], prerender: true) do
|
|
149
|
+
some_slow_method_that_returns_props
|
|
150
|
+
end %>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Suppose you only want to cache when `current_user.nil?`. Use the `:if` option (`unless:` is analogous):
|
|
154
|
+
```ruby
|
|
155
|
+
<%= cached_react_component("App", cache_key: [@user, @post], prerender: true, if: current_user.nil?) do
|
|
156
|
+
some_slow_method_that_returns_props
|
|
157
|
+
end %>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
And a fragment caching version for the `react_component_hash`:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
<% result = cached_react_component_hash("ReactHelmetApp", cache_key: [@user, @post],
|
|
164
|
+
id: "react-helmet-0") do
|
|
165
|
+
some_slow_method_that_returns_props
|
|
166
|
+
end %>
|
|
167
|
+
|
|
168
|
+
<% content_for :title do %>
|
|
169
|
+
<%= react_helmet_app['title'] %>
|
|
170
|
+
<% end %>
|
|
171
|
+
|
|
172
|
+
<%= react_helmet_app["componentHtml"] %>
|
|
173
|
+
|
|
174
|
+
<% printable_cache_key = ReactOnRailsPro::Utils.printable_cache_key(result[:RORP_CACHE_KEY]) %>
|
|
175
|
+
<!-- <%= "CACHE_HIT: #{result[:RORP_CACHE_HIT]}, RORP_CACHE_KEY: #{printable_cache_key}" %> -->
|
|
176
|
+
````
|
|
177
|
+
Note in the above example, React on Rails Pro returns both the raw cache key and whether or not there was a cache hit.
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
### Your JavaScript Bundles and Cache Keys
|
|
181
|
+
|
|
182
|
+
When doing fragment caching of server rendering with React on Rails Pro, the cache key must reflect
|
|
183
|
+
your React. This is analogous to how Rails puts an MD5 hash of your views in
|
|
184
|
+
the cache key so that if the views change, then your cache is busted. In the case
|
|
185
|
+
of React code, if your React code changes, then your bundle name will
|
|
186
|
+
change if you are doing the inclusion of a hash in the name. However, if you are
|
|
187
|
+
using a separate webpack configuration to generate the server bundle file,
|
|
188
|
+
then you **must not** include the hash in the output filename or else you will
|
|
189
|
+
have a race condition overwriting your `manifest.json`. Regardless of which
|
|
190
|
+
case you have, React on Rails handles it.
|
|
191
|
+
|
|
192
|
+
# Confirming and Debugging Cache Keys
|
|
193
|
+
|
|
194
|
+
Cache key composition can be confirmed in development mode with the following steps. THe goal is to confirm that some change that should trigger new cached data actually triggers a new cache key. For example, when the server bundle changes, does that trigger a new cache key for any server rendering?
|
|
195
|
+
|
|
196
|
+
1. Run `Rails.cache.clear` to clear the cache.
|
|
197
|
+
1. Run `rails dev:cache` to toggle caching in development mode.
|
|
198
|
+
|
|
199
|
+
You will see a message like:
|
|
200
|
+
> Development mode is now being cached.
|
|
201
|
+
|
|
202
|
+
You might need to check your `config/development.rb`contains the following:
|
|
203
|
+
```ruby
|
|
204
|
+
# Enable/disable caching. By default caching is disabled.
|
|
205
|
+
if Rails.root.join("tmp/caching-dev.txt").exist?
|
|
206
|
+
config.action_controller.perform_caching = true
|
|
207
|
+
|
|
208
|
+
config.cache_store = :memory_store
|
|
209
|
+
config.public_file_server.headers = {
|
|
210
|
+
"Cache-Control" => "public, max-age=172800"
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# For Rails >= 5.1 determines whether to log fragment cache reads and writes in verbose format as follows:
|
|
214
|
+
config.action_controller.enable_fragment_cache_logging
|
|
215
|
+
else
|
|
216
|
+
config.action_controller.perform_caching = false
|
|
217
|
+
|
|
218
|
+
config.cache_store = :null_store
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
3. Start your server in development mode. You should see cache entries in the console log. Fetch the page that uses the cache. Make a note of the cache key used for the cached component.
|
|
223
|
+
|
|
224
|
+
4. Suppose you want to confirm that updated JavaScript causes a cache key change. Make any change to the JavaScript that's server rendered or change the version of any package in the bundle.
|
|
225
|
+
|
|
226
|
+
5. Check the cache entry again. You should have noticed that it changed.
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
To avoid seeing the cache calls to the prerender_caching, you can temporarily set:
|
|
230
|
+
```
|
|
231
|
+
config.prerender_caching = false
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# Server-side rendering with code-splitting using Loadable/Components
|
|
2
|
+
by ShakaCode
|
|
3
|
+
|
|
4
|
+
*Last updated September 19, 2022*
|
|
5
|
+
|
|
6
|
+
## Introduction
|
|
7
|
+
The [React library recommends](https://loadable-components.com/docs/getting-started/) the use of React.lazy for code splitting with dynamic imports except
|
|
8
|
+
when using server-side rendering. In that case, as of February 2020, they recommend [Loadable Components](https://loadable-components.com)
|
|
9
|
+
for server-side rendering with dynamic imports.
|
|
10
|
+
|
|
11
|
+
Note, in 2019 and prior, the code-splitting feature was implemented using `react-loadable`. The React
|
|
12
|
+
team no longer recommends that library. The new way is far preferable.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
yarn add @loadable/babel-plugin @loadable/component @loadable/server @loadable/webpack-plugin
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Summary
|
|
21
|
+
- [`@loadable/babel-plugin`](https://loadable-components.com/docs/getting-started/) - The plugin transforms your code to be ready for Server Side Rendering.
|
|
22
|
+
- `@loadable/component` - Main library for creating loadable components.
|
|
23
|
+
- `@loadable/server` - Has functions for collecting chunks and provide style, script, link tags for the server.
|
|
24
|
+
- `@loadable/webpack-plugin` - The plugin to create a stats file with all chunks, assets information.
|
|
25
|
+
|
|
26
|
+
## Configuration
|
|
27
|
+
|
|
28
|
+
These instructions mainly repeat the [server-side rendering steps from the official documentation for Loadable Components](https://loadable-components.com/docs/server-side-rendering/), but with some additions specifically to react_on_rails_pro.
|
|
29
|
+
|
|
30
|
+
### Webpack
|
|
31
|
+
|
|
32
|
+
#### Server Bundle Configuration
|
|
33
|
+
|
|
34
|
+
See example of server configuration differences in the loadable-components [example of the webpack.config.babel.js
|
|
35
|
+
for server-side rendering](https://github.com/gregberge/loadable-components/blob/master/examples/server-side-rendering/webpack.config.babel.js)
|
|
36
|
+
|
|
37
|
+
You need to configure 3 things:
|
|
38
|
+
1. `target`
|
|
39
|
+
a. client-side: `web`
|
|
40
|
+
b. server-side: `node`
|
|
41
|
+
2. `output.libraryTarget`
|
|
42
|
+
a. client-side: `undefined`
|
|
43
|
+
b. server-side: `commonjs2`
|
|
44
|
+
3. babel-loader options.caller = 'node' or 'web'
|
|
45
|
+
3. `plugins`
|
|
46
|
+
a. server-side: `new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })`
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
{
|
|
50
|
+
target: 'node',
|
|
51
|
+
plugins: [
|
|
52
|
+
...,
|
|
53
|
+
new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Explanation:
|
|
59
|
+
|
|
60
|
+
- `target: 'node'` is required to be able to run the server bundle with the dynamic import logic on nodejs.
|
|
61
|
+
If that is not done, webpack will add and invoke browser-specific functions to fetch the chunks into the bundle, which throws an error on server-rendering.
|
|
62
|
+
|
|
63
|
+
- `new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })`
|
|
64
|
+
The react_on_rails_pro node-renderer expects only one single server-bundle. In other words, we cannot and do not want to split the server bundle.
|
|
65
|
+
|
|
66
|
+
#### Client config
|
|
67
|
+
|
|
68
|
+
For the client config we only need to add the plugin:
|
|
69
|
+
```js
|
|
70
|
+
{
|
|
71
|
+
plugins: [
|
|
72
|
+
...,
|
|
73
|
+
new LoadablePlugin({ filename: 'loadable-stats.json' })
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
This plugin collects all the information about entrypoints, chunks, and files, that have these chunks and creates a stats file during client bundle build.
|
|
78
|
+
This stats file is used later to map rendered components to file assets. While you can use any filename, our documentation will use the default name.
|
|
79
|
+
|
|
80
|
+
### Babel
|
|
81
|
+
|
|
82
|
+
Per [the docs](https://loadable-components.com/docs/babel-plugin/#transformation):
|
|
83
|
+
> The plugin transforms your code to be ready for Server Side Rendering
|
|
84
|
+
|
|
85
|
+
Add this to `babel.config.js`:
|
|
86
|
+
```js
|
|
87
|
+
{
|
|
88
|
+
"plugins": ["@loadable/babel-plugin"]
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
https://loadable-components.com/docs/babel-plugin/
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
### Convert components into loadable components
|
|
95
|
+
|
|
96
|
+
Instead of importing the component directly, use a dynamic import:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
import load from '@loadable/component'
|
|
100
|
+
const MyComponent = load(() => import('./MyComponent'))
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Resolving issue with ChunkLoadError
|
|
104
|
+
|
|
105
|
+
Sometimes chunks might not be loaded (network issues or others). You may get errors like this:
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
ChunkLoadError: Loading chunk 6 failed.
|
|
109
|
+
(error: https://www.cityfalcon.com/packs/js/News-58215546ef43bc340bac.chunk.js)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This can be fixed by using a retry loop:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
// https://gist.github.com/briancavalier/842626
|
|
116
|
+
const consoleDebug = (fn) => {
|
|
117
|
+
if (typeof console.debug !== 'undefined') {
|
|
118
|
+
console.debug(fn());
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const retry = (fn, retryMessage = '', retriesLeft = 3, interval = 500) => new Promise((resolve, reject) => {
|
|
122
|
+
fn()
|
|
123
|
+
.then(resolve)
|
|
124
|
+
.catch(() => {
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
if (retriesLeft === 1) {
|
|
127
|
+
console.warn(`Maximum retries exceeded, retryMessage: ${retryMessage}. Reloading page...`);
|
|
128
|
+
window.location.reload();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Passing on "reject" is the important part
|
|
132
|
+
consoleDebug(() => `Trying request, retryMessage: ${retryMessage}, retriesLeft: ${retriesLeft - 1}`);
|
|
133
|
+
retry(fn, retryMessage, retriesLeft - 1, interval).then(resolve, reject);
|
|
134
|
+
}, interval);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
export default retry;
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Then use it in your component:
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
import retry from 'utils/retry';
|
|
144
|
+
const HomePage = loadable(() => retry(() => import('./HomePage')));
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Please note that babel must not be configured to strip comments, since the chunk name is defined in a comment.**
|
|
148
|
+
|
|
149
|
+
### Server and client entries
|
|
150
|
+
|
|
151
|
+
#### Client
|
|
152
|
+
|
|
153
|
+
In the client bundle, we need to wrap the `hydrateRoot` call into a `loadableReady` function.
|
|
154
|
+
So, hydration will be fired only after all necessary chunks preloads. In this example below,
|
|
155
|
+
`ClientApp` is registering as `App`.
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
import React from 'react';
|
|
159
|
+
import ReactOnRails from 'react-on-rails';
|
|
160
|
+
import { hydrateRoot } from 'react-dom/client'
|
|
161
|
+
import { loadableReady } from '@loadable/component'
|
|
162
|
+
import App from './App';
|
|
163
|
+
|
|
164
|
+
const ClientApp = (props, railsContext, domId) => {
|
|
165
|
+
loadableReady(() => {
|
|
166
|
+
const root = document.getElementById(domId)
|
|
167
|
+
hydrateRoot(root, <App {...props} />);
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
ReactOnRails.register({
|
|
172
|
+
App: ClientApp,
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### Server
|
|
177
|
+
|
|
178
|
+
The purpose of the server function is to collect all rendered chunks and pass them as script, link,
|
|
179
|
+
style tags to the Rails view. In this example below, `ServerApp` is registering as `App`.
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
import React from 'react';
|
|
183
|
+
import ReactOnRails from 'react-on-rails';
|
|
184
|
+
import { ChunkExtractor } from '@loadable/server'
|
|
185
|
+
import App from './App'
|
|
186
|
+
import path from 'path'
|
|
187
|
+
|
|
188
|
+
const ServerApp = (props, railsContext) => {
|
|
189
|
+
// This loadable-stats file was generated by `LoadablePlugin` in client webpack config.
|
|
190
|
+
// You must configure the path to resolve per your setup. If you are copying the file to
|
|
191
|
+
// a remote server, the file should be a sibling of this file.
|
|
192
|
+
// __dirname is going to be the directory where the server-bundle.js exists
|
|
193
|
+
// Note, React on Rails Pro automatically copies the loadable-stats.json to the same place as the
|
|
194
|
+
// server-bundle.js. Thus, the __dirname of this code is where we can find loadable-stats.json.
|
|
195
|
+
// Be sure to configure ReactOnRailsPro.config.assets_top_copy to this file.
|
|
196
|
+
const statsFile = path.resolve(__dirname, 'loadable-stats.json');
|
|
197
|
+
|
|
198
|
+
// This object is used to search filenames by corresponding chunk names.
|
|
199
|
+
// See https://loadable-components.com/docs/api-loadable-server/#chunkextractor
|
|
200
|
+
// for the entryPoints, pass an array of all your entryPoints using dynamic imports
|
|
201
|
+
const extractor = new ChunkExtractor({ statsFile, entrypoints: ['client-bundle'] })
|
|
202
|
+
|
|
203
|
+
// It creates the wrapper `ChunkExtractorManager` around `App` to collect chunk names of rendered components.
|
|
204
|
+
const jsx = extractor.collectChunks(<App {...props} railsContext={railsContext} />)
|
|
205
|
+
|
|
206
|
+
const componentHtml = renderToString(jsx);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
renderedHtml: {
|
|
210
|
+
componentHtml,
|
|
211
|
+
// Returns all the files with rendered chunks for furture insert into rails view.
|
|
212
|
+
linkTags: extractor.getLinkTags(),
|
|
213
|
+
styleTags: extractor.getStyleTags(),
|
|
214
|
+
scriptTags: extractor.getScriptTags()
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
ReactOnRails.register({
|
|
220
|
+
App: ServerApp,
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Configure react_on_rails_pro
|
|
225
|
+
|
|
226
|
+
### React on Rails Pro
|
|
227
|
+
You must set `config.assets_top_copy` so that the node-renderer will have access to the loadable-stats.json.
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
config.assets_to_copy = Rails.root.join("public", "webpack", Rails.env, "loadable-stats.json")
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Your server rendering code, per the above, will find this file like this:
|
|
234
|
+
|
|
235
|
+
```js
|
|
236
|
+
const statsFile = path.resolve(__dirname, 'loadable-stats.json');
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Note, if `__dirname` is not working in your webpack build, that's because you didn't set `node: false`
|
|
240
|
+
in your webpack configuration. That turns off the polyfills for things like `__dirname`.
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
### Node Renderer
|
|
244
|
+
In your `node-renderer.js` file which runs node renderer, you need to specify `supportModules` options as follows:
|
|
245
|
+
```js
|
|
246
|
+
const path = require('path');
|
|
247
|
+
const env = process.env;
|
|
248
|
+
const { reactOnRailsProNodeRenderer } = require('react-on-rails-pro-node-renderer');
|
|
249
|
+
|
|
250
|
+
const config = {
|
|
251
|
+
...
|
|
252
|
+
supportModules: env.RENDERER_SUPPORT_MODULES || null,
|
|
253
|
+
};
|
|
254
|
+
...
|
|
255
|
+
|
|
256
|
+
reactOnRailsProNodeRenderer(config);
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Rails View
|
|
260
|
+
|
|
261
|
+
```erb
|
|
262
|
+
<% res = react_component_hash("App", props: {}, prerender: true) %>
|
|
263
|
+
<%= content_for :link_tags, res['linkTags'] %>
|
|
264
|
+
<%= content_for :style_tags, res['styleTags'] %>
|
|
265
|
+
|
|
266
|
+
<%= res['componentHtml'].html_safe %>
|
|
267
|
+
|
|
268
|
+
<%= content_for :script_tags, res['scriptTags'] %>
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Making HMR Work
|
|
272
|
+
To make HMR work, it's best to disable loadable-components when using the Dev Server.
|
|
273
|
+
Note: you will need access to our **private** React on Rails Pro repository to open the following links.
|
|
274
|
+
|
|
275
|
+
Take a look at the code searches for ['imports-loadable'](https://github.com/shakacode/react_on_rails_pro/search?q=imports-loadable&type=code) and ['imports-hmr'](https://github.com/shakacode/react_on_rails_pro/search?q=imports-hmr&type=code)
|
|
276
|
+
|
|
277
|
+
The general concept is that we have a non-loadable, HMR-ready, file that substitutes for the loadable-enabled one, with the suffixes `imports-hmr.js` instead of `imports-loadable.js`
|
|
278
|
+
|
|
279
|
+
### Webpack configuration
|
|
280
|
+
Use the [NormalModuleReplacement plugin](https://webpack.js.org/plugins/normal-module-replacement-plugin/):
|
|
281
|
+
|
|
282
|
+
[code](https://github.com/shakacode/react_on_rails_pro/blob/a361f4e163b9170f180ae07ee312fb9b4c719fc3/spec/dummy/config/webpack/environment.js#L81-L91)
|
|
283
|
+
```js
|
|
284
|
+
if (isWebpackDevServer) {
|
|
285
|
+
environment.plugins.append(
|
|
286
|
+
'NormalModuleReplacement',
|
|
287
|
+
new webpack.NormalModuleReplacementPlugin(/(.*)\.imports-loadable(\.jsx)?/, (resource) => {
|
|
288
|
+
// eslint-disable-next-line no-param-reassign
|
|
289
|
+
resource.request = resource.request.replace(/imports-loadable/, 'imports-hmr');
|
|
290
|
+
return resource.request;
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
And compare:
|
|
297
|
+
|
|
298
|
+
### Routes file
|
|
299
|
+
|
|
300
|
+
Note: you will need access to our **private** React on Rails Pro repository to open the following links.
|
|
301
|
+
|
|
302
|
+
- [spec/dummy/client/app/components/Loadable/routes/Routes.imports-hmr.jsx](https://github.com/shakacode/react_on_rails_pro/blob/master/spec/dummy/client/app/components/Loadable/routes/Routes.imports-hmr.jsx)
|
|
303
|
+
- [spec/dummy/client/app/components/Loadable/routes/Routes.imports-loadable.jsx](https://github.com/shakacode/react_on_rails_pro/blob/master/spec/dummy/client/app/components/Loadable/routes/Routes.imports-loadable.jsx)
|
|
304
|
+
|
|
305
|
+
### Client-Side Startup
|
|
306
|
+
|
|
307
|
+
- [spec/dummy/client/app/loadable/loadable-client.imports-hmr.js](https://github.com/shakacode/react_on_rails_pro/blob/master/spec/dummy/client/app/loadable/loadable-client.imports-hmr.js)
|
|
308
|
+
- [spec/dummy/client/app/loadable/loadable-client.imports-loadable.js](https://github.com/shakacode/react_on_rails_pro/blob/master/spec/dummy/client/app/loadable/loadable-client.imports-loadable.js)
|
|
309
|
+
|
|
310
|
+
### Server-Side Startup
|
|
311
|
+
|
|
312
|
+
- [spec/dummy/client/app/loadable/loadable-server.imports-hmr.jsx](https://github.com/shakacode/react_on_rails_pro/blob/master/spec/dummy/client/app/loadable/loadable-server.imports-hmr.jsx)
|
|
313
|
+
- [spec/dummy/client/app/loadable/loadable-server.imports-loadable.jsx](https://github.com/shakacode/react_on_rails_pro/blob/master/spec/dummy/client/app/loadable/loadable-server.imports-loadable.jsx)
|