Skip to content

Commit d834f32

Browse files
committed
routing and ssr, handling redirects too
1 parent 53a7f00 commit d834f32

File tree

13 files changed

+259
-34
lines changed

13 files changed

+259
-34
lines changed

package.json

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,30 @@
2323
"style-loader": "^0.14.1",
2424
"url-loader": "^0.5.8",
2525
"webpack": "^2.2.1",
26-
"webpack-dev-server": "^2.4.2"
26+
"webpack-dev-server": "^2.4.2",
27+
"webpack-manifest-plugin": "^1.1.0"
2728
},
2829
"scripts": {
29-
"lint": "eslint --fix src/",
3030
"build": "rimraf build/ && webpack -p",
31-
"start": "webpack-dev-server --config webpack.config.dev.js"
31+
"dev": "webpack-dev-server --config webpack.config.dev.js",
32+
"lint": "eslint --fix src/ server.js",
33+
"start": "node server"
3234
},
3335
"dependencies": {
3436
"babel-core": "^6.24.0",
3537
"babel-preset-es2015": "^6.24.0",
38+
"babel-preset-react": "^6.23.0",
39+
"babel-preset-stage-2": "^6.22.0",
40+
"babel-register": "^6.24.0",
41+
"express": "^4.15.2",
42+
"ignore-styles": "^5.0.1",
43+
"isomorphic-fetch": "^2.2.1",
3644
"react": "^15.4.2",
3745
"react-dom": "^15.4.2",
3846
"react-hot-loader": "3.0.0-beta.6",
3947
"react-redux": "^5.0.3",
48+
"react-router": "next",
49+
"react-router-dom": "next",
4050
"redux": "^3.6.0",
4151
"redux-thunk": "^2.2.0"
4252
}

server.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
require('babel-register')({
2+
extensions: ['.js'],
3+
presets: ['es2015'],
4+
})
5+
require('ignore-styles')
6+
const express = require('express')
7+
const { createElement } = require('react')
8+
const { StaticRouter } = require('react-router')
9+
const { Provider } = require('react-redux')
10+
const { renderToString } = require('react-dom/server')
11+
12+
const { htmlTemplate } = require('./src/index.html.js')
13+
const manifest = require('./build/manifest.json')
14+
const App = require('./src/components/app/app').default
15+
const storeFactory = require('./src/store').default
16+
17+
const app = express()
18+
const htmlCache = {}
19+
20+
function makeCacheObject (html, expires = 3600000) {
21+
return {
22+
html,
23+
expires,
24+
createdAt: Date.now(),
25+
}
26+
}
27+
28+
function isFresh (cacheObj) {
29+
return (Date.now() - cacheObj.createdAt) < cacheObj.expires
30+
}
31+
32+
function bootstrapApp(location, store) {
33+
const context = {}
34+
const appEntry = createElement(
35+
Provider, { store }, createElement(
36+
StaticRouter, { location, context }, createElement(
37+
App)))
38+
39+
const appHTML = renderToString(appEntry)
40+
return { appHTML, context }
41+
}
42+
43+
function checkCache(request, response, next) {
44+
if (htmlCache[request.url] && isFresh(htmlCache[request.url])) {
45+
response.send(htmlCache[request.url].html)
46+
return
47+
}
48+
49+
next()
50+
}
51+
52+
function handleSSRRequest (request, response) {
53+
const store = storeFactory()
54+
const unsubscribe = store.subscribe(() => {
55+
const state = store.getState()
56+
if (!state.robotData.isPending) {
57+
unsubscribe()
58+
const { appHTML } = bootstrapApp(request.url, store)
59+
const htmlResponse = htmlTemplate({
60+
jsPath: manifest['main.js'],
61+
cssPath: manifest['main.css'],
62+
appHTML,
63+
state,
64+
})
65+
response.send(htmlResponse)
66+
htmlCache[request.url] = makeCacheObject(htmlResponse)
67+
}
68+
})
69+
70+
const { context } = bootstrapApp(request.url, store)
71+
if (context.url) {
72+
response.redirect(context.url)
73+
unsubscribe()
74+
return
75+
}
76+
store.dispatch({ type: 'INIT_SSR' })
77+
}
78+
79+
app.use('/static', express.static('./build/static'))
80+
app.use(checkCache)
81+
app.use(handleSSRRequest)
82+
83+
app.listen(8080)

src/actions/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'isomorphic-fetch'
12
import {
23
SET_SEARCH_TERM,
34
GET_ROBOTS_IS_PENDING,
@@ -11,7 +12,11 @@ export const setSearchTerm = term => ({
1112
})
1213

1314

14-
export const getRobots = () => (dispatch) => {
15+
export const getRobots = () => (dispatch, getState) => {
16+
const state = getState()
17+
if (state.robotData.robots.length >= 10) {
18+
return
19+
}
1520
dispatch({ type: GET_ROBOTS_IS_PENDING })
1621

1722
fetch('https://jsonplaceholder.typicode.com/users')

src/components/app/app.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import React from 'react'
2+
import { Route, Switch, Redirect } from 'react-router-dom'
23
import './app.css'
3-
// import RobotFilterViewContainer from '../../containers/robot-filter-view.container'
4+
import RobotFilterViewContainer from '../../containers/robot-filter-view.container'
45
import RobotProfileViewContainer from '../../containers/robot-profile-view.container'
56

67
function App() {
78
return (
89
<div className="tc">
910
<h1>RoboDex</h1>
10-
{/* <RobotFilterViewContainer /> */}
11-
<RobotProfileViewContainer />
11+
<Switch>
12+
<Route path="/" exact component={RobotFilterViewContainer} />
13+
<Route path="/profile/:id" component={RobotProfileViewContainer} />
14+
<Redirect to={{ pathname: '/' }} />
15+
</Switch>
1216
</div>
1317
)
1418
}

src/components/card.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import React from 'react'
2+
import { Link } from 'react-router-dom'
23

34
const Card = ({ id, name, email }) => (
4-
<div className="grow bg-light-green br3 pa3 ma2 dib">
5-
<img alt={name} src={`//robohash.org/${id}?size=200x200`} />
6-
<div>
7-
<h2>{name}</h2>
8-
<p>{email}</p>
5+
<Link to={`/profile/${id}`}>
6+
<div className="grow bg-light-green br3 pa3 ma2 dib">
7+
<img alt={name} src={`//robohash.org/${id}?size=200x200`} />
8+
<div>
9+
<h2>{name}</h2>
10+
<p>{email}</p>
11+
</div>
912
</div>
10-
</div>
13+
</Link>
1114
)
1215

1316
Card.propTypes = {

src/components/profile/profile-view.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { Component } from 'react'
2+
import { Link } from 'react-router-dom'
23
import Profile from './profile'
34
import './profile.css'
45

@@ -16,6 +17,7 @@ class ProfileView extends Component {
1617
? <Profile robot={robot} />
1718
: <h2>Loading... </h2>
1819
}
20+
<Link className="button" to="/">Back</Link>
1921
</div>
2022
)
2123
}

src/containers/robot-profile-view.container.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { getRobots } from '../actions'
33
import ProfileView from '../components/profile/profile-view'
44

55
const mapStateToProps = (state, ownProps) => {
6-
const id = ownProps.id || 1
6+
const id = parseInt(ownProps.match.params.id, 10) || 1
77
const isPending = state.robotData.isPending
88
let robot = null
99

src/index.dev.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ import React from 'react'
22
import ReactDOM from 'react-dom'
33
import { Provider } from 'react-redux'
44
import { AppContainer } from 'react-hot-loader'
5-
5+
import { BrowserRouter as Router } from 'react-router-dom'
66
import App from './components/app/app'
7-
import store from './store'
7+
import storeFactory from './store'
88

99
const render = (Component) => {
1010
ReactDOM.render(
1111
<AppContainer>
12-
<Provider store={store}>
13-
<Component />
12+
<Provider store={storeFactory()}>
13+
<Router>
14+
<Component />
15+
</Router>
1416
</Provider>
1517
</AppContainer>,
1618
document.getElementById('root'),

src/index.html.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module.exports.htmlTemplate = ({ cssPath, jsPath, appHTML, state }) => `<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<link rel="stylesheet" href="https://unpkg.com/tachyons/css/tachyons.min.css">
7+
<link rel="stylesheet" href="${cssPath}">
8+
<title>react app</title>
9+
</head>
10+
<body>
11+
<div id="root">${appHTML}</div>
12+
<script>
13+
window.INITIAL_STATE = ${JSON.stringify(state).replace(/</g, '\\u003c')}
14+
</script>
15+
<script src="${jsPath}"></script>
16+
</body>
17+
</html>`

src/index.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import React from 'react'
22
import ReactDOM from 'react-dom'
33
import { Provider } from 'react-redux'
4+
import { BrowserRouter as Router } from 'react-router-dom'
45
import App from './components/app/app'
5-
import store from './store'
6+
import storeFactory from './store'
67

78
ReactDOM.render(
8-
<Provider store={store}>
9-
<App />
9+
<Provider store={storeFactory()}>
10+
<Router>
11+
<App />
12+
</Router>
1013
</Provider>,
11-
document.getElementById('root'),
12-
)
14+
document.getElementById('root'))

0 commit comments

Comments
 (0)