Skip to content

Commit

Permalink
Doc/xhr hydration examples (#1095)
Browse files Browse the repository at this point in the history
* Document hydration for XHR-substituted components
* Add PR reference to CHANGELOG.md
* Add missing route to dummy_no_webpacker
  • Loading branch information
hchevalier authored and justin808 committed Jun 1, 2018
1 parent 73cf07e commit 047f229
Show file tree
Hide file tree
Showing 15 changed files with 223 additions and 6 deletions.
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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,10 @@ 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
adding meta-tags to a page. It is exactly like react_component except for the following:
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') %>
70 changes: 70 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,70 @@
<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 asynchronous refresh of component-container content.<br/>
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" javascript event dispacthed from xhr_refresh.js.erb<br />
</p>

<hr/>
<h2>Setup</h2>
<ol>
<li>
Create component source: spec/dummy/client/app/components/HellowWorldRehydratable.jsx
</li>
<li>
Expose the HellowWorldRehydratable 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 HellowWorldRehydratable from '../components/HellowWorldRehydratable';
import ReactOnRails from 'react-on-rails';
ReactOnRails.register({ HellowWorldRehydratable });
</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("HellowWorldRehydratable", props: { helloWorldData: { name: 'HelloWorld' } }, prerender: true, trace: true, id: "HellowWorldRehydratable-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 HellowWorldRehydratable event handler
<br/>
<pre style='white-space: pre-wrap; word-break: keep-all;'>
var container = document.getElementById('component-container');
<%% new_component = react_component("HellowWorldRehydratable", props: { helloWorldData: { name: 'HelloWorld' } }, prerender: true, trace: true, id: "HellowWorldRehydratable-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
83 changes: 83 additions & 0 deletions spec/dummy/client/app/components/HelloWorldRehydratable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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.forceClientHydration = this.forceClientHydration.bind(this);
this.handleChange = this.handleChange.bind(this);
}

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

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

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

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 += 1) {
const component = match[i];
// Get component specification <script> tag
const componentSpecificationTag = document.querySelector(`script[data-dom-id=${component.id}]`);
// Read props from the component specification tag and merge railsContext
const mergedProps = { ...JSON.parse(componentSpecificationTag.textContent), railsContext };
// Hydrate
ReactOnRails.render(registeredComponentName, mergedProps, component.id);
}
}

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
10 changes: 5 additions & 5 deletions spec/dummy/client/webpack.client.base.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,12 @@ module.exports = {

{
test: require.resolve('jquery'),
use: {
loader: 'expose-loader',
options: {
jQuery: true,
use: [
{
loader: 'expose-loader',
options: 'jQuery'
},
},
],
},
],
},
Expand Down
5 changes: 5 additions & 0 deletions spec/dummy/client/webpack.client.rails.build.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ if (devBuild) {
}

module.exports = merge(config, {
entry: {
'vendor-bundle': [
'jquery-ujs',
],
},

output: {
filename: isHMR ? '[name]-[hash].js' : '[name]-[chunkhash].js',
Expand Down
3 changes: 3 additions & 0 deletions spec/dummy/client/webpack.client.rails.hot.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ module.exports = merge.strategy(
devtool: 'eval-source-map',

entry: {
'vendor-bundle': [
'jquery-ujs',
],
'app-bundle': [
'react-hot-loader/patch',
`webpack-dev-server/client?http://${settings.dev_server.host}:${settings.dev_server.port}`,
Expand Down
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

0 comments on commit 047f229

Please sign in to comment.