Skip to content

Commit 56ee2bd

Browse files
should force load react-components which send over turbo-stream (#1620)
* should force load react-components which send over turbo-stream * fix navigation error in spec
1 parent 4cdc686 commit 56ee2bd

20 files changed

+158
-6
lines changed

Gemfile.development_dependencies

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ gem "sprockets", "~> 4.0"
2222

2323
gem "amazing_print"
2424

25+
gem "turbo-rails"
26+
2527
group :development, :test do
2628
gem "package_json"
2729
gem "listen"

Gemfile.lock

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
react_on_rails (14.0.3)
4+
react_on_rails (14.0.4)
55
addressable
66
connection_pool
77
execjs (~> 2.5)
@@ -369,6 +369,10 @@ GEM
369369
tins (1.33.0)
370370
bigdecimal
371371
sync
372+
turbo-rails (2.0.6)
373+
actionpack (>= 6.0.0)
374+
activejob (>= 6.0.0)
375+
railties (>= 6.0.0)
372376
turbolinks (5.2.1)
373377
turbolinks-source (~> 5.2)
374378
turbolinks-source (5.2.0)
@@ -431,6 +435,7 @@ DEPENDENCIES
431435
spring (~> 4.0)
432436
sprockets (~> 4.0)
433437
sqlite3 (~> 1.6)
438+
turbo-rails
434439
turbolinks
435440
uglifier
436441
webdrivers (= 5.3.0)

lib/react_on_rails/configuration.rb

+7-3
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ def self.configuration
3939
i18n_output_format: nil,
4040
components_subdirectory: nil,
4141
make_generated_server_bundle_the_entrypoint: false,
42-
defer_generated_component_packs: true
42+
defer_generated_component_packs: true,
43+
# forces the loading of React components
44+
force_load: false
4345
)
4446
end
4547

@@ -53,7 +55,8 @@ class Configuration
5355
:server_render_method, :random_dom_id, :auto_load_bundle,
5456
:same_bundle_for_client_and_server, :rendering_props_extension,
5557
:make_generated_server_bundle_the_entrypoint,
56-
:defer_generated_component_packs
58+
:defer_generated_component_packs,
59+
:force_load
5760

5861
# rubocop:disable Metrics/AbcSize
5962
def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender: nil,
@@ -68,7 +71,7 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
6871
same_bundle_for_client_and_server: nil,
6972
i18n_dir: nil, i18n_yml_dir: nil, i18n_output_format: nil,
7073
random_dom_id: nil, server_render_method: nil, rendering_props_extension: nil,
71-
components_subdirectory: nil, auto_load_bundle: nil)
74+
components_subdirectory: nil, auto_load_bundle: nil, force_load: nil)
7275
self.node_modules_location = node_modules_location.present? ? node_modules_location : Rails.root
7376
self.generated_assets_dirs = generated_assets_dirs
7477
self.generated_assets_dir = generated_assets_dir
@@ -106,6 +109,7 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
106109
self.auto_load_bundle = auto_load_bundle
107110
self.make_generated_server_bundle_the_entrypoint = make_generated_server_bundle_the_entrypoint
108111
self.defer_generated_component_packs = defer_generated_component_packs
112+
self.force_load = force_load
109113
end
110114
# rubocop:enable Metrics/AbcSize
111115

lib/react_on_rails/helper.rb

+8
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,14 @@ def internal_react_component(react_component_name, options = {})
441441
"data-trace" => (render_options.trace ? true : nil),
442442
"data-dom-id" => render_options.dom_id)
443443

444+
if render_options.force_load
445+
component_specification_tag.concat(
446+
content_tag(:script, %(
447+
ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
448+
).html_safe)
449+
)
450+
end
451+
444452
load_pack_for_generated_component(react_component_name, render_options)
445453
# Create the HTML rendering part
446454
result = server_rendered_react_component(render_options)

lib/react_on_rails/react_component/render_options.rb

+4
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ def logging_on_server
9191
retrieve_configuration_value_for(:logging_on_server)
9292
end
9393

94+
def force_load
95+
retrieve_configuration_value_for(:force_load)
96+
end
97+
9498
def to_s
9599
"{ react_component_name = #{react_component_name}, options = #{options}, request_digest = #{request_digest}"
96100
end

node_package/src/ReactOnRails.ts

+4
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ ctx.ReactOnRails = {
133133
ClientStartup.reactOnRailsPageLoaded();
134134
},
135135

136+
reactOnRailsComponentLoaded(domId: string): void {
137+
ClientStartup.reactOnRailsComponentLoaded(domId);
138+
},
139+
136140
/**
137141
* Returns CSRF authenticity token inserted by Rails csrf_meta_tags
138142
* @returns String or null

node_package/src/clientStartup.ts

+19
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,25 @@ export function reactOnRailsPageLoaded(): void {
218218
forEachReactOnRailsComponentRender(context, railsContext);
219219
}
220220

221+
export function reactOnRailsComponentLoaded(domId: string): void {
222+
debugTurbolinks(`reactOnRailsComponentLoaded ${domId}`);
223+
224+
const railsContext = parseRailsContext();
225+
226+
// If no react on rails components
227+
if (!railsContext) return;
228+
229+
const context = findContext();
230+
if (supportsRootApi) {
231+
context.roots = [];
232+
}
233+
234+
const el = document.querySelector(`[data-dom-id=${domId}]`);
235+
if (!el) return;
236+
237+
render(el, context, railsContext);
238+
}
239+
221240
function unmount(el: Element): void {
222241
const domNodeId = domNodeIdForEl(el);
223242
const domNode = document.getElementById(domNodeId);

node_package/src/types/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export interface ReactOnRails {
126126
setOptions(newOptions: {traceTurbolinks: boolean}): void;
127127
reactHydrateOrRender(domNode: Element, reactElement: ReactElement, hydrate: boolean): RenderReturnType;
128128
reactOnRailsPageLoaded(): void;
129+
reactOnRailsComponentLoaded(domId: string): void;
129130
authenticityToken(): string | null;
130131
authenticityHeaders(otherHeaders: { [id: string]: string }): AuthenticityHeaders;
131132
option(key: string): string | number | boolean | undefined;

spec/dummy/Gemfile.lock

+5
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,10 @@ GEM
362362
timeout (0.4.1)
363363
tins (1.32.1)
364364
sync
365+
turbo-rails (2.0.6)
366+
actionpack (>= 6.0.0)
367+
activejob (>= 6.0.0)
368+
railties (>= 6.0.0)
365369
turbolinks (5.2.1)
366370
turbolinks-source (~> 5.2)
367371
turbolinks-source (5.2.0)
@@ -423,6 +427,7 @@ DEPENDENCIES
423427
spring (~> 4.0)
424428
sprockets (~> 4.0)
425429
sqlite3 (~> 1.6)
430+
turbo-rails
426431
turbolinks
427432
uglifier
428433
webdrivers (= 5.3.0)

spec/dummy/app/controllers/pages_controller.rb

+6
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ def data
3636
}.merge(xss_payload)
3737
}
3838

39+
@app_props_hello_from_turbo_stream = {
40+
helloTurboStreamData: {
41+
name: "Mrs. Client Side Rendering From Turbo Stream"
42+
}.merge(xss_payload)
43+
}
44+
3945
@app_props_hello_again = {
4046
helloWorldData: {
4147
name: "Mrs. Client Side Hello Again"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<%= turbo_frame_tag 'hello-turbo-stream' do %>
2+
<%= button_to "send me hello-turbo-stream component", turbo_stream_send_hello_world_path %>
3+
<% end %>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<%= turbo_stream.update 'hello-turbo-stream' do %>
2+
<%= react_component("HelloTurboStream", props: @app_props_hello_from_turbo_stream, force_load: true) %>
3+
<% end %>

spec/dummy/client/app/packs/client-bundle.js

+7
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@ import 'core-js/stable';
22
import 'regenerator-runtime/runtime';
33
import 'jquery';
44
import 'jquery-ujs';
5+
import '@hotwired/turbo-rails';
56

67
import ReactOnRails from 'react-on-rails';
78

9+
import HelloTurboStream from '../startup/HelloTurboStream';
810
import SharedReduxStore from '../stores/SharedReduxStore';
911

1012
ReactOnRails.setOptions({
1113
traceTurbolinks: true,
14+
turbo: true,
15+
});
16+
17+
ReactOnRails.register({
18+
HelloTurboStream,
1219
});
1320

1421
ReactOnRails.registerStore({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import PropTypes from 'prop-types';
2+
import React, { useState, useRef } from 'react';
3+
import RailsContext from '../components/RailsContext';
4+
5+
import css from '../components/HelloWorld.module.scss';
6+
7+
const HelloTurboStream = ({ helloTurboStreamData, railsContext }) => {
8+
const [name, setName] = useState(helloTurboStreamData.name);
9+
const nameDomRef = useRef(null);
10+
11+
const handleChange = () => {
12+
setName(nameDomRef.current.value);
13+
};
14+
15+
return (
16+
<div>
17+
<h3 className={css.brightColor}>Hello, {name}!</h3>
18+
{railsContext && <RailsContext {...{ railsContext }} />}
19+
</div>
20+
);
21+
};
22+
23+
HelloTurboStream.propTypes = {
24+
helloTurboStreamData: PropTypes.shape({
25+
name: PropTypes.string,
26+
}).isRequired,
27+
railsContext: PropTypes.object,
28+
};
29+
30+
export default HelloTurboStream;

spec/dummy/config/routes.rb

+2
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,6 @@
4141
get "image_example" => "pages#image_example"
4242
get "context_function_return_jsx" => "pages#context_function_return_jsx"
4343
get "pure_component_wrapped_in_function" => "pages#pure_component_wrapped_in_function"
44+
get "turbo_frame_tag_hello_world" => "pages#turbo_frame_tag_hello_world"
45+
post "turbo_stream_send_hello_world" => "pages#turbo_stream_send_hello_world"
4446
end

spec/dummy/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@babel/preset-env": "7",
1313
"@babel/preset-react": "^7.10.4",
1414
"@babel/runtime": "7.17.9",
15+
"@hotwired/turbo-rails": "^8.0.4",
1516
"@rescript/react": "^0.10.3",
1617
"babel-loader": "8.2.4",
1718
"babel-plugin-macros": "^3.1.0",

spec/dummy/spec/helpers/react_on_rails_helper_spec.rb

+20
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,26 @@ class PlainReactOnRailsHelper
290290
it { is_expected.not_to include '<span id="App-react-component-0"></span>' }
291291
it { is_expected.to include '<div id="App-react-component-0"></div>' }
292292
end
293+
294+
describe "'force_load' tag option" do
295+
let(:force_load_script) do
296+
%(
297+
ReactOnRails.reactOnRailsComponentLoaded('App-react-component-0');
298+
).html_safe
299+
end
300+
301+
context "with 'force_load' == true" do
302+
subject { react_component("App", force_load: true) }
303+
304+
it { is_expected.to include force_load_script }
305+
end
306+
307+
context "without 'force_load' tag option" do
308+
subject { react_component("App") }
309+
310+
it { is_expected.not_to include force_load_script }
311+
end
312+
end
293313
end
294314

295315
describe "#redux_store" do

spec/dummy/spec/system/integration_spec.rb

+11-2
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@ def finished_all_ajax_requests?
9797
end
9898
end
9999

100+
describe "TurboStream send react component", :js do
101+
subject { page }
102+
103+
it "force load hello-world component immediately" do
104+
visit "/turbo_frame_tag_hello_world"
105+
click_on "send me hello-turbo-stream component"
106+
expect(page).to have_text "Hello, Mrs. Client Side Rendering From Turbo Stream!"
107+
end
108+
end
109+
100110
describe "Pages/client_side_log_throw", :ignore_js_errors, :js do
101111
subject { page }
102112

@@ -163,8 +173,7 @@ def finished_all_ajax_requests?
163173
subject { page }
164174

165175
before do
166-
visit "/"
167-
click_on "React Router"
176+
visit "/react_router"
168177
end
169178

170179
context "when rendering /react_router" do

spec/dummy/yarn.lock

+18
Original file line numberDiff line numberDiff line change
@@ -2056,6 +2056,19 @@
20562056
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz#d5e0706cf8c6acd8c6032f8d54070af261bbbb2f"
20572057
integrity sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==
20582058

2059+
"@hotwired/turbo-rails@^8.0.4":
2060+
version "8.0.5"
2061+
resolved "https://registry.yarnpkg.com/@hotwired/turbo-rails/-/turbo-rails-8.0.5.tgz#18c2f0e4f7f952307650308590edf5eb9544b0d3"
2062+
integrity sha512-1A9G9u28IRAl0C57z8Ka3AhNPyJdwfOrbjr+ABZk2ZEUw2QO7cJ0pgs77asUj2E/tzn1PgrxrSVu24W+1Q5uBA==
2063+
dependencies:
2064+
"@hotwired/turbo" "^8.0.5"
2065+
"@rails/actioncable" "^7.0"
2066+
2067+
"@hotwired/turbo@^8.0.5":
2068+
version "8.0.5"
2069+
resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-8.0.5.tgz#abae6dad018a891e4286e87fa0959217e3866d5a"
2070+
integrity sha512-TdZDA7fxVQ2ZycygvpnzjGPmFq4sO/E2QVg+2em/sJ3YTSsIWVEis8HmWlumz+c9DjWcUkcCuB+muF08TInpAQ==
2071+
20592072
"@jridgewell/gen-mapping@^0.1.0":
20602073
version "0.1.1"
20612074
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996"
@@ -2116,6 +2129,11 @@
21162129
schema-utils "^3.0.0"
21172130
source-map "^0.7.3"
21182131

2132+
"@rails/actioncable@^7.0":
2133+
version "7.2.0"
2134+
resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.2.0.tgz#dee66d21bc125a9819dc8080ce896eac78d8c63f"
2135+
integrity sha512-crcsPF3skrqJkFZLxesZoyUEt8ol25XtTuOAUMdLa5qQKWTZpL8eLVW71bDCwKDQLbV2z5sBZ/XGEC0i+ZZa+A==
2136+
21192137
"@rescript/react@^0.10.3":
21202138
version "0.10.3"
21212139
resolved "https://registry.yarnpkg.com/@rescript/react/-/react-0.10.3.tgz#a2a8bed6b017940ec26c2154764b350f50348889"

spec/react_on_rails/react_component/render_options_spec.rb

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
replay_console
1010
raise_on_prerender_error
1111
random_dom_id
12+
force_load
1213
].freeze
1314

1415
def the_attrs(react_component_name: "App", options: {})

0 commit comments

Comments
 (0)