Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,32 +44,31 @@ Contributions and pull requests welcome!
cd spec/dummy
bundle
npm i
foreman start
```
foreman start
```
2. Caching is turned for development mode. Open the console and run `Rails.cache.clear` to clear
the cache. Note, even if you stop the server, you'll still have the cache entries around.
3. Visit http://localhost:3000
4. Notice that the first time you hit the page, you'll see a message that server is rendering.
See `spec/dummy/app/views/pages/index.html.erb:17` for the generation of that message.
5. Open up the browser console and see some tracing.
6. Open up the source for the page and see the server rendered code.
7. If you want to turn off server caching, run the server like:
7. If you want to turn off server caching, run the server like:
`export RAILS_USE_CACHE=N && foreman start`
8. If you click back and forth between the about and react page links, you can see the rails console
log as well as the browser console to see what's going on with regards to server rendering and
caching.

# Key Tips
1. See sample app in `spec/dummy` for how to set this up.
1. See sample app in `spec/dummy` for how to set this up.
2. The file used for server rendering is hard coded as `generated/server.js`
(assets/javascripts/generated/server.js).
3. If you're only doing client rendering, you still *MUST* create an empty version of this file. This
will soon change so that this is not necessary.
3. The default for rendering right now is `prerender: false`. **NOTE:** Server side rendering does
not work for some components, namely react-router, that use an async setup for server rendering.
You can configure the default for prerender in your config.
4. The API for objects exposed differs from the react-rails gem in that you expose a function that
returns a react component. We'll be changing that to take either a function or a React component.
3. The default for rendering right now is `prerender: false`. **NOTE:** Server side rendering does
not work for some components, namely react-router, that use an async setup for server rendering.
You can configure the default for prerender in your config.
4. You can expose either a React component or a function that returns a React component. If you wish to create a React component via a function, rather than simply props, then you need to set the property "generator" on that function to true. When that is done, the function is invoked with a single parameter of "props", and that function should return a React element.

# Example Configuration, config/react_on_rails.rb
```ruby
Expand Down Expand Up @@ -104,7 +103,7 @@ Or install it yourself as:

## Usage

PENDING. See `spec/dummy` for the sample app.
PENDING. See `spec/dummy` for the sample app.

## Development

Expand Down
32 changes: 18 additions & 14 deletions app/helpers/react_on_rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,18 @@ def react_component(component_name, props = {}, options = {})
prerender = options.fetch(:prerender) { ReactOnRails.configuration.prerender }
trace = options.fetch(:trace, false)

dataVariable = "__#{component_name.camelize(:lower)}Data#{@react_component_index}__"
reactComponent = component_name.camelize
domId = "#{component_name}-react-component-#{@react_component_index}"
data_variable = "__#{component_name.camelize(:lower)}Data#{@react_component_index}__"
react_component_name = component_name.camelize
dom_id = "#{component_name}-react-component-#{@react_component_index}"
@react_component_index += 1

turbolinks_loaded = Object.const_defined?(:Turbolinks)
install_render_events = turbolinks_loaded ? turbolinks_bootstrap(domId) : non_turbolinks_bootstrap
install_render_events = turbolinks_loaded ? turbolinks_bootstrap(dom_id) : non_turbolinks_bootstrap

page_loaded_js = <<-JS
(function() {
window.#{dataVariable} = #{props.to_json};
#{define_render_if_dom_node_present(reactComponent, dataVariable, domId, trace)}
window.#{data_variable} = #{props.to_json};
#{define_render_if_dom_node_present(react_component_name, data_variable, dom_id, trace)}
#{install_render_events}
})();
JS
Expand All @@ -49,7 +49,7 @@ def react_component(component_name, props = {}, options = {})
# Create the HTML rendering part
if prerender
render_js_expression = <<-JS
renderReactComponent(this.#{reactComponent}, #{props.to_json})
this.React.renderToString(#{render_js_react_element(react_component_name, props.to_json)});
Copy link
Member

Choose a reason for hiding this comment

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

add comment above this line:

    # create the server generated html of the react component with props

JS
server_rendered_react_component_html = render_js(render_js_expression)
else
Expand All @@ -58,7 +58,7 @@ def react_component(component_name, props = {}, options = {})

rendered_output = content_tag(:div,
server_rendered_react_component_html,
id: domId)
id: dom_id)

<<-HTML.strip_heredoc.html_safe
#{data_from_server_script_tag}
Expand All @@ -76,24 +76,28 @@ def render_js(js_expression)

private

def debug_js(react_component, data_variable, dom_id, trace)
def debug_js(react_component_name, data_variable, dom_id, trace)
if trace
<<-JS.strip_heredoc
console.log("CLIENT SIDE RENDERED #{react_component} with dataVariable #{data_variable} to dom node with id: #{dom_id}");
console.log("CLIENT SIDE RENDERED #{react_component_name} with data_variable #{data_variable} to dom node with id: #{dom_id}");
JS
else
""
end
end

def define_render_if_dom_node_present(react_component, data_variable, dom_id, trace)
def render_js_react_element(react_component_name, props_name)
Copy link
Member

Choose a reason for hiding this comment

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

Let's change props_name to props_string b/c it's either a name or the full json version of a hash.

ReactOnRails::ReactRenderer.render_js_react_element(react_component_name, props_name)
end

def define_render_if_dom_node_present(react_component_name, data_variable, dom_id, trace)
<<-JS.strip_heredoc
Copy link
Member

Choose a reason for hiding this comment

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

Let's move this method over to lib/react_on_rails/react_renderer.rb.

Then we can reuse the common lines from 96 to 103.

The below code has the var names hard coded as reactComponent and props. The upper code uses the rails variables.

        var element;

        if (Object.getPrototypeOf(reactComponent) === this.React.Component) {
          element = this.React.createElement(reactComponent, props);
        } else {
          // when using Redux, we need to pull the component off the wrapper function
          element = reactComponent(props);
        }

var renderIfDomNodePresent = function() {
var domNode = document.getElementById('#{dom_id}');
if (domNode) {
#{debug_js(react_component, data_variable, dom_id, trace)}
var reactComponent = #{react_component}(#{data_variable});
React.render(reactComponent, domNode);
#{debug_js(react_component_name, data_variable, dom_id, trace)}
var reactElement = #{render_js_react_element(react_component_name, data_variable)};
React.render(reactElement, domNode);
}
}
JS
Expand Down
19 changes: 10 additions & 9 deletions lib/react_on_rails/react_renderer.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
module ReactOnRails
class ReactRenderer

# "this" does not need a closure as it refers to the "this" defined by the
# calling the calling context which is the "this" in the execJs environment.
def render_js_react_component
# Returns the JavaScript code to generate a React element.
Copy link
Member

Choose a reason for hiding this comment

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

Let's put back this comment.

# The parameter react_component_name can be a React component or a generator function
# that returns a React component. To be invoked as a function, react_component_name
# must have the property "generator" set to true and be a function that
# takes one parameter, props, that is used to construct the React component.
def self.render_js_react_element(react_component_name, props_name)
Copy link
Member

Choose a reason for hiding this comment

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

change props_name to props_string.

Let's put this comment:

   # props_string is either the variable name used to hold the props (client side) or the
   # stringified hash of props from the Ruby server side.

<<-JS.strip_heredoc
function renderReactComponent(componentClass, props) {
return this.React.renderToString(
componentClass(props)
);
}
#{react_component_name}.generator ?
#{react_component_name}(#{props_name}) :
this.React.createElement(#{react_component_name}, #{props_name})
JS
end

def initialize
js_code = "#{bundle_js_code};\n#{render_js_react_component}"
js_code = "#{bundle_js_code};"
@context = ExecJS.compile(js_code)
end

Expand Down
61 changes: 40 additions & 21 deletions spec/dummy/app/views/pages/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
<%= render "header" %>

<h1>React Rails Server Rendering</h1>

<hr/>

<h1>Simple Client Rendered Component</h1>
<!-- Passing prerender: false to not render on server -->
<code>
<%%= react_component("HelloWorldComponent", @app_props_hello, prerender: false, trace: true) %>
</code>
<%= react_component("HelloWorldComponent", @app_props_hello, prerender: false, trace: true) %>
<hr/>

<h1>Showing you can put the same component twice on a page with different props</h1>
<code>
<%%= react_component("HelloWorldComponent", @app_props_hello_again, prerender: false, trace: true) %>
</code>

<%= react_component("HelloWorldComponent", @app_props_hello_again, prerender: false, trace: true) %>

<hr/>

<h1>Server Rendered/Cached React/Redux Component</h1>
Expand All @@ -28,6 +10,7 @@
<br/><b>WARNING: be sure to clear the cache by opening a console and running Rails.cache.clear</b>
<% end %>
</p>

<code>
<%% cache @app_props_server_render do %><br/>
<%% = react_component("App", @app_props_server_render, trace: true) %><br/>
Expand All @@ -39,16 +22,52 @@

<% cache @app_props_server_render do %>
<% puts "=" * 80 %>
<% puts "server rendering react component" %>
<% puts "server rendering react components" %>
<% puts "=" * 80 %>
<!-- Default for prerender is true for the app, set in config/react_on_rails.rb -->
<%= react_component("App", @app_props_server_render, trace: true) %>
<hr/>

<h1>Server Rendered/Cached React Component Without Redux</h1>
<code>
<%% cache @app_props_server_render do %><br/>
<%% = react_component("HelloWorld", @app_props_server_render, trace: true) %><br/>
Copy link
Member

Choose a reason for hiding this comment

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

Maybe it's confusing that we call it HelloWorld in one case, and HelloWorldComponent in another case. Why is this done? Maybe I'm missing something. Be sure to note that ClientApp.jsx is ONLY loaded client side in the browser, and ServerApp.jsx is ONLY loaded server side by the rails server, so there would be no naming conflict.

Copy link
Member Author

Choose a reason for hiding this comment

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

@justin808 HelloWorldComponent is a function that returns the component <HelloWorld />.
Here, the HelloWorld component is directly called without the wrapper function, and loaded server side (after exported by ServerApp), for demonstration purposes.
Below, the same HelloWorld component is loaded by the client.

Copy link
Member

Choose a reason for hiding this comment

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

We should make the component generator "function" name more explicit. Like HelloWorldComponentGenerator or HelloWorldApp. @alexfedoseev What do you think? I think we might be using the suffix App to mean these generators.

Copy link
Member

Choose a reason for hiding this comment

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

👍 for App suffix

<%% end %>
</code>
<p>
And this is an example of a server rendered React component without Redux
</p>

<%= react_component("HelloWorld", @app_props_server_render, trace: true) %>
<% end %>
<hr/>

<h1>Simple Client Rendered Component</h1>
<!-- Passing prerender: false to not render on server -->
<code>
<%%= react_component("HelloWorldApp", @app_props_hello, prerender: false, trace: true) %>
</code>
<%= react_component("HelloWorldApp", @app_props_hello, prerender: false, trace: true) %>
<hr/>

<h1>Showing you can put the same component twice on a page with different props</h1>
<code>
<%%= react_component("HelloWorldApp", @app_props_hello_again, prerender: false, trace: true) %>
</code>
<%= react_component("HelloWorldApp", @app_props_hello_again, prerender: false, trace: true) %>
<hr/>

<h1>Simple Component Without Redux</h1>
<code>
<%%= react_component("HelloWorld", @app_props_hello, prerender: false, trace: true) %>
<%%= react_component("HelloES5", @app_props_hello, prerender: false, trace: true) %>
</code>
<%= react_component("HelloWorld", @app_props_hello, prerender: false, trace: true) %>
<%= react_component("HelloES5", @app_props_hello, prerender: false, trace: true) %>
<hr/>

<h1>Non-React Component</h1>
For example, Suppose you have some JavaScript that generates a string of HTML:
<br/>
<p>For example, Suppose you have some JavaScript that generates a string of HTML:</p>
<code>
this.HelloString.world()
</code>
Expand Down
32 changes: 32 additions & 0 deletions spec/dummy/client/app/components/HelloES5.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';

// Super simple example of React component using React.createClass
const HelloES5 = React.createClass({

getInitialState() {
return this.props.helloWorldData;
},

_handleChange() {
const name = React.findDOMNode(this.refs.name).value;
this.setState({name});
},

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

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

export default HelloES5;
14 changes: 14 additions & 0 deletions spec/dummy/client/app/startup/ClientApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import middleware from 'redux-thunk';

import reducers from '../reducers/reducersIndex';
import HelloWorldContainer from '../components/HelloWorldContainer';
import HelloWorld from '../components/HelloWorld';
import HelloES5 from '../components/HelloES5';

/*
* Export a function that takes the props and returns a ReactComponent.
Expand All @@ -28,3 +30,15 @@ window.App = props => {
);
return reactComponent;
};

/*
* If you wish to create a React component via a function, rather than simply props,
* then you need to set the property "generator" on that function to true.
* When that is done, the function is invoked with a single parameter of "props",
* and that function should return a react element.
*/
window.App.generator = true;

// This is an example of how to render a React component directly, without using Redux
window.HelloWorld = HelloWorld;
window.HelloES5 = HelloES5;
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
// Top level component for simple client side only rendering

import React from 'react';

import HelloWorld from '../components/HelloWorld';

/*
* Export a function that takes the props and returns a ReactComponent.
* This is used for the client rendering hook after the page html is rendered.
* React will see that the state is the same and not do anything.
*/
window.HelloWorldComponent = props => {
window.HelloWorldApp = props => {
return <HelloWorld {...props}/>;
};

/*
* If you wish to create a React component via a function, rather than simply props,
* then you need to set the property "generator" on that function to true.
* When that is done, the function is invoked with a single parameter of "props",
* and that function should return a react element.
*/
window.HelloWorldApp.generator = true;
6 changes: 6 additions & 0 deletions spec/dummy/client/app/startup/ServerApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import middleware from 'redux-thunk';

// Uses the index
import reducers from '../reducers/reducersIndex';

import HelloWorldContainer from '../components/HelloWorldContainer';
import HelloWorld from '../components/HelloWorld';
import HelloES5 from '../components/HelloES5';

export default props => {
const combinedReducer = combineReducers(reducers);
Expand All @@ -29,3 +32,6 @@ export default props => {
</Provider>
);
};

// This is an example of how to render a React component directly, without using Redux
export { HelloWorld, HelloES5 };
16 changes: 14 additions & 2 deletions spec/dummy/client/app/startup/serverGlobals.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,26 @@
// Shows the mapping from the exported object to the name used by the server rendering.
import App from './ServerApp';

// Example of server rendering without using Redux
import { HelloWorld, HelloES5 } from './ServerApp';

// Example of server rendering with no React
import HelloString from '../non_react/HelloString';

Copy link
Member

Choose a reason for hiding this comment

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

good job cleaning up out of date docs!

/*
* If you wish to create a React component via a function, rather than simply props,
* then you need to set the property "generator" on that function to true.
* When that is done, the function is invoked with a single parameter of "props",
* and that function should return a react element.
*/
App.generator = true;

// We can use the node global object for exposing.
// NodeJs: https://nodejs.org/api/globals.html#globals_global
// Uncomment next 4 lines to use global
global.HelloString = HelloString;
global.App = App;
global.HelloWorld = HelloWorld;
global.HelloES5 = HelloES5;
global.HelloString = HelloString;

// Alternative syntax for exposing Vars
// require("expose?HelloString!./non_react/HelloString.js");
Expand Down
2 changes: 1 addition & 1 deletion spec/dummy/client/webpack.client.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const path = require('path');
module.exports = {
entry: [
'startup/ClientApp',
'startup/ClientHelloWorldComponent',
'startup/ClientHelloWorldApp',
],
output: {
path: '../app/assets/javascripts/generated',
Expand Down