Skip to content

Commit e463a67

Browse files
committed
Add react-router support incl server rendering
* Updated to react_on_rails 1.1.0 * Update config to get non-minified server msgs * Support for server rendering with react-router * Implement active class for react-router links and use IndexLink and IndexRoute * cleanup code duplication
1 parent 088fac1 commit e463a67

32 files changed

+345
-69
lines changed

Diff for: Gemfile

+2-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ gem "rails-html-sanitizer"
4040
# Use Unicorn as the app server
4141
gem "unicorn"
4242

43-
gem "react_on_rails", "~> 1.0.0.pre"
43+
gem "react_on_rails"
44+
4445
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
4546
gem "therubyracer"
4647

Diff for: Gemfile.lock

+2-2
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ GEM
204204
raindrops (0.15.0)
205205
rake (10.4.2)
206206
rdoc (4.2.0)
207-
react_on_rails (1.0.3)
207+
react_on_rails (1.1.0)
208208
connection_pool
209209
execjs (~> 2.5)
210210
rails (>= 3.2)
@@ -350,7 +350,7 @@ DEPENDENCIES
350350
rails (~> 4.2)
351351
rails-html-sanitizer
352352
rails_12factor
353-
react_on_rails (~> 1.0.0.pre)
353+
react_on_rails
354354
rspec-rails
355355
rubocop
356356
ruby-lint

Diff for: app/controllers/pages_controller.rb

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
class PagesController < ApplicationController
2-
def index
3-
@comments = Comment.all
2+
before_action :set_comments
43

4+
def index
55
# NOTE: The below notes apply if you want to set the value of the props in the controller, as
66
# compared to he view. However, it's more convenient to use Jbuilder from the view. See
77
# app/views/pages/index.html.erb:20
@@ -22,6 +22,16 @@ def index
2222
# end
2323
end
2424

25+
# Declaring no_router and simple to indicate we have views for them
26+
def no_router
27+
end
28+
2529
def simple
2630
end
31+
32+
private
33+
34+
def set_comments
35+
@comments = Comment.all
36+
end
2737
end

Diff for: app/views/layouts/application.html.erb

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
<!-- Collect the nav links, forms, and other content for toggling -->
2323
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
2424
<ul class="nav navbar-nav">
25-
<li class="active"><%= link_to "React Demo", root_path %></li>
25+
<li class="active"><%= link_to "React Router Demo", root_path %></li>
26+
<li><%= link_to "React Demo", no_router_path %></li>
2627
<li><%= link_to "Simple React", simple_path %></li>
2728
<li><%= link_to "Classic Rails", comments_path %></li>
2829
<li><%= link_to "www.shakacode.com", "http://www.shakacode.com" %></li>

Diff for: app/views/pages/_header.html.erb

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<p>Current Commit:
2+
<%= link_to git_commit_sha_short,
3+
"https://github.com/shakacode/react-webpack-rails-tutorial/commit/#{git_commit_sha}",
4+
id: "git-commit-sha" %>
5+
</p>
6+
<ul>
7+
<li>
8+
If this work interests you and you're a developer or designer looking for full or part-time remote work: please
9+
<%= link_to "click here",
10+
"http://forum.shakacode.com/t/railsonmaui-is-hiring-and-partnering-part-time-remote-is-ok/156/3" %>
11+
and
12+
<%= link_to "here",
13+
"http://forum.shakacode.com/t/railsonmaui-is-hiring-and-partnering-part-time-remote-is-ok/156/2" %>.
14+
</li>
15+
<li>
16+
Please see the
17+
<%= link_to "github.com/shakacode/react-webpack-rails-tutorial/README.md",
18+
"https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/README.md" %>
19+
for details of how this example site was built.
20+
</li>
21+
</ul>
22+
<hr/>
23+

Diff for: app/views/pages/index.html.erb

+4-25
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,6 @@
1-
<h2>Using React (Server rendering) + Redux + Rails Backend (using the
1+
<h2>Using React (Server rendering) + Redux + React Router + Rails Backend (using the
22
<a href="https://github.com/shakacode/react_on_rails">react_on_rails gem</a>)</h2>
3-
<p>Current Commit:
4-
<%= link_to git_commit_sha_short,
5-
"https://github.com/shakacode/react-webpack-rails-tutorial/commit/#{git_commit_sha}",
6-
id: "git-commit-sha" %>
7-
</p>
8-
<ul>
9-
<li>
10-
If this work interests you and you're a developer or designer looking for full or part-time remote work: please
11-
<%= link_to "click here",
12-
"http://forum.shakacode.com/t/railsonmaui-is-hiring-and-partnering-part-time-remote-is-ok/156/3" %>
13-
and
14-
<%= link_to "here",
15-
"http://forum.shakacode.com/t/railsonmaui-is-hiring-and-partnering-part-time-remote-is-ok/156/2" %>.
16-
</li>
17-
<li>
18-
Please see the
19-
<%= link_to "github.com/shakacode/react-webpack-rails-tutorial/README.md",
20-
"https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/README.md" %>
21-
for details of how this example site was built.
22-
</li>
23-
</ul>
24-
<hr/>
3+
<%= render "header" %>
254

26-
<%= react_component('App', render(template: "/comments/index.json.jbuilder"),
27-
generator_function: true, prerender: true) %>
5+
<%= react_component('RouterApp', render(template: "/comments/index.json.jbuilder"),
6+
generator_function: true, prerender: true, raise_on_prerender_error: true) %>

Diff for: app/views/pages/no_router.html.erb

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<h2>Using React (Server rendering) + Redux + Rails Backend (using the
2+
<a href="https://github.com/shakacode/react_on_rails">react_on_rails gem</a>)</h2>
3+
<%= render "header" %>
4+
5+
<%= react_component('App', render(template: "/comments/index.json.jbuilder"),
6+
generator_function: true, prerender: true) %>

Diff for: client/app/components/CommentBox.jsx

+8-4
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ class CommentBox extends React.Component {
1919
componentDidMount() {
2020
const { fetchComments } = this.props.actions;
2121
fetchComments();
22-
setInterval(fetchComments, this.props.pollInterval);
22+
this.intervalId = setInterval(fetchComments, this.props.pollInterval);
23+
}
24+
25+
componentWillUnmount() {
26+
clearInterval(this.intervalId);
2327
}
2428

2529
render() {
@@ -28,11 +32,11 @@ class CommentBox extends React.Component {
2832
return (
2933
<div className="commentBox container">
3034
<h2>
31-
Comments { data.get('isFetching') && 'Loading...' }
35+
Comments {data.get('isFetching') && 'Loading...'}
3236
</h2>
3337
<p>
34-
Text take Github Flavored Markdown. Comments older than 24 hours are deleted.
35-
<b>Name</b> is preserved, <b>Text</b> is reset, between submits.
38+
Text take Github Flavored Markdown. Comments older than 24 hours are deleted.<br/>
39+
<b>Name</b> is preserved. <b>Text</b> is reset, between submits.
3640
</p>
3741
<CommentForm
3842
isSaving={data.get('isSaving')}

Diff for: client/app/components/CommentScreen.jsx

+5-16
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,19 @@
11
import React, { PropTypes } from 'react';
22
import CommentBox from './CommentBox';
3-
import { connect } from 'react-redux';
4-
import { bindActionCreators } from 'redux';
5-
import * as commentsActionCreators from '../actions/commentsActionCreators';
6-
7-
function select(state) {
8-
// Which part of the Redux global state does our component want to receive as props?
9-
return { data: state.$$commentsStore };
10-
}
113

124
class CommentScreen extends React.Component {
13-
static displayName = 'CommentScreen';
14-
155
static propTypes = {
16-
dispatch: PropTypes.func.isRequired,
6+
actions: PropTypes.object.isRequired,
177
data: PropTypes.object.isRequired,
188
};
199

2010
render() {
21-
const { dispatch, data } = this.props;
22-
const actions = bindActionCreators(commentsActionCreators, dispatch);
11+
const { data, actions } = this.props;
12+
2313
return (
2414
<div>
2515
<CommentBox
26-
pollInterval={5000}
16+
pollInterval={10000}
2717
data={data}
2818
actions={actions}
2919
ajaxCounter={data.get('ajaxCounter')}
@@ -45,5 +35,4 @@ class CommentScreen extends React.Component {
4535
}
4636
}
4737

48-
// Don't forget to actually use connect!
49-
export default connect(select)(CommentScreen);
38+
export default CommentScreen;

Diff for: client/app/components/NonRouterCommentScreen.jsx

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React, { PropTypes } from 'react';
2+
import CommentScreen from './CommentScreen';
3+
import { connect } from 'react-redux';
4+
import { bindActionCreators } from 'redux';
5+
import * as commentsActionCreators from '../actions/commentsActionCreators';
6+
7+
function select(state) {
8+
// Which part of the Redux global state does our component want to receive as props?
9+
return { data: state.$$commentsStore };
10+
}
11+
12+
class NonRouterCommentScreen extends React.Component {
13+
static propTypes = {
14+
dispatch: PropTypes.func.isRequired,
15+
data: PropTypes.object.isRequired,
16+
};
17+
18+
render() {
19+
const { dispatch, data } = this.props;
20+
const actions = bindActionCreators(commentsActionCreators, dispatch);
21+
return (
22+
<CommentScreen {...{actions, data}} />
23+
);
24+
}
25+
}
26+
27+
// Don't forget to actually use connect!
28+
export default connect(select)(NonRouterCommentScreen);

Diff for: client/app/components/RouterCommentScreen.jsx

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React, { PropTypes } from 'react';
2+
import CommentScreen from './CommentScreen';
3+
import { connect } from 'react-redux';
4+
import { bindActionCreators } from 'redux';
5+
import * as commentsActionCreators from '../actions/commentsActionCreators';
6+
7+
function select(state) {
8+
// Which part of the Redux global state does our component want to receive as props?
9+
return { data: state.$$commentsStore };
10+
}
11+
12+
class RouterCommentScreen extends React.Component {
13+
static propTypes = {
14+
dispatch: PropTypes.func.isRequired,
15+
data: PropTypes.object.isRequired,
16+
location: PropTypes.shape({
17+
state: PropTypes.object,
18+
}).isRequired,
19+
};
20+
21+
_renderNotification() {
22+
const locationState = this.props.location.state;
23+
if (!locationState || !locationState.redirectFrom) return null;
24+
25+
return (
26+
<div className="notification bg-success">
27+
You've been redirected from <strong>{locationState.redirectFrom}</strong>
28+
</div>
29+
);
30+
}
31+
32+
render() {
33+
const { dispatch, data } = this.props;
34+
const actions = bindActionCreators(commentsActionCreators, dispatch);
35+
36+
return (
37+
<div>
38+
{::this._renderNotification()}
39+
<CommentScreen {...{actions, data}} />
40+
</div>
41+
);
42+
}
43+
}
44+
45+
// Don't forget to actually use connect!
46+
export default connect(select)(RouterCommentScreen);

Diff for: client/app/components/SimpleCommentScreen.jsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ class SimpleCommentScreen extends React.Component {
6464
<div className="commentBox container">
6565
<h2>Comments</h2>
6666
<p>
67-
Text take Github Flavored Markdown. Comments older than 24 hours are deleted.
68-
<b> Name</b> is preserved, <b>Text</b> is reset, between submits.
67+
Text take Github Flavored Markdown. Comments older than 24 hours are deleted.<br/>
68+
<b>Name</b> is preserved. <b>Text</b> is reset, between submits.
6969
</p>
7070
<CommentForm
7171
ajaxSending={this.state.ajaxSending}

Diff for: client/app/components/TestReactRouter.jsx

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react';
2+
3+
export default class TestReactRouter extends React.Component {
4+
5+
render() {
6+
return (
7+
<div className="container">
8+
<h1>React Router is working!</h1>
9+
<p>
10+
Woohoo, we can use <code>react-router</code> here!
11+
</p>
12+
</div>
13+
);
14+
}
15+
16+
}

Diff for: client/app/components/TestReactRouterRedirect.jsx

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react';
2+
3+
export default class TestReactRouterRedirect extends React.Component {
4+
5+
static checkAuth(nextState, replaceState) {
6+
// Hard code this to demonstrate the effect
7+
const notAuthorized = true;
8+
if (notAuthorized) {
9+
replaceState({ redirectFrom: nextState.location.pathname }, '/');
10+
}
11+
}
12+
13+
render() {
14+
return (
15+
<div>Nope.</div>
16+
);
17+
}
18+
19+
}

Diff for: client/app/layout/Layout.jsx

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React, { PropTypes } from 'react';
2+
import { IndexLink, Link } from 'react-router';
3+
4+
export default class Layout extends React.Component {
5+
6+
static propTypes = {
7+
children: PropTypes.object.isRequired,
8+
}
9+
10+
render() {
11+
return (
12+
<section>
13+
<header>
14+
<ul>
15+
<li>
16+
<IndexLink to="/" activeClassName="active">Comments (Root URL)</IndexLink>
17+
</li>
18+
<li>
19+
<Link to="/react-router" activeClassName="active">Test React Router ('/react-router')</Link>
20+
</li>
21+
<li>
22+
<Link to="/react-router/redirect" activeClassName="active">Test Redirect (url to '/react-router/redirect' which goes to root '/')</Link>
23+
</li>
24+
</ul>
25+
</header>
26+
{this.props.children}
27+
</section>
28+
);
29+
}
30+
}

Diff for: client/app/routes/routes.jsx

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from 'react';
2+
import { Route, IndexRoute } from 'react-router';
3+
import Layout from '../layout/Layout';
4+
import TestReactRouter from '../components/TestReactRouter';
5+
import TestReactRouterRedirect from '../components/TestReactRouterRedirect';
6+
import RouterCommentScreen from '../components/RouterCommentScreen';
7+
8+
export default (
9+
<Route path="/" component={Layout}>
10+
<IndexRoute
11+
component={RouterCommentScreen}
12+
/>
13+
<Route
14+
path="react-router"
15+
component={TestReactRouter}
16+
/>
17+
<Route
18+
path="react-router/redirect"
19+
component={TestReactRouterRedirect}
20+
onEnter={TestReactRouterRedirect.checkAuth}
21+
/>
22+
</Route>
23+
);

Diff for: client/app/startup/ClientApp.jsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import React from 'react';
22
import { Provider } from 'react-redux';
33

44
import createStore from '../stores/commentsStore';
5-
import CommentScreen from '../components/CommentScreen';
5+
import NonRouterCommentScreen from '../components/NonRouterCommentScreen';
66

77
const App = props => {
88
const store = createStore(props);
99
return (
1010
<Provider store={store}>
11-
<CommentScreen />
11+
<NonRouterCommentScreen />
1212
</Provider>
1313
);
1414
};

0 commit comments

Comments
 (0)