Skip to content

Commit 4cf969a

Browse files
committed
Added property renderedHtml to return gen func
This is to support React Router v4, which needs the result of calling renderToString.
1 parent 4e567a3 commit 4cf969a

19 files changed

+203
-17
lines changed

README.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ On production deployments that use asset precompilation, such as Heroku deployme
217217
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.
218218
219219
### Rails Context
220-
When you use a "generator function" to create react components or you used shared redux stores, you get 2 params passed to your function:
220+
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:
221221
222222
1. Props that you pass in the view helper of either `react_component` or `redux_store`
223223
2. Rails contextual information, such as the current pathname. You can customize this in your config file.
@@ -318,6 +318,8 @@ If you do want different code to run, you'd setup a separate webpack compilation
318318
#### Generator Functions
319319
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.
320320

321+
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 }.
322+
321323
#### Renderer Functions
322324
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).
323325

@@ -340,7 +342,7 @@ react_component(component_name,
340342
html_options: {})
341343
```
342344

343-
+ **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).
345+
+ **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).
344346
+ **options:**
345347
+ **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.
346348
+ **prerender:** enable server-side rendering of component. Set to false when debugging!

docs/additional-reading/react-router.md

+65
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,68 @@ For a fleshed out integration of react_on_rails with react-router, check out [Re
4646
* [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)
4747

4848
* [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)
49+
50+
51+
# Server Rendering Using React Router V4
52+
53+
Your generator function may not return an object with the property `renderedHtml`. Thus, you call
54+
renderToString() and return an object with this property.
55+
56+
This example **only applies to server rendering** and should be only used in the server side bundle.
57+
58+
From the [original example in the ReactRouter docs](https://react-router.now.sh/ServerRouter)
59+
60+
```javascript
61+
import React from 'react'
62+
import { renderToString } from 'react-dom/server'
63+
import { ServerRouter, createServerRenderContext } from 'react-router'
64+
65+
const ReactRouterComponent = (props, railsContext) => {
66+
67+
// first create a context for <ServerRouter>, it's where we keep the
68+
// results of rendering for the second pass if necessary
69+
const context = createServerRenderContext()
70+
const { location } = railsContext;
71+
72+
// render the first time
73+
let markup = renderToString(
74+
<ServerRouter
75+
location={location}
76+
context={context}
77+
>
78+
<App/>
79+
</ServerRouter>
80+
)
81+
82+
// get the result
83+
const result = context.getResult()
84+
85+
// the result will tell you if it redirected, if so, we ignore
86+
// the markup and send a proper redirect.
87+
if (result.redirect) {
88+
return {
89+
redirectLocation: result.redirect.pathname
90+
};
91+
} else {
92+
93+
// the result will tell you if there were any misses, if so
94+
// we can send a 404 and then do a second render pass with
95+
// the context to clue the <Miss> components into rendering
96+
// this time (on the client they know from componentDidMount)
97+
if (result.missed) {
98+
// React on Rails does not support the 404 status code for the browser.
99+
// res.writeHead(404)
100+
101+
markup = renderToString(
102+
<ServerRouter
103+
location={location}
104+
context={context}
105+
>
106+
<App/>
107+
</ServerRouter>
108+
)
109+
}
110+
return { renderedHtml: markup };
111+
}
112+
}
113+
```

docs/api/javascript-api.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ The best source of docs is the main [ReactOnRails.js](../../node_package/src/Rea
44
```js
55
/**
66
* Main entry point to using the react-on-rails npm package. This is how Rails will be able to
7-
* find you components for rendering.
7+
* find you components for rendering. Components get called with props, or you may use a
8+
* "generator function" to return a React component or an object with the following shape:
9+
* { renderedHtml, redirectLocation, error }.
810
* @param components (key is component name, value is component)
911
*/
1012
register(components)

node_package/src/clientStartup.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import ReactDOM from 'react-dom';
44

55
import createReactElement from './createReactElement';
6-
import isRouterResult from './isRouterResult';
6+
import isRouterResult from './isCreateReactElementResultNonReactComponent';
77

88
const REACT_ON_RAILS_COMPONENT_CLASS_NAME = 'js-react-on-rails-component';
99
const REACT_ON_RAILS_STORE_CLASS_NAME = 'js-react-on-rails-store';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default function isResultNonReactComponent(reactElementOrRouterResult) {
2+
return !!(
3+
reactElementOrRouterResult.renderedHtml ||
4+
reactElementOrRouterResult.redirectLocation ||
5+
reactElementOrRouterResult.error);
6+
}

node_package/src/isRouterResult.js

-5
This file was deleted.

node_package/src/serverRenderReactComponent.js

+17-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import ReactDOMServer from 'react-dom/server';
22

33
import ComponentRegistry from './ComponentRegistry';
44
import createReactElement from './createReactElement';
5-
import isRouterResult from './isRouterResult';
5+
import isCreateReactElementResultNonReactComponent from
6+
'./isCreateReactElementResultNonReactComponent';
67
import buildConsoleReplay from './buildConsoleReplay';
78
import handleError from './handleError';
89

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

31-
if (isRouterResult(reactElementOrRouterResult)) {
32+
if (isCreateReactElementResultNonReactComponent(reactElementOrRouterResult)) {
3233
// We let the client side handle any redirect
3334
// Set hasErrors in case we want to throw a Rails exception
3435
hasErrors = !!reactElementOrRouterResult.routeError;
36+
3537
if (hasErrors) {
3638
console.error(
3739
`React Router ERROR: ${JSON.stringify(reactElementOrRouterResult.routeError)}`,
3840
);
39-
} else if (trace) {
40-
const redirectLocation = reactElementOrRouterResult.redirectLocation;
41-
const redirectPath = redirectLocation.pathname + redirectLocation.search;
42-
console.log(`\
41+
}
42+
43+
if (reactElementOrRouterResult.redirectLocation) {
44+
if (trace) {
45+
const redirectLocation = reactElementOrRouterResult.redirectLocation;
46+
const redirectPath = redirectLocation.pathname + redirectLocation.search;
47+
console.log(`\
4348
ROUTER REDIRECT: ${name} to dom node with id: ${domNodeId}, redirect to ${redirectPath}`,
44-
);
49+
);
50+
}
51+
// For redirects on server rendering, we can't stop Rails from returning the same result.
52+
// Possibly, someday, we could have the rails server redirect.
53+
} else {
54+
htmlResult = reactElementOrRouterResult.renderedHtml;
4555
}
4656
} else {
4757
htmlResult = ReactDOMServer.renderToString(reactElementOrRouterResult);

node_package/tests/serverRenderReactComponent.test.js

+17
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,23 @@ test('serverRenderReactComponent renders errors', (assert) => {
3838
assert.ok(hasErrors, 'serverRenderReactComponent should have errors if exception thrown');
3939
});
4040

41+
test('serverRenderReactComponent renders html', (assert) => {
42+
assert.plan(3);
43+
const expectedHtml = '<div>Hello</div>';
44+
const X3 = () => ({ renderedHtml: expectedHtml });
45+
46+
ComponentStore.register({ X3 });
47+
48+
assert.comment('Expect to see renderedHtml');
49+
50+
const { html, hasErrors, renderedHtml } =
51+
JSON.parse(serverRenderReactComponent({ name: 'X3', domNodeId: 'myDomId', trace: false }));
52+
53+
assert.ok(html === expectedHtml, 'serverRenderReactComponent HTML should render renderedHtml value');
54+
assert.ok(!hasErrors, 'serverRenderReactComponent should not have errors if no exception thrown');
55+
assert.ok(!hasErrors, 'serverRenderReactComponent should have errors if exception thrown');
56+
});
57+
4158
test('serverRenderReactComponent renders an error if attempting to render a renderer', (assert) => {
4259
assert.plan(1);
4360
const X3 = (a1, a2, a3) => null;

spec/dummy/Procfile

+6
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
rails: REACT_ON_RAILS_ENV=HOT rails s -b 0.0.0.0
2+
3+
# Build client assets, watching for changes.
4+
rails-client-assets: rm app/assets/webpack/* || true && npm run build:dev:client
5+
6+
# Build server assets, watching for changes. Remove if not server rendering.
7+
rails-server-assets: npm run build:dev:server

spec/dummy/app/views/pages/_header.erb

+3
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,8 @@
7474
<li>
7575
<%= link_to "Turbolinks Cache Disabled Example", turbolinks_cache_disabled_path %>
7676
</li>
77+
<li>
78+
<%= link_to "Generator function returns object with renderedHtml", rendered_html_path %>
79+
</li>
7780
</ul>
7881
<hr/>

spec/dummy/app/views/pages/index.html.erb

+8
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ This page demonstrates a few things the other pages do not show:
6969
<%= react_component("HelloWorldApp", props: @app_props_hello_again, prerender: false, trace: true, id: "HelloWorldApp-react-component-3") %>
7070
<hr/>
7171

72+
<h1>Component that returns string html on server</h1>
73+
<pre>
74+
<%%= react_component("HelloWorld", props: @app_props_hello, prerender: false, trace: true, id: "HelloWorld-react-component-4") %>
75+
<%%= react_component("HelloES5", props: @app_props_hello, prerender: false, trace: true, id: "HelloES5-react-component-5") %>
76+
</pre>
77+
<%= react_component("RenderedHtml", prerender: true, trace: true, id: "HelloWorld-react-component-4") %>
78+
<hr/>
79+
7280
<h1>Simple Component Without Redux</h1>
7381
<pre>
7482
<%%= react_component("HelloWorld", props: @app_props_hello, prerender: false, trace: true, id: "HelloWorld-react-component-4") %>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<%= render "header" %>
2+
3+
<%= react_component("RenderedHtml", prerender: true, props: { hello: "world" }, trace: true) %>
4+
5+
<hr/>
6+
7+
This page demonstrates a component that returns renderToString on the server side.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react';
2+
3+
const EchoProps = (props) => (
4+
<div>
5+
Props: {JSON.stringify(props)}
6+
</div>
7+
);
8+
9+
export default EchoProps
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Top level component for simple client side only rendering
2+
import React from 'react';
3+
4+
import EchoProps from '../components/EchoProps';
5+
6+
/*
7+
* Export a function that takes the props and returns a ReactComponent.
8+
* This is used for the client rendering hook after the page html is rendered.
9+
* React will see that the state is the same and not do anything.
10+
* Note, this is imported as "HelloWorldApp" by "clientRegistration.jsx"
11+
*
12+
* Note, this is a fictional example, as you'd only use a generator function if you wanted to run
13+
* some extra code, such as setting up Redux and React-Router.
14+
*/
15+
export default (props, railsContext) => (
16+
<EchoProps {...props} />
17+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Top level component for simple client side only rendering
2+
import React from 'react';
3+
import { renderToString } from 'react-dom/server'
4+
import EchoProps from '../components/EchoProps';
5+
6+
/*
7+
* Export a function that takes the props and returns an object with { renderedHtml }
8+
* Note, this is imported as "RenderedHtml" by "serverRegistration.jsx"
9+
*
10+
* Note, this is a fictional example, as you'd only use a generator function if you wanted to run
11+
* some extra code, such as setting up Redux and React-Router.
12+
*
13+
* And the use of renderToString would probably be done with react-router v4
14+
*
15+
*/
16+
export default (props, railsContext) => {
17+
const renderedHtml = renderToString(
18+
<EchoProps {...props} />
19+
);
20+
return { renderedHtml };
21+
};

spec/dummy/client/app/startup/clientRegistration.jsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import DeferredRenderApp from './DeferredRenderAppRenderer';
1616

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

19+
// Deferred render on the client side w/ server render
20+
import RenderedHtml from './ClientRenderedHtml';
21+
1922
ReactOnRails.setOptions({
2023
traceTurbolinks: true,
2124
});
@@ -32,7 +35,8 @@ ReactOnRails.register({
3235
CssModulesImagesFontsExample,
3336
ManualRenderApp,
3437
DeferredRenderApp,
35-
CacheDisabled
38+
CacheDisabled,
39+
RenderedHtml,
3640
});
3741

3842
ReactOnRails.registerStore({

spec/dummy/client/app/startup/serverRegistration.jsx

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ import SharedReduxStore from '../stores/SharedReduxStore';
2929
// Deferred render on the client side w/ server render
3030
import DeferredRenderApp from './DeferredRenderAppServer';
3131

32+
// Deferred render on the client side w/ server render
33+
import RenderedHtml from './ServerRenderedHtml';
34+
3235
ReactOnRails.register({
3336
HelloWorld,
3437
HelloWorldWithLogAndThrow,
@@ -41,6 +44,7 @@ ReactOnRails.register({
4144
PureComponent,
4245
CssModulesImagesFontsExample,
4346
DeferredRenderApp,
47+
RenderedHtml,
4448
});
4549

4650
ReactOnRails.registerStore({

spec/dummy/config/routes.rb

+1
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@
2828
get "pure_component" => "pages#pure_component"
2929
get "css_modules_images_fonts_example" => "pages#css_modules_images_fonts_example"
3030
get "turbolinks_cache_disabled" => "pages#turbolinks_cache_disabled"
31+
get "rendered_html" => "pages#rendered_html"
3132
end

spec/dummy/spec/features/integration_spec.rb

+9
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,15 @@ def change_text_expect_dom_selector(dom_selector)
176176
end
177177
end
178178

179+
feature "renderedHtml from generator function", :js do
180+
subject { page }
181+
background { visit "/rendered_html" }
182+
scenario "renderedHtml should not have any errors" do
183+
expect(subject).to have_text "Props: {\"hello\":\"world\"}"
184+
expect(subject.html).to include("[SERVER] RENDERED RenderedHtml to dom node with id")
185+
end
186+
end
187+
179188
shared_examples "React Component Shared Store" do |url|
180189
subject { page }
181190
background { visit url }

0 commit comments

Comments
 (0)