Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Doc/xhr hydration examples #1095

Merged
merged 12 commits into from
Jun 1, 2018
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Changes since last non-beta release.
*Please add entries here for your pull requests that are not yet released.*

#### Changed
- Document how to manually rehydrate XHR-substituted components on client side. [PR 1095](https://github.com/shakacode/react_on_rails/pull/1095) by [hchevalier](https://github.com/hchevalier).

### [11.0.7] - 2018-05-11
#### Fixed
- Fix npm publshing. [PR 1090](https://github.com/shakacode/react_on_rails/pull/1090) by [justin808](https://github.com/justin808).
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,9 @@ All options except `props, id, html_options` will inherit from your `react_on_ra
+ **replay_console:** Default is true. False will disable echoing server-rendering logs to the browser. While this can make troubleshooting server rendering difficult, so long as you have the configuration of `logging_on_server` set to true, you'll still see the errors on the server.
+ **logging_on_server:** Default is true. True will log JS console messages and errors to the server.
+ **raise_on_prerender_error:** Default is false. True will throw an error on the server side rendering. Your controller will have to handle the error.

Note: client hydration will not trigger for components rendered through XHR. You will have to handle it with javascript.
For an example, see [spec/dummy/app/views/pages/xhr_refresh.rb](https://github.com/shakacode/react_on_rails/tree/master/spec/dummy/app/views/pages/xhr_refresh.rb).

### react_component_hash
`react_component_hash` is used to return multiple HTML strings for server rendering, such as for
Expand Down
2 changes: 2 additions & 0 deletions spec/dummy/app/assets/javascripts/jquery_rails_manifest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
//= require jquery
//= require jquery_ujs
2 changes: 2 additions & 0 deletions spec/dummy/app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<%= javascript_pack_tag('vendor-bundle', 'data-turbolinks-track': true) %>
<%= javascript_pack_tag('app-bundle', 'data-turbolinks-track': true) %>

<%= yield :other_javascript_tags %>

<%= csrf_meta_tags %>
</head>
<body>
Expand Down
9 changes: 9 additions & 0 deletions spec/dummy/app/views/pages/_xhr_refresh_partial.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<%= react_component('HelloWorld', props: { helloWorldData: { name: 'HelloWorld' } },
prerender: true,
trace: true,
id: "HelloWorld-react-component-0") %>

<%= react_component('HelloWorldRehydratable', props: { helloWorldData: { name: 'HelloWorldRehydratable' } },
prerender: true,
trace: true,
id: 'HelloWorldRehydratable-react-component-1') %>
74 changes: 74 additions & 0 deletions spec/dummy/app/views/pages/xhr_refresh.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<% provide :other_javascript_tags do %>
<%= javascript_include_tag 'jquery_rails_manifest', defer: true %>
<% end %>

<div id='component-container'>
<%= render partial: 'xhr_refresh_partial' %>
</div>

<div>
Click to refresh components through XHR (first component event handlers won't work anymore)<br/>
<%= form_tag '/xhr_refresh', method: :get, remote: true, format: :js do %>
<%= submit_tag 'Refresh', id: 'refresh', name: 'refresh' %>
<% end %>
</div>
<hr/>

<h1>React Rails Client Rehydration</h1>
<p>
This example demonstrates client side manual rehydration after a component replacement through XHR.<br/><br/>

The "Refresh" button on this page will trigger an asynchrounous refresh of component-container content.<br/>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo on "asynchronous"

Components will be prerendered by the server and inserted in the DOM (spec/dummy/app/views/pages/xhr_refresh.js.erb)<br/>
No client rehydration will occur, preventing any event handler to be correctly attached<br/><br/>

Thus, the onChange handler of the HelloWorld component won't trigger whereas the one from HellowWorldRehydratable will, thanks to the "hydrate" javscript event dispacthed from xhr_refresh.js.erb<br />
Copy link
Contributor Author

@hchevalier hchevalier May 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo on "javascript"

</p>

<hr/>
<h2>Setup</h2>
<ol>
<li>
Create component source: spec/dummy/client/app/components/HelloWorldHydratable.jsx
</li>
<li>
Expose the HelloWorldHydratable Component: spec/dummy/client/app/startup/serverRegistration.jsx and spec/dummy/client/app/startup/clientRegistration.jsx
<br/>
<pre style='white-space: pre-wrap; word-break: keep-all;'>
import HelloWorldHydratable from '../components/HelloWorldHydratable';
import ReactOnRails from 'react-on-rails';
ReactOnRails.register({ HelloWorldHydratable });
</pre>
</li>
<li>
Place the component on the view: spec/dummy/app/views/pages/xhr_refresh.html.erb, making sure it has a parent node easily selectable
<br/>
<pre style='white-space: pre-wrap; word-break: keep-all;'>
<div id='my-component-container'>
<%%= react_component("HelloWorldHydratable", props: { helloWorldData: { name: 'HelloWorld' } }, prerender: true, trace: true, id: "HelloWorldHydratable-react-component-0") %>
</div>
</pre>
</li>
<li>
Have a remote form allow to get xhr_request.js.erb
<br/>
<pre style='white-space: pre-wrap; word-break: keep-all;'>
<%%= form_tag '/xhr_refresh', method: :get, remote: true, format: :js do %>
<%%= submit_tag 'Refresh' %>
<%% end %>
</pre>
</li>
<li>
In your xhr_request.js.erb, replace your container content and dispatch the 'hydrate' event that will be caught by HelloWorldHydratable event handler
<br/>
<pre style='white-space: pre-wrap; word-break: keep-all;'>
var container = document.getElementById('component-container');
<%% new_component = react_component("HelloWorldHydratable", props: { helloWorldData: { name: 'HelloWorld' } }, prerender: true, trace: true, id: "HelloWorldHydratable-react-component-0") %>
container.innerHTML = "<%%= escape_javascript(new_component) %>";

var event = document.createEvent('Event');
event.initEvent('hydrate', true, true);
document.dispatchEvent(event);
</pre>
</li>
</ol>
6 changes: 6 additions & 0 deletions spec/dummy/app/views/pages/xhr_refresh.js.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
var container = document.getElementById('component-container');
container.innerHTML = "<%= escape_javascript(render partial: 'xhr_refresh_partial') %>";

var event = document.createEvent('Event');
event.initEvent('hydrate', true, true);
document.dispatchEvent(event);
3 changes: 3 additions & 0 deletions spec/dummy/app/views/shared/_header.erb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
<li>
<%= link_to "render_js only example", render_js_path %>
</li>
<li>
<%= link_to "XHR Refresh", xhr_refresh_path %>
</li>
<li>
<%= link_to "One Page with Many Examples at Once", root_path %>
</li>
Expand Down
93 changes: 93 additions & 0 deletions spec/dummy/client/app/components/HelloWorldRehydratable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import PropTypes from 'prop-types';
import React from 'react';
import ReactOnRails from 'react-on-rails';
import RailsContext from './RailsContext';

class HelloWorldRehydratable extends React.Component {

static propTypes = {
helloWorldData: PropTypes.shape({
name: PropTypes.string,
}).isRequired,
railsContext: PropTypes.object,
};

// Not necessary if we only call super, but we'll need to initialize state, etc.
constructor(props) {
super(props);
this.state = props.helloWorldData;
this.setNameDomRef = this.setNameDomRef.bind(this);
this.handleChange = this.handleChange.bind(this);
this.forceClientHydration = this.forceClientHydration.bind(this)
}

componentDidMount() {
document.addEventListener('hydrate', this.forceClientHydration);
}

componentWillUnmount() {
document.removeEventListener('hydrate', this.forceClientHydration);
}

forceClientHydration() {
const registeredComponentName = 'HelloWorldRehydratable';
const { railsContext } = this.props;

// Target all instances of the component in the DOM
const match = document.querySelectorAll(`[id^=${registeredComponentName}-react-component-]`);
// Not all browsers support forEach on NodeList so we go with a classic for-loop
for (let i = 0; i < match.length; ++i) {
const component = match[i];

// Get component specification <script> tag
let domNode = component;
while (domNode && !domNode.classList.contains('js-react-on-rails-component')) {
// Before ReactOnRails v11.0.7, component specifications where inserted before the actual component
// See https://github.com/shakacode/react_on_rails/commit/912118445f55c6f59664bf2b9f9ed97779ee71c9
// You may have to replace "nextElementSibling" by "previousElementSibling" if you use an older version
domNode = domNode.nextElementSibling;
}

if (domNode) {
// Read props from the component specification tag and merge railsContext
const mergedProps = {...JSON.parse(domNode.textContent), railsContext};
// Hydrate
ReactOnRails.render(registeredComponentName, mergedProps, component.id);
}
}
}

setNameDomRef(nameDomNode) {
this.nameDomRef = nameDomNode;
}

handleChange() {
const name = this.nameDomRef.value;
this.setState({ name });
}

render() {
const { name } = this.state;
const { railsContext } = this.props;

return (
<div>
<h3>
Hello, {name}!
</h3>
<p>
Say hello to:
<input
type="text"
ref={this.setNameDomRef}
defaultValue={name}
onChange={this.handleChange}
/>
</p>
{ railsContext && <RailsContext {...{ railsContext }} /> }
</div>
);
}
}

export default HelloWorldRehydratable;
2 changes: 2 additions & 0 deletions spec/dummy/client/app/startup/clientRegistration.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ReactOnRails from 'react-on-rails';
import HelloWorld from '../components/HelloWorld';
import HelloWorldWithLogAndThrow from '../components/HelloWorldWithLogAndThrow';
import HelloWorldES5 from '../components/HelloWorldES5';
import HelloWorldRehydratable from '../components/HelloWorldRehydratable';
import HelloWorldApp from './HelloWorldApp';
import BrokenApp from './BrokenApp';

Expand Down Expand Up @@ -40,6 +41,7 @@ ReactOnRails.register({
HelloWorld,
HelloWorldWithLogAndThrow,
HelloWorldES5,
HelloWorldRehydratable,
ReduxApp,
ReduxSharedStoreApp,
HelloWorldApp,
Expand Down
2 changes: 2 additions & 0 deletions spec/dummy/client/app/startup/serverRegistration.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import HelloString from '../non_react/HelloString';
// React components
import HelloWorld from '../components/HelloWorld';
import HelloWorldES5 from '../components/HelloWorldES5';
import HelloWorldRehydratable from '../components/HelloWorldRehydratable';
import HelloWorldWithLogAndThrow from '../components/HelloWorldWithLogAndThrow';

// Generator function
Expand Down Expand Up @@ -49,6 +50,7 @@ ReactOnRails.register({
HelloWorld,
HelloWorldWithLogAndThrow,
HelloWorldES5,
HelloWorldRehydratable,
ReduxApp,
ReduxSharedStoreApp,
HelloWorldApp,
Expand Down
2 changes: 1 addition & 1 deletion spec/dummy/config/initializers/assets.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@

# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
# Rails.application.config.assets.precompile += %w( search.js )
Rails.application.config.assets.precompile += %w[jquery_rails_manifest.js]
1 change: 1 addition & 0 deletions spec/dummy/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
get "css_modules_images_fonts_example" => "pages#css_modules_images_fonts_example"
get "turbolinks_cache_disabled" => "pages#turbolinks_cache_disabled"
get "rendered_html" => "pages#rendered_html"
get "xhr_refresh" => "pages#xhr_refresh"
get "react_helmet" => "pages#react_helmet"
get "broken_app" => "pages#broken_app"
get "image_example" => "pages#image_example"
Expand Down
27 changes: 27 additions & 0 deletions spec/dummy/spec/system/integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ def change_text_expect_dom_selector(dom_selector)
end
end

def wait_for_ajax
Timeout.timeout(Capybara.default_max_wait_time) do
loop until finished_all_ajax_requests?
end
end

def finished_all_ajax_requests?
page.evaluate_script("jQuery.active").zero?
end

shared_examples "React Component" do |dom_selector|
scenario { is_expected.to have_css dom_selector }

Expand Down Expand Up @@ -189,6 +199,23 @@ def change_text_expect_dom_selector(dom_selector)
end
end

feature "Manual client hydration", :js, type: :system do
subject { page }
background { visit "/xhr_refresh" }
scenario "HelloWorldRehydratable onChange should trigger" do
within("form") do
click_button "refresh"
end
wait_for_ajax
within("#HelloWorldRehydratable-react-component-1") do
find("input").set "Should update"
within("h3") do
is_expected.to have_content "Should update"
end
end
end
end

feature "returns hash if hash_result == true even with prerendering error", :js, type: :system do
subject { page }
background do
Expand Down
1 change: 1 addition & 0 deletions spec/dummy_no_webpacker/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
get "css_modules_images_fonts_example" => "pages#css_modules_images_fonts_example"
get "turbolinks_cache_disabled" => "pages#turbolinks_cache_disabled"
get "rendered_html" => "pages#rendered_html"
get "xhr_refresh" => "pages#xhr_refresh"
get "react_helmet" => "pages#react_helmet"
get "broken_app" => "pages#broken_app"
get "image_example" => "pages#image_example"
Expand Down