Skip to content

Commit

Permalink
Merge pull request #689 from shakacode/justin800/allow-router-result-…
Browse files Browse the repository at this point in the history
…to-return-html-string

Added property renderedHtml to return gen func
  • Loading branch information
justin808 committed Jan 27, 2017
2 parents 3f84aa8 + 4cf969a commit c5e968d
Show file tree
Hide file tree
Showing 19 changed files with 203 additions and 17 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ On production deployments that use asset precompilation, such as Heroku deployme
If you have used the provided generator, these bundles will automatically be added to your `.gitignore` to prevent extraneous noise from re-generated code in your pull requests. You will want to do this manually if you do not use the provided generator.

### Rails Context
When you use a "generator function" to create react components or you used shared redux stores, you get 2 params passed to your function:
When you use a "generator function" to create react components (or renderedHtml on the server) or you used shared redux stores, you get 2 params passed to your function:

1. Props that you pass in the view helper of either `react_component` or `redux_store`
2. Rails contextual information, such as the current pathname. You can customize this in your config file.
Expand Down Expand Up @@ -319,6 +319,8 @@ If you do want different code to run, you'd setup a separate webpack compilation
#### Generator Functions
Why would you create a function that returns a React component? For example, you may want the ability to use the passed-in props to initialize a redux store or setup react-router. Or you may want to return different components depending on what's in the props. ReactOnRails will automatically detect a registered generator function.

Another reason to user a generator function is that sometimes in server rendering, specifically with React Router, you need to return the result of calling ReactDOMServer.renderToString(element). You can do this by returning an object with the following shape: { renderedHtml, redirectLocation, error }.

#### Renderer Functions
A renderer function is a generator function that accepts three arguments: `(props, railsContext, domNodeId) => { ... }`. Instead of returning a React component, a renderer is responsible for calling `ReactDOM.render` to manually render a React component into the dom. Why would you want to call `ReactDOM.render` yourself? One possible use case is [code splitting](docs/additional-reading/code-splitting.md).

Expand All @@ -341,7 +343,7 @@ react_component(component_name,
html_options: {})
```
+ **component_name:** Can be a React component, created using a ES6 class, or `React.createClass`, a generator function that returns a React component, or a renderer function that manually renders a React component to the dom (client side only).
+ **component_name:** Can be a React component, created using a ES6 class, or `React.createClass`, a generator function that returns a React component (or only on the server side, an object with shape { redirectLocation, error, renderedHtml }), or a renderer function that manually renders a React component to the dom (client side only).
+ **options:**
+ **props:** Ruby Hash which contains the properties to pass to the react object, or a JSON string. If you pass a string, we'll escape it for you.
+ **prerender:** enable server-side rendering of component. Set to false when debugging!
Expand Down
65 changes: 65 additions & 0 deletions docs/additional-reading/react-router.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,68 @@ For a fleshed out integration of react_on_rails with react-router, check out [Re
* [react-webpack-rails-tutorial/client/app/bundles/comments/startup/ClientRouterApp.jsx](https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/client/app/bundles/comments/startup/ClientRouterApp.jsx)

* [react-webpack-rails-tutorial/client/app/bundles/comments/startup/ServerRouterApp.jsx](https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/client/app/bundles/comments/startup/ServerRouterApp.jsx)


# Server Rendering Using React Router V4

Your generator function may not return an object with the property `renderedHtml`. Thus, you call
renderToString() and return an object with this property.

This example **only applies to server rendering** and should be only used in the server side bundle.

From the [original example in the ReactRouter docs](https://react-router.now.sh/ServerRouter)

```javascript
import React from 'react'
import { renderToString } from 'react-dom/server'
import { ServerRouter, createServerRenderContext } from 'react-router'

const ReactRouterComponent = (props, railsContext) => {

// first create a context for <ServerRouter>, it's where we keep the
// results of rendering for the second pass if necessary
const context = createServerRenderContext()
const { location } = railsContext;

// render the first time
let markup = renderToString(
<ServerRouter
location={location}
context={context}
>
<App/>
</ServerRouter>
)

// get the result
const result = context.getResult()

// the result will tell you if it redirected, if so, we ignore
// the markup and send a proper redirect.
if (result.redirect) {
return {
redirectLocation: result.redirect.pathname
};
} else {

// the result will tell you if there were any misses, if so
// we can send a 404 and then do a second render pass with
// the context to clue the <Miss> components into rendering
// this time (on the client they know from componentDidMount)
if (result.missed) {
// React on Rails does not support the 404 status code for the browser.
// res.writeHead(404)

markup = renderToString(
<ServerRouter
location={location}
context={context}
>
<App/>
</ServerRouter>
)
}
return { renderedHtml: markup };
}
}
```
4 changes: 3 additions & 1 deletion docs/api/javascript-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ The best source of docs is the main [ReactOnRails.js](../../node_package/src/Rea
```js
/**
* Main entry point to using the react-on-rails npm package. This is how Rails will be able to
* find you components for rendering.
* find you components for rendering. Components get called with props, or you may use a
* "generator function" to return a React component or an object with the following shape:
* { renderedHtml, redirectLocation, error }.
* @param components (key is component name, value is component)
*/
register(components)
Expand Down
2 changes: 1 addition & 1 deletion node_package/src/clientStartup.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import ReactDOM from 'react-dom';

import createReactElement from './createReactElement';
import isRouterResult from './isRouterResult';
import isRouterResult from './isCreateReactElementResultNonReactComponent';

const REACT_ON_RAILS_COMPONENT_CLASS_NAME = 'js-react-on-rails-component';
const REACT_ON_RAILS_STORE_CLASS_NAME = 'js-react-on-rails-store';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default function isResultNonReactComponent(reactElementOrRouterResult) {
return !!(
reactElementOrRouterResult.renderedHtml ||
reactElementOrRouterResult.redirectLocation ||
reactElementOrRouterResult.error);
}
5 changes: 0 additions & 5 deletions node_package/src/isRouterResult.js

This file was deleted.

24 changes: 17 additions & 7 deletions node_package/src/serverRenderReactComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import ReactDOMServer from 'react-dom/server';

import ComponentRegistry from './ComponentRegistry';
import createReactElement from './createReactElement';
import isRouterResult from './isRouterResult';
import isCreateReactElementResultNonReactComponent from
'./isCreateReactElementResultNonReactComponent';
import buildConsoleReplay from './buildConsoleReplay';
import handleError from './handleError';

Expand All @@ -28,20 +29,29 @@ See https://github.com/shakacode/react_on_rails#renderer-functions`);
railsContext,
});

if (isRouterResult(reactElementOrRouterResult)) {
if (isCreateReactElementResultNonReactComponent(reactElementOrRouterResult)) {
// We let the client side handle any redirect
// Set hasErrors in case we want to throw a Rails exception
hasErrors = !!reactElementOrRouterResult.routeError;

if (hasErrors) {
console.error(
`React Router ERROR: ${JSON.stringify(reactElementOrRouterResult.routeError)}`,
);
} else if (trace) {
const redirectLocation = reactElementOrRouterResult.redirectLocation;
const redirectPath = redirectLocation.pathname + redirectLocation.search;
console.log(`\
}

if (reactElementOrRouterResult.redirectLocation) {
if (trace) {
const redirectLocation = reactElementOrRouterResult.redirectLocation;
const redirectPath = redirectLocation.pathname + redirectLocation.search;
console.log(`\
ROUTER REDIRECT: ${name} to dom node with id: ${domNodeId}, redirect to ${redirectPath}`,
);
);
}
// For redirects on server rendering, we can't stop Rails from returning the same result.
// Possibly, someday, we could have the rails server redirect.
} else {
htmlResult = reactElementOrRouterResult.renderedHtml;
}
} else {
htmlResult = ReactDOMServer.renderToString(reactElementOrRouterResult);
Expand Down
17 changes: 17 additions & 0 deletions node_package/tests/serverRenderReactComponent.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ test('serverRenderReactComponent renders errors', (assert) => {
assert.ok(hasErrors, 'serverRenderReactComponent should have errors if exception thrown');
});

test('serverRenderReactComponent renders html', (assert) => {
assert.plan(3);
const expectedHtml = '<div>Hello</div>';
const X3 = () => ({ renderedHtml: expectedHtml });

ComponentStore.register({ X3 });

assert.comment('Expect to see renderedHtml');

const { html, hasErrors, renderedHtml } =
JSON.parse(serverRenderReactComponent({ name: 'X3', domNodeId: 'myDomId', trace: false }));

assert.ok(html === expectedHtml, 'serverRenderReactComponent HTML should render renderedHtml value');
assert.ok(!hasErrors, 'serverRenderReactComponent should not have errors if no exception thrown');
assert.ok(!hasErrors, 'serverRenderReactComponent should have errors if exception thrown');
});

test('serverRenderReactComponent renders an error if attempting to render a renderer', (assert) => {
assert.plan(1);
const X3 = (a1, a2, a3) => null;
Expand Down
6 changes: 6 additions & 0 deletions spec/dummy/Procfile
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
rails: REACT_ON_RAILS_ENV=HOT rails s -b 0.0.0.0

# Build client assets, watching for changes.
rails-client-assets: rm app/assets/webpack/* || true && npm run build:dev:client

# Build server assets, watching for changes. Remove if not server rendering.
rails-server-assets: npm run build:dev:server
3 changes: 3 additions & 0 deletions spec/dummy/app/views/pages/_header.erb
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,8 @@
<li>
<%= link_to "Turbolinks Cache Disabled Example", turbolinks_cache_disabled_path %>
</li>
<li>
<%= link_to "Generator function returns object with renderedHtml", rendered_html_path %>
</li>
</ul>
<hr/>
8 changes: 8 additions & 0 deletions spec/dummy/app/views/pages/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ This page demonstrates a few things the other pages do not show:
<%= react_component("HelloWorldApp", props: @app_props_hello_again, prerender: false, trace: true, id: "HelloWorldApp-react-component-3") %>
<hr/>

<h1>Component that returns string html on server</h1>
<pre>
<%%= react_component("HelloWorld", props: @app_props_hello, prerender: false, trace: true, id: "HelloWorld-react-component-4") %>
<%%= react_component("HelloES5", props: @app_props_hello, prerender: false, trace: true, id: "HelloES5-react-component-5") %>
</pre>
<%= react_component("RenderedHtml", prerender: true, trace: true, id: "HelloWorld-react-component-4") %>
<hr/>

<h1>Simple Component Without Redux</h1>
<pre>
<%%= react_component("HelloWorld", props: @app_props_hello, prerender: false, trace: true, id: "HelloWorld-react-component-4") %>
Expand Down
7 changes: 7 additions & 0 deletions spec/dummy/app/views/pages/rendered_html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<%= render "header" %>
<%= react_component("RenderedHtml", prerender: true, props: { hello: "world" }, trace: true) %>

<hr/>

This page demonstrates a component that returns renderToString on the server side.
9 changes: 9 additions & 0 deletions spec/dummy/client/app/components/EchoProps.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';

const EchoProps = (props) => (
<div>
Props: {JSON.stringify(props)}
</div>
);

export default EchoProps
17 changes: 17 additions & 0 deletions spec/dummy/client/app/startup/ClientRenderedHtml.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Top level component for simple client side only rendering
import React from 'react';

import EchoProps from '../components/EchoProps';

/*
* 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.
* Note, this is imported as "HelloWorldApp" by "clientRegistration.jsx"
*
* Note, this is a fictional example, as you'd only use a generator function if you wanted to run
* some extra code, such as setting up Redux and React-Router.
*/
export default (props, railsContext) => (
<EchoProps {...props} />
);
21 changes: 21 additions & 0 deletions spec/dummy/client/app/startup/ServerRenderedHtml.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Top level component for simple client side only rendering
import React from 'react';
import { renderToString } from 'react-dom/server'
import EchoProps from '../components/EchoProps';

/*
* Export a function that takes the props and returns an object with { renderedHtml }
* Note, this is imported as "RenderedHtml" by "serverRegistration.jsx"
*
* Note, this is a fictional example, as you'd only use a generator function if you wanted to run
* some extra code, such as setting up Redux and React-Router.
*
* And the use of renderToString would probably be done with react-router v4
*
*/
export default (props, railsContext) => {
const renderedHtml = renderToString(
<EchoProps {...props} />
);
return { renderedHtml };
};
6 changes: 5 additions & 1 deletion spec/dummy/client/app/startup/clientRegistration.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import DeferredRenderApp from './DeferredRenderAppRenderer';

import SharedReduxStore from '../stores/SharedReduxStore';

// Deferred render on the client side w/ server render
import RenderedHtml from './ClientRenderedHtml';

ReactOnRails.setOptions({
traceTurbolinks: true,
});
Expand All @@ -32,7 +35,8 @@ ReactOnRails.register({
CssModulesImagesFontsExample,
ManualRenderApp,
DeferredRenderApp,
CacheDisabled
CacheDisabled,
RenderedHtml,
});

ReactOnRails.registerStore({
Expand Down
4 changes: 4 additions & 0 deletions spec/dummy/client/app/startup/serverRegistration.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import SharedReduxStore from '../stores/SharedReduxStore';
// Deferred render on the client side w/ server render
import DeferredRenderApp from './DeferredRenderAppServer';

// Deferred render on the client side w/ server render
import RenderedHtml from './ServerRenderedHtml';

ReactOnRails.register({
HelloWorld,
HelloWorldWithLogAndThrow,
Expand All @@ -41,6 +44,7 @@ ReactOnRails.register({
PureComponent,
CssModulesImagesFontsExample,
DeferredRenderApp,
RenderedHtml,
});

ReactOnRails.registerStore({
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 @@ -28,4 +28,5 @@
get "pure_component" => "pages#pure_component"
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"
end
9 changes: 9 additions & 0 deletions spec/dummy/spec/features/integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,15 @@ def change_text_expect_dom_selector(dom_selector)
end
end

feature "renderedHtml from generator function", :js do
subject { page }
background { visit "/rendered_html" }
scenario "renderedHtml should not have any errors" do
expect(subject).to have_text "Props: {\"hello\":\"world\"}"
expect(subject.html).to include("[SERVER] RENDERED RenderedHtml to dom node with id")
end
end

shared_examples "React Component Shared Store" do |url|
subject { page }
background { visit url }
Expand Down

0 comments on commit c5e968d

Please sign in to comment.