diff --git a/README.md b/README.md index 4cd6bf7c9d..26d3726ed5 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ 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 @@ -53,23 +53,22 @@ Contributions and pull requests welcome! 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 @@ -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 diff --git a/app/helpers/react_on_rails_helper.rb b/app/helpers/react_on_rails_helper.rb index 57fbda0e2d..e920b8adb1 100644 --- a/app/helpers/react_on_rails_helper.rb +++ b/app/helpers/react_on_rails_helper.rb @@ -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 @@ -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)}); JS server_rendered_react_component_html = render_js(render_js_expression) else @@ -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} @@ -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) + 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 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 diff --git a/lib/react_on_rails/react_renderer.rb b/lib/react_on_rails/react_renderer.rb index 2717c7fe7c..c8db262d5f 100644 --- a/lib/react_on_rails/react_renderer.rb +++ b/lib/react_on_rails/react_renderer.rb @@ -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. + # 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) <<-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 diff --git a/spec/dummy/app/views/pages/index.html.erb b/spec/dummy/app/views/pages/index.html.erb index 762fdcd165..31f03bc45f 100644 --- a/spec/dummy/app/views/pages/index.html.erb +++ b/spec/dummy/app/views/pages/index.html.erb @@ -1,24 +1,6 @@ <%= render "header" %>

React Rails Server Rendering

- -
- -

Simple Client Rendered Component

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

Showing you can put the same component twice on a page with different props

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

Server Rendered/Cached React/Redux Component

@@ -28,6 +10,7 @@
WARNING: be sure to clear the cache by opening a console and running Rails.cache.clear <% end %>

+ <%% cache @app_props_server_render do %>
<%% = react_component("App", @app_props_server_render, trace: true) %>
@@ -39,16 +22,52 @@ <% cache @app_props_server_render do %> <% puts "=" * 80 %> - <% puts "server rendering react component" %> + <% puts "server rendering react components" %> <% puts "=" * 80 %> <%= react_component("App", @app_props_server_render, trace: true) %> +
+ +

Server Rendered/Cached React Component Without Redux

+ + <%% cache @app_props_server_render do %>
+ <%% = react_component("HelloWorld", @app_props_server_render, trace: true) %>
+ <%% end %> +
+

+ And this is an example of a server rendered React component without Redux +

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

Simple Client Rendered Component

+ + + <%%= react_component("HelloWorldApp", @app_props_hello, prerender: false, trace: true) %> + +<%= react_component("HelloWorldApp", @app_props_hello, prerender: false, trace: true) %> +
+

Showing you can put the same component twice on a page with different props

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

Simple Component Without Redux

+ + <%%= react_component("HelloWorld", @app_props_hello, prerender: false, trace: true) %> + <%%= react_component("HelloES5", @app_props_hello, prerender: false, trace: true) %> + +<%= react_component("HelloWorld", @app_props_hello, prerender: false, trace: true) %> +<%= react_component("HelloES5", @app_props_hello, prerender: false, trace: true) %>
+

Non-React Component

-For example, Suppose you have some JavaScript that generates a string of HTML: -
+

For example, Suppose you have some JavaScript that generates a string of HTML:

this.HelloString.world() diff --git a/spec/dummy/client/app/components/HelloES5.jsx b/spec/dummy/client/app/components/HelloES5.jsx new file mode 100644 index 0000000000..e2051efa36 --- /dev/null +++ b/spec/dummy/client/app/components/HelloES5.jsx @@ -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 ( +
+

+ Hello ES5, {name}! +

+

+ Say hello to: + +

+
+ ); + }, +}); + +export default HelloES5; diff --git a/spec/dummy/client/app/startup/ClientApp.jsx b/spec/dummy/client/app/startup/ClientApp.jsx index f1568fc829..1e4f092428 100644 --- a/spec/dummy/client/app/startup/ClientApp.jsx +++ b/spec/dummy/client/app/startup/ClientApp.jsx @@ -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. @@ -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; diff --git a/spec/dummy/client/app/startup/ClientHelloWorldComponent.jsx b/spec/dummy/client/app/startup/ClientHelloWorldApp.jsx similarity index 52% rename from spec/dummy/client/app/startup/ClientHelloWorldComponent.jsx rename to spec/dummy/client/app/startup/ClientHelloWorldApp.jsx index 8ef9628a4f..40b73a170e 100644 --- a/spec/dummy/client/app/startup/ClientHelloWorldComponent.jsx +++ b/spec/dummy/client/app/startup/ClientHelloWorldApp.jsx @@ -1,7 +1,5 @@ // Top level component for simple client side only rendering - import React from 'react'; - import HelloWorld from '../components/HelloWorld'; /* @@ -9,6 +7,14 @@ import HelloWorld from '../components/HelloWorld'; * 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 ; }; + +/* + * 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; diff --git a/spec/dummy/client/app/startup/ServerApp.jsx b/spec/dummy/client/app/startup/ServerApp.jsx index 60327c89ae..ca9215dae3 100644 --- a/spec/dummy/client/app/startup/ServerApp.jsx +++ b/spec/dummy/client/app/startup/ServerApp.jsx @@ -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); @@ -29,3 +32,6 @@ export default props => { ); }; + +// This is an example of how to render a React component directly, without using Redux +export { HelloWorld, HelloES5 }; diff --git a/spec/dummy/client/app/startup/serverGlobals.jsx b/spec/dummy/client/app/startup/serverGlobals.jsx index 44fc826465..2d0748539a 100644 --- a/spec/dummy/client/app/startup/serverGlobals.jsx +++ b/spec/dummy/client/app/startup/serverGlobals.jsx @@ -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'; +/* + * 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"); diff --git a/spec/dummy/client/webpack.client.js b/spec/dummy/client/webpack.client.js index 47fb3dd77e..d7af6aae61 100644 --- a/spec/dummy/client/webpack.client.js +++ b/spec/dummy/client/webpack.client.js @@ -3,7 +3,7 @@ const path = require('path'); module.exports = { entry: [ 'startup/ClientApp', - 'startup/ClientHelloWorldComponent', + 'startup/ClientHelloWorldApp', ], output: { path: '../app/assets/javascripts/generated',