react_on_rails_pro 16.2.0.beta.12 → 16.2.0.beta.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/.prettierignore +0 -1
- data/CHANGELOG.md +12 -2
- data/Gemfile.development_dependencies +1 -1
- data/Gemfile.lock +117 -106
- data/app/helpers/react_on_rails_pro_helper.rb +188 -32
- data/docs/code-splitting-loadable-components.md +69 -53
- data/eslint.config.mjs +26 -7
- data/lib/react_on_rails_pro/async_value.rb +35 -0
- data/lib/react_on_rails_pro/concerns/async_rendering.rb +68 -0
- data/lib/react_on_rails_pro/concerns/stream.rb +62 -49
- data/lib/react_on_rails_pro/configuration.rb +20 -10
- data/lib/react_on_rails_pro/immediate_async_value.rb +27 -0
- data/lib/react_on_rails_pro/version.rb +1 -1
- data/lib/react_on_rails_pro.rb +3 -0
- data/package-scripts.yml +20 -37
- data/package.json +9 -154
- data/rakelib/run_rspec.rake +1 -1
- data/react_on_rails_pro.gemspec +9 -7
- data/sig/react_on_rails_pro/async_value.rbs +15 -0
- data/sig/react_on_rails_pro/concerns/async_rendering.rbs +15 -0
- data/sig/react_on_rails_pro/immediate_async_value.rbs +15 -0
- metadata +24 -6
- data/script/preinstall.js +0 -31
- data/yarn.lock +0 -7599
|
@@ -128,7 +128,11 @@ module ReactOnRailsProHelper
|
|
|
128
128
|
# Because setting prerender to false is equivalent to calling react_component with prerender: false
|
|
129
129
|
options[:prerender] = true
|
|
130
130
|
options = options.merge(immediate_hydration: true) unless options.key?(:immediate_hydration)
|
|
131
|
-
|
|
131
|
+
|
|
132
|
+
# Extract streaming-specific callback
|
|
133
|
+
on_complete = options.delete(:on_complete)
|
|
134
|
+
|
|
135
|
+
consumer_stream_async(on_complete: on_complete) do
|
|
132
136
|
internal_stream_react_component(component_name, options)
|
|
133
137
|
end
|
|
134
138
|
end
|
|
@@ -185,7 +189,11 @@ module ReactOnRailsProHelper
|
|
|
185
189
|
# rsc_payload_react_component doesn't have the prerender option
|
|
186
190
|
# Because setting prerender to false will not do anything
|
|
187
191
|
options[:prerender] = true
|
|
188
|
-
|
|
192
|
+
|
|
193
|
+
# Extract streaming-specific callback
|
|
194
|
+
on_complete = options.delete(:on_complete)
|
|
195
|
+
|
|
196
|
+
consumer_stream_async(on_complete: on_complete) do
|
|
189
197
|
internal_rsc_payload_react_component(component_name, options)
|
|
190
198
|
end
|
|
191
199
|
end
|
|
@@ -209,6 +217,63 @@ module ReactOnRailsProHelper
|
|
|
209
217
|
end
|
|
210
218
|
end
|
|
211
219
|
|
|
220
|
+
# Renders a React component asynchronously, returning an AsyncValue immediately.
|
|
221
|
+
# Multiple async_react_component calls will execute their HTTP rendering requests
|
|
222
|
+
# concurrently instead of sequentially.
|
|
223
|
+
#
|
|
224
|
+
# Requires the controller to include ReactOnRailsPro::AsyncRendering and call
|
|
225
|
+
# enable_async_react_rendering.
|
|
226
|
+
#
|
|
227
|
+
# @param component_name [String] Name of your registered component
|
|
228
|
+
# @param options [Hash] Same options as react_component
|
|
229
|
+
# @return [ReactOnRailsPro::AsyncValue] Call .value to get the rendered HTML
|
|
230
|
+
#
|
|
231
|
+
# @example
|
|
232
|
+
# <% header = async_react_component("Header", props: @header_props) %>
|
|
233
|
+
# <% sidebar = async_react_component("Sidebar", props: @sidebar_props) %>
|
|
234
|
+
# <%= header.value %>
|
|
235
|
+
# <%= sidebar.value %>
|
|
236
|
+
#
|
|
237
|
+
def async_react_component(component_name, options = {})
|
|
238
|
+
unless defined?(@react_on_rails_async_barrier) && @react_on_rails_async_barrier
|
|
239
|
+
raise ReactOnRailsPro::Error,
|
|
240
|
+
"async_react_component requires AsyncRendering concern. " \
|
|
241
|
+
"Include ReactOnRailsPro::AsyncRendering in your controller and call enable_async_react_rendering."
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
task = @react_on_rails_async_barrier.async do
|
|
245
|
+
react_component(component_name, options)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
ReactOnRailsPro::AsyncValue.new(task: task)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Renders a React component asynchronously with caching support.
|
|
252
|
+
# Cache lookup is synchronous - cache hits return immediately without async.
|
|
253
|
+
# Cache misses trigger async render and cache the result on completion.
|
|
254
|
+
#
|
|
255
|
+
# All the same options as cached_react_component apply:
|
|
256
|
+
# 1. You must pass the props as a block (evaluated only on cache miss)
|
|
257
|
+
# 2. Provide the cache_key option
|
|
258
|
+
# 3. Optionally provide :cache_options for Rails.cache (expires_in, etc.)
|
|
259
|
+
# 4. Provide :if or :unless for conditional caching
|
|
260
|
+
#
|
|
261
|
+
# @param component_name [String] Name of your registered component
|
|
262
|
+
# @param options [Hash] Options including cache_key and cache_options
|
|
263
|
+
# @yield Block that returns props (evaluated only on cache miss)
|
|
264
|
+
# @return [ReactOnRailsPro::AsyncValue, ReactOnRailsPro::ImmediateAsyncValue]
|
|
265
|
+
#
|
|
266
|
+
# @example
|
|
267
|
+
# <% card = cached_async_react_component("ProductCard", cache_key: @product) { @product.to_props } %>
|
|
268
|
+
# <%= card.value %>
|
|
269
|
+
#
|
|
270
|
+
def cached_async_react_component(component_name, raw_options = {}, &block)
|
|
271
|
+
ReactOnRailsPro::Utils.with_trace(component_name) do
|
|
272
|
+
check_caching_options!(raw_options, block)
|
|
273
|
+
fetch_async_react_component(component_name, raw_options, &block)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
212
277
|
if defined?(ScoutApm)
|
|
213
278
|
include ScoutApm::Tracer
|
|
214
279
|
instrument_method :cached_react_component, type: "ReactOnRails", name: "cached_react_component"
|
|
@@ -246,30 +311,29 @@ module ReactOnRailsProHelper
|
|
|
246
311
|
load_pack_for_generated_component(component_name, render_options)
|
|
247
312
|
|
|
248
313
|
initial_result, *rest_chunks = cached_chunks
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
314
|
+
|
|
315
|
+
# Enqueue remaining chunks asynchronously
|
|
316
|
+
@async_barrier.async do
|
|
317
|
+
rest_chunks.each { |chunk| @main_output_queue.enqueue(chunk) }
|
|
252
318
|
end
|
|
253
|
-
|
|
319
|
+
|
|
320
|
+
# Return first chunk directly
|
|
254
321
|
initial_result
|
|
255
322
|
end
|
|
256
323
|
|
|
257
324
|
def handle_stream_cache_miss(component_name, raw_options, auto_load_bundle, view_cache_key, &block)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
end
|
|
271
|
-
@rorp_rendering_fibers << wrapper_fiber
|
|
272
|
-
initial_result
|
|
325
|
+
cache_aware_options = raw_options.merge(
|
|
326
|
+
on_complete: lambda { |chunks|
|
|
327
|
+
Rails.cache.write(view_cache_key, chunks, raw_options[:cache_options] || {})
|
|
328
|
+
}
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
render_stream_component_with_props(
|
|
332
|
+
component_name,
|
|
333
|
+
cache_aware_options,
|
|
334
|
+
auto_load_bundle,
|
|
335
|
+
&block
|
|
336
|
+
)
|
|
273
337
|
end
|
|
274
338
|
|
|
275
339
|
def render_stream_component_with_props(component_name, raw_options, auto_load_bundle)
|
|
@@ -291,25 +355,117 @@ module ReactOnRailsProHelper
|
|
|
291
355
|
raise ReactOnRailsPro::Error, "Option 'cache_key' is required for React on Rails caching"
|
|
292
356
|
end
|
|
293
357
|
|
|
294
|
-
|
|
295
|
-
|
|
358
|
+
# Async version of fetch_react_component. Handles cache lookup synchronously,
|
|
359
|
+
# returns ImmediateAsyncValue on hit, AsyncValue on miss.
|
|
360
|
+
def fetch_async_react_component(component_name, raw_options, &block)
|
|
361
|
+
unless defined?(@react_on_rails_async_barrier) && @react_on_rails_async_barrier
|
|
362
|
+
raise ReactOnRailsPro::Error,
|
|
363
|
+
"cached_async_react_component requires AsyncRendering concern. " \
|
|
364
|
+
"Include ReactOnRailsPro::AsyncRendering in your controller and call enable_async_react_rendering."
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Check conditional caching (:if / :unless options)
|
|
368
|
+
unless ReactOnRailsPro::Cache.use_cache?(raw_options)
|
|
369
|
+
return render_async_react_component_uncached(component_name, raw_options, &block)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
cache_key = ReactOnRailsPro::Cache.react_component_cache_key(component_name, raw_options)
|
|
373
|
+
cache_options = raw_options[:cache_options] || {}
|
|
374
|
+
Rails.logger.debug { "React on Rails Pro async cache_key is #{cache_key.inspect}" }
|
|
375
|
+
|
|
376
|
+
# Synchronous cache lookup
|
|
377
|
+
cached_result = Rails.cache.read(cache_key, cache_options)
|
|
378
|
+
if cached_result
|
|
379
|
+
Rails.logger.debug { "React on Rails Pro async cache HIT for #{cache_key.inspect}" }
|
|
380
|
+
render_options = ReactOnRails::ReactComponent::RenderOptions.new(
|
|
381
|
+
react_component_name: component_name,
|
|
382
|
+
options: raw_options
|
|
383
|
+
)
|
|
384
|
+
load_pack_for_generated_component(component_name, render_options)
|
|
385
|
+
return ReactOnRailsPro::ImmediateAsyncValue.new(cached_result)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
Rails.logger.debug { "React on Rails Pro async cache MISS for #{cache_key.inspect}" }
|
|
389
|
+
render_async_react_component_with_cache(component_name, raw_options, cache_key, cache_options, &block)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Renders async without caching (when :if/:unless conditions disable cache)
|
|
393
|
+
def render_async_react_component_uncached(component_name, raw_options, &block)
|
|
394
|
+
options = prepare_async_render_options(raw_options, &block)
|
|
395
|
+
|
|
396
|
+
task = @react_on_rails_async_barrier.async do
|
|
397
|
+
react_component(component_name, options)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
ReactOnRailsPro::AsyncValue.new(task: task)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Renders async and writes to cache on completion
|
|
404
|
+
def render_async_react_component_with_cache(component_name, raw_options, cache_key, cache_options, &block)
|
|
405
|
+
options = prepare_async_render_options(raw_options, &block)
|
|
406
|
+
|
|
407
|
+
task = @react_on_rails_async_barrier.async do
|
|
408
|
+
result = react_component(component_name, options)
|
|
409
|
+
Rails.cache.write(cache_key, result, cache_options)
|
|
410
|
+
result
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
ReactOnRailsPro::AsyncValue.new(task: task)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def prepare_async_render_options(raw_options)
|
|
417
|
+
raw_options.merge(
|
|
418
|
+
props: yield,
|
|
419
|
+
skip_prerender_cache: true,
|
|
420
|
+
auto_load_bundle: ReactOnRails.configuration.auto_load_bundle || raw_options[:auto_load_bundle]
|
|
421
|
+
)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def consumer_stream_async(on_complete:)
|
|
425
|
+
require "async/variable"
|
|
426
|
+
|
|
427
|
+
if @async_barrier.nil?
|
|
296
428
|
raise ReactOnRails::Error,
|
|
297
429
|
"You must call stream_view_containing_react_components to render the view containing the react component"
|
|
298
430
|
end
|
|
299
431
|
|
|
300
|
-
|
|
432
|
+
# Create a variable to hold the first chunk for synchronous return
|
|
433
|
+
first_chunk_var = Async::Variable.new
|
|
434
|
+
all_chunks = [] if on_complete # Only collect if callback provided
|
|
435
|
+
|
|
436
|
+
# Start an async task on the barrier to stream all chunks
|
|
437
|
+
@async_barrier.async do
|
|
301
438
|
stream = yield
|
|
302
|
-
stream
|
|
303
|
-
|
|
304
|
-
end
|
|
439
|
+
process_stream_chunks(stream, first_chunk_var, all_chunks)
|
|
440
|
+
on_complete&.call(all_chunks)
|
|
305
441
|
end
|
|
306
442
|
|
|
307
|
-
|
|
443
|
+
# Wait for and return the first chunk (blocking)
|
|
444
|
+
first_chunk_var.wait
|
|
445
|
+
first_chunk_var.value
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def process_stream_chunks(stream, first_chunk_var, all_chunks)
|
|
449
|
+
is_first = true
|
|
450
|
+
|
|
451
|
+
stream.each_chunk do |chunk|
|
|
452
|
+
# Check if client disconnected before processing chunk
|
|
453
|
+
break if response.stream.closed?
|
|
454
|
+
|
|
455
|
+
all_chunks&.push(chunk)
|
|
456
|
+
|
|
457
|
+
if is_first
|
|
458
|
+
# Store first chunk in variable for synchronous return
|
|
459
|
+
first_chunk_var.value = chunk
|
|
460
|
+
is_first = false
|
|
461
|
+
else
|
|
462
|
+
# Enqueue remaining chunks to main output queue
|
|
463
|
+
@main_output_queue.enqueue(chunk)
|
|
464
|
+
end
|
|
465
|
+
end
|
|
308
466
|
|
|
309
|
-
#
|
|
310
|
-
|
|
311
|
-
# all updates will be appended to the stream sent to browser
|
|
312
|
-
rendering_fiber.resume
|
|
467
|
+
# Handle case where stream has no chunks
|
|
468
|
+
first_chunk_var.value = nil if is_first
|
|
313
469
|
end
|
|
314
470
|
|
|
315
471
|
def internal_stream_react_component(component_name, options = {})
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# Server-side rendering with code-splitting using Loadable/Components
|
|
2
|
+
|
|
2
3
|
by ShakaCode
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
_Last updated September 19, 2022_
|
|
5
6
|
|
|
6
7
|
## Introduction
|
|
8
|
+
|
|
7
9
|
The [React library recommends](https://loadable-components.com/docs/getting-started/) the use of React.lazy for code splitting with dynamic imports except
|
|
8
10
|
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.
|
|
11
|
+
for server-side rendering with dynamic imports.
|
|
10
12
|
|
|
11
13
|
Note, in 2019 and prior, the code-splitting feature was implemented using `react-loadable`. The React
|
|
12
14
|
team no longer recommends that library. The new way is far preferable.
|
|
@@ -18,7 +20,8 @@ yarn add @loadable/babel-plugin @loadable/component @loadable/server @loadable/
|
|
|
18
20
|
```
|
|
19
21
|
|
|
20
22
|
### Summary
|
|
21
|
-
|
|
23
|
+
|
|
24
|
+
- [`@loadable/babel-plugin`](https://loadable-components.com/docs/getting-started/) - The plugin transforms your code to be ready for Server Side Rendering.
|
|
22
25
|
- `@loadable/component` - Main library for creating loadable components.
|
|
23
26
|
- `@loadable/server` - Has functions for collecting chunks and provide style, script, link tags for the server.
|
|
24
27
|
- `@loadable/webpack-plugin` - The plugin to create a stats file with all chunks, assets information.
|
|
@@ -35,15 +38,16 @@ See example of server configuration differences in the loadable-components [exam
|
|
|
35
38
|
for server-side rendering](https://github.com/gregberge/loadable-components/blob/master/examples/server-side-rendering/webpack.config.babel.js)
|
|
36
39
|
|
|
37
40
|
You need to configure 3 things:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
|
|
42
|
+
1. `target`
|
|
43
|
+
a. client-side: `web`
|
|
44
|
+
b. server-side: `node`
|
|
41
45
|
2. `output.libraryTarget`
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
3. babel-loader options.caller = 'node' or 'web'
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
a. client-side: `undefined`
|
|
47
|
+
b. server-side: `commonjs2`
|
|
48
|
+
3. babel-loader options.caller = 'node' or 'web'
|
|
49
|
+
4. `plugins`
|
|
50
|
+
a. server-side: `new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })`
|
|
47
51
|
|
|
48
52
|
```js
|
|
49
53
|
{
|
|
@@ -58,14 +62,15 @@ You need to configure 3 things:
|
|
|
58
62
|
Explanation:
|
|
59
63
|
|
|
60
64
|
- `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.
|
|
65
|
+
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
66
|
|
|
63
67
|
- `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.
|
|
68
|
+
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
69
|
|
|
66
70
|
#### Client config
|
|
67
71
|
|
|
68
72
|
For the client config we only need to add the plugin:
|
|
73
|
+
|
|
69
74
|
```js
|
|
70
75
|
{
|
|
71
76
|
plugins: [
|
|
@@ -74,30 +79,33 @@ For the client config we only need to add the plugin:
|
|
|
74
79
|
]
|
|
75
80
|
}
|
|
76
81
|
```
|
|
82
|
+
|
|
77
83
|
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
84
|
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
85
|
|
|
80
86
|
### Babel
|
|
81
87
|
|
|
82
88
|
Per [the docs](https://loadable-components.com/docs/babel-plugin/#transformation):
|
|
89
|
+
|
|
83
90
|
> The plugin transforms your code to be ready for Server Side Rendering
|
|
84
91
|
|
|
85
92
|
Add this to `babel.config.js`:
|
|
93
|
+
|
|
86
94
|
```js
|
|
87
95
|
{
|
|
88
96
|
"plugins": ["@loadable/babel-plugin"]
|
|
89
97
|
}
|
|
90
98
|
```
|
|
91
|
-
https://loadable-components.com/docs/babel-plugin/
|
|
92
99
|
|
|
100
|
+
https://loadable-components.com/docs/babel-plugin/
|
|
93
101
|
|
|
94
102
|
### Convert components into loadable components
|
|
95
103
|
|
|
96
104
|
Instead of importing the component directly, use a dynamic import:
|
|
97
105
|
|
|
98
106
|
```js
|
|
99
|
-
import load from '@loadable/component'
|
|
100
|
-
const MyComponent = load(() => import('./MyComponent'))
|
|
107
|
+
import load from '@loadable/component';
|
|
108
|
+
const MyComponent = load(() => import('./MyComponent'));
|
|
101
109
|
```
|
|
102
110
|
|
|
103
111
|
### Resolving issue with ChunkLoadError
|
|
@@ -118,22 +126,25 @@ const consoleDebug = (fn) => {
|
|
|
118
126
|
console.debug(fn());
|
|
119
127
|
}
|
|
120
128
|
};
|
|
121
|
-
const retry = (fn, retryMessage = '', retriesLeft = 3, interval = 500) =>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
const retry = (fn, retryMessage = '', retriesLeft = 3, interval = 500) =>
|
|
130
|
+
new Promise((resolve, reject) => {
|
|
131
|
+
fn()
|
|
132
|
+
.then(resolve)
|
|
133
|
+
.catch(() => {
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
if (retriesLeft === 1) {
|
|
136
|
+
console.warn(`Maximum retries exceeded, retryMessage: ${retryMessage}. Reloading page...`);
|
|
137
|
+
window.location.reload();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Passing on "reject" is the important part
|
|
141
|
+
consoleDebug(
|
|
142
|
+
() => `Trying request, retryMessage: ${retryMessage}, retriesLeft: ${retriesLeft - 1}`,
|
|
143
|
+
);
|
|
144
|
+
retry(fn, retryMessage, retriesLeft - 1, interval).then(resolve, reject);
|
|
145
|
+
}, interval);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
137
148
|
export default retry;
|
|
138
149
|
```
|
|
139
150
|
|
|
@@ -152,21 +163,21 @@ const HomePage = loadable(() => retry(() => import('./HomePage')));
|
|
|
152
163
|
|
|
153
164
|
In the client bundle, we need to wrap the `hydrateRoot` call into a `loadableReady` function.
|
|
154
165
|
So, hydration will be fired only after all necessary chunks preloads. In this example below,
|
|
155
|
-
`ClientApp` is registering as `App`.
|
|
166
|
+
`ClientApp` is registering as `App`.
|
|
156
167
|
|
|
157
168
|
```js
|
|
158
169
|
import React from 'react';
|
|
159
170
|
import ReactOnRails from 'react-on-rails';
|
|
160
|
-
import { hydrateRoot } from 'react-dom/client'
|
|
161
|
-
import { loadableReady } from '@loadable/component'
|
|
171
|
+
import { hydrateRoot } from 'react-dom/client';
|
|
172
|
+
import { loadableReady } from '@loadable/component';
|
|
162
173
|
import App from './App';
|
|
163
174
|
|
|
164
175
|
const ClientApp = (props, railsContext, domId) => {
|
|
165
176
|
loadableReady(() => {
|
|
166
|
-
const root = document.getElementById(domId)
|
|
177
|
+
const root = document.getElementById(domId);
|
|
167
178
|
hydrateRoot(root, <App {...props} />);
|
|
168
|
-
})
|
|
169
|
-
}
|
|
179
|
+
});
|
|
180
|
+
};
|
|
170
181
|
|
|
171
182
|
ReactOnRails.register({
|
|
172
183
|
App: ClientApp,
|
|
@@ -175,20 +186,20 @@ ReactOnRails.register({
|
|
|
175
186
|
|
|
176
187
|
#### Server
|
|
177
188
|
|
|
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`.
|
|
189
|
+
The purpose of the server function is to collect all rendered chunks and pass them as script, link,
|
|
190
|
+
style tags to the Rails view. In this example below, `ServerApp` is registering as `App`.
|
|
180
191
|
|
|
181
192
|
```js
|
|
182
193
|
import React from 'react';
|
|
183
194
|
import ReactOnRails from 'react-on-rails';
|
|
184
|
-
import { ChunkExtractor } from '@loadable/server'
|
|
185
|
-
import App from './App'
|
|
186
|
-
import path from 'path'
|
|
195
|
+
import { ChunkExtractor } from '@loadable/server';
|
|
196
|
+
import App from './App';
|
|
197
|
+
import path from 'path';
|
|
187
198
|
|
|
188
199
|
const ServerApp = (props, railsContext) => {
|
|
189
200
|
// This loadable-stats file was generated by `LoadablePlugin` in client webpack config.
|
|
190
201
|
// 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.
|
|
202
|
+
// a remote server, the file should be a sibling of this file.
|
|
192
203
|
// __dirname is going to be the directory where the server-bundle.js exists
|
|
193
204
|
// Note, React on Rails Pro automatically copies the loadable-stats.json to the same place as the
|
|
194
205
|
// server-bundle.js. Thus, the __dirname of this code is where we can find loadable-stats.json.
|
|
@@ -198,10 +209,10 @@ const ServerApp = (props, railsContext) => {
|
|
|
198
209
|
// This object is used to search filenames by corresponding chunk names.
|
|
199
210
|
// See https://loadable-components.com/docs/api-loadable-server/#chunkextractor
|
|
200
211
|
// for the entryPoints, pass an array of all your entryPoints using dynamic imports
|
|
201
|
-
const extractor = new ChunkExtractor({ statsFile, entrypoints: ['client-bundle'] })
|
|
212
|
+
const extractor = new ChunkExtractor({ statsFile, entrypoints: ['client-bundle'] });
|
|
202
213
|
|
|
203
214
|
// It creates the wrapper `ChunkExtractorManager` around `App` to collect chunk names of rendered components.
|
|
204
|
-
const jsx = extractor.collectChunks(<App {...props} railsContext={railsContext} />)
|
|
215
|
+
const jsx = extractor.collectChunks(<App {...props} railsContext={railsContext} />);
|
|
205
216
|
|
|
206
217
|
const componentHtml = renderToString(jsx);
|
|
207
218
|
|
|
@@ -211,8 +222,8 @@ const ServerApp = (props, railsContext) => {
|
|
|
211
222
|
// Returns all the files with rendered chunks for furture insert into rails view.
|
|
212
223
|
linkTags: extractor.getLinkTags(),
|
|
213
224
|
styleTags: extractor.getStyleTags(),
|
|
214
|
-
scriptTags: extractor.getScriptTags()
|
|
215
|
-
}
|
|
225
|
+
scriptTags: extractor.getScriptTags(),
|
|
226
|
+
},
|
|
216
227
|
};
|
|
217
228
|
};
|
|
218
229
|
|
|
@@ -224,6 +235,7 @@ ReactOnRails.register({
|
|
|
224
235
|
## Configure react_on_rails_pro
|
|
225
236
|
|
|
226
237
|
### React on Rails Pro
|
|
238
|
+
|
|
227
239
|
You must set `config.assets_top_copy` so that the node-renderer will have access to the loadable-stats.json.
|
|
228
240
|
|
|
229
241
|
```ruby
|
|
@@ -233,15 +245,16 @@ You must set `config.assets_top_copy` so that the node-renderer will have access
|
|
|
233
245
|
Your server rendering code, per the above, will find this file like this:
|
|
234
246
|
|
|
235
247
|
```js
|
|
236
|
-
|
|
237
|
-
```
|
|
248
|
+
const statsFile = path.resolve(__dirname, 'loadable-stats.json');
|
|
249
|
+
```
|
|
238
250
|
|
|
239
251
|
Note, if `__dirname` is not working in your webpack build, that's because you didn't set `node: false`
|
|
240
252
|
in your webpack configuration. That turns off the polyfills for things like `__dirname`.
|
|
241
253
|
|
|
242
|
-
|
|
243
254
|
### Node Renderer
|
|
255
|
+
|
|
244
256
|
In your `node-renderer.js` file which runs node renderer, you need to specify `supportModules` options as follows:
|
|
257
|
+
|
|
245
258
|
```js
|
|
246
259
|
const path = require('path');
|
|
247
260
|
const env = process.env;
|
|
@@ -261,7 +274,7 @@ reactOnRailsProNodeRenderer(config);
|
|
|
261
274
|
```erb
|
|
262
275
|
<% res = react_component_hash("App", props: {}, prerender: true) %>
|
|
263
276
|
<%= content_for :link_tags, res['linkTags'] %>
|
|
264
|
-
<%= content_for :style_tags, res['styleTags'] %>
|
|
277
|
+
<%= content_for :style_tags, res['styleTags'] %>
|
|
265
278
|
|
|
266
279
|
<%= res['componentHtml'].html_safe %>
|
|
267
280
|
|
|
@@ -269,6 +282,7 @@ reactOnRailsProNodeRenderer(config);
|
|
|
269
282
|
```
|
|
270
283
|
|
|
271
284
|
## Making HMR Work
|
|
285
|
+
|
|
272
286
|
To make HMR work, it's best to disable loadable-components when using the Dev Server.
|
|
273
287
|
Note: you will need access to our **private** React on Rails Pro repository to open the following links.
|
|
274
288
|
|
|
@@ -277,9 +291,11 @@ Take a look at the code searches for ['imports-loadable'](https://github.com/sha
|
|
|
277
291
|
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
292
|
|
|
279
293
|
### Webpack configuration
|
|
294
|
+
|
|
280
295
|
Use the [NormalModuleReplacement plugin](https://webpack.js.org/plugins/normal-module-replacement-plugin/):
|
|
281
296
|
|
|
282
297
|
[code](https://github.com/shakacode/react_on_rails_pro/blob/a361f4e163b9170f180ae07ee312fb9b4c719fc3/spec/dummy/config/webpack/environment.js#L81-L91)
|
|
298
|
+
|
|
283
299
|
```js
|
|
284
300
|
if (isWebpackDevServer) {
|
|
285
301
|
environment.plugins.append(
|
|
@@ -305,7 +321,7 @@ Note: you will need access to our **private** React on Rails Pro repository to o
|
|
|
305
321
|
### Client-Side Startup
|
|
306
322
|
|
|
307
323
|
- [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.
|
|
324
|
+
- [spec/dummy/client/app/loadable/loadable-client.imports-loadable.jsx](https://github.com/shakacode/react_on_rails_pro/blob/master/spec/dummy/client/app/loadable/loadable-client.imports-loadable.jsx)
|
|
309
325
|
|
|
310
326
|
### Server-Side Startup
|
|
311
327
|
|
data/eslint.config.mjs
CHANGED
|
@@ -3,7 +3,6 @@ import { includeIgnoreFile } from '@eslint/compat';
|
|
|
3
3
|
import js from '@eslint/js';
|
|
4
4
|
import { FlatCompat } from '@eslint/eslintrc';
|
|
5
5
|
import { defineConfig, globalIgnores } from 'eslint/config';
|
|
6
|
-
import importPlugin from 'eslint-plugin-import';
|
|
7
6
|
import jest from 'eslint-plugin-jest';
|
|
8
7
|
import prettierRecommended from 'eslint-plugin-prettier/recommended';
|
|
9
8
|
import globals from 'globals';
|
|
@@ -22,8 +21,9 @@ export default defineConfig([
|
|
|
22
21
|
'spec/react_on_rails/dummy-for-generators',
|
|
23
22
|
// includes some generated code
|
|
24
23
|
'spec/dummy/client/app/packs/server-bundle.js',
|
|
25
|
-
'packages/
|
|
26
|
-
'packages/node-renderer/
|
|
24
|
+
'../packages/react-on-rails/', // Ignore open-source package (has its own linting)
|
|
25
|
+
'../packages/react-on-rails-pro-node-renderer/lib/',
|
|
26
|
+
'../packages/react-on-rails-pro-node-renderer/tests/fixtures',
|
|
27
27
|
'**/node_modules/',
|
|
28
28
|
'**/assets/webpack/',
|
|
29
29
|
'**/generated/',
|
|
@@ -129,7 +129,7 @@ export default defineConfig([
|
|
|
129
129
|
},
|
|
130
130
|
{
|
|
131
131
|
files: ['**/*.ts{x,}'],
|
|
132
|
-
extends: [
|
|
132
|
+
extends: [typescriptEslint.configs.strictTypeChecked],
|
|
133
133
|
|
|
134
134
|
languageOptions: {
|
|
135
135
|
parserOptions: {
|
|
@@ -166,7 +166,7 @@ export default defineConfig([
|
|
|
166
166
|
},
|
|
167
167
|
},
|
|
168
168
|
{
|
|
169
|
-
files: ['packages/node-renderer/tests/**', '**/*.test.{js,jsx,ts,tsx}'],
|
|
169
|
+
files: ['../packages/react-on-rails-pro-node-renderer/tests/**', '**/*.test.{js,jsx,ts,tsx}'],
|
|
170
170
|
|
|
171
171
|
extends: [jest.configs['flat/recommended'], jest.configs['flat/style']],
|
|
172
172
|
|
|
@@ -183,8 +183,8 @@ export default defineConfig([
|
|
|
183
183
|
},
|
|
184
184
|
},
|
|
185
185
|
{
|
|
186
|
-
files: ['packages/node-renderer/src/integrations/**'],
|
|
187
|
-
ignores: ['packages/node-renderer/src/integrations/api.ts'],
|
|
186
|
+
files: ['../packages/react-on-rails-pro-node-renderer/src/integrations/**'],
|
|
187
|
+
ignores: ['../packages/react-on-rails-pro-node-renderer/src/integrations/api.ts'],
|
|
188
188
|
|
|
189
189
|
rules: {
|
|
190
190
|
// Integrations should only use the public integration API
|
|
@@ -214,6 +214,25 @@ export default defineConfig([
|
|
|
214
214
|
'react-hooks/rules-of-hooks': ['off'],
|
|
215
215
|
},
|
|
216
216
|
},
|
|
217
|
+
{
|
|
218
|
+
// Dummy apps have dependencies managed separately and may not be installed
|
|
219
|
+
files: ['spec/dummy/**/*', 'spec/execjs-compatible-dummy/**/*'],
|
|
220
|
+
rules: {
|
|
221
|
+
'import/no-unresolved': 'off',
|
|
222
|
+
'import/named': 'off',
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
// Pro packages use monorepo workspace imports that ESLint can't resolve
|
|
227
|
+
// TypeScript compiler validates these imports
|
|
228
|
+
files: ['../packages/react-on-rails-pro/**/*', '../packages/react-on-rails-pro-node-renderer/**/*'],
|
|
229
|
+
rules: {
|
|
230
|
+
'import/named': 'off',
|
|
231
|
+
'import/no-unresolved': 'off',
|
|
232
|
+
'import/no-cycle': 'off',
|
|
233
|
+
'import/extensions': 'off',
|
|
234
|
+
},
|
|
235
|
+
},
|
|
217
236
|
// must be the last config in the array
|
|
218
237
|
// https://github.com/prettier/eslint-plugin-prettier?tab=readme-ov-file#configuration-new-eslintconfigjs
|
|
219
238
|
prettierRecommended,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ReactOnRailsPro
|
|
4
|
+
# AsyncValue wraps an Async task to provide a simple interface for
|
|
5
|
+
# retrieving the result of an async react_component render.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# async_value = async_react_component("MyComponent", props: { name: "World" })
|
|
9
|
+
# # ... do other work ...
|
|
10
|
+
# html = async_value.value # blocks until result is ready
|
|
11
|
+
#
|
|
12
|
+
class AsyncValue
|
|
13
|
+
def initialize(task:)
|
|
14
|
+
@task = task
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Blocks until result is ready, returns HTML string.
|
|
18
|
+
# If the async task raised an exception, it will be re-raised here.
|
|
19
|
+
def value
|
|
20
|
+
@task.wait
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def resolved?
|
|
24
|
+
@task.finished?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_s
|
|
28
|
+
value.to_s
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def html_safe
|
|
32
|
+
value.html_safe
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|