MILAN 25-26 NOVEMBER 2016
{ Universal JS Web Applications with React
Luciano Mammino
Use Node.js modules in the browser.
Render the views of the application from the server (first request) and then in the browser (next requests)
Recognise the view associated to the current route from both the server and the browser.
Access data (and APIs) from both the server and the browser.
Manage changes on the state tree both on the server and the client...
curl -sS "https://judo-heroes.herokuapp.com/athlete/teddy-riner"
The data set
// src/data/athletes.js
const athletes = [
{
'id': 'driulis-gonzalez',
'name': 'Driulis González',
'country': {
'id': 'cu',
'name': 'Cuba',
'icon': 'flag-cu.png',
},
'birth': '1973',
'image': 'driulis-gonzalez.jpg',
'cover': 'driulis-gonzalez-cover.jpg',
'link': 'https://en.wikipedia.org/wiki/Driulis_González',
'medals': [
{ 'year': '1992', 'type': 'B', 'city': 'Barcelona', 'event': 'Olympic Games', 'category': '-57kg' },
{ 'year': '1993', 'type': 'B', 'city': 'Hamilton', 'event': 'World Championships', 'category': '-57kg' },
{ 'year': '1995', 'type': 'G', 'city': 'Chiba', 'event': 'World Championships', 'category': '-57kg' },
{ 'year': '1995', 'type': 'G', 'city': 'Mar del Plata', 'event': 'Pan American Games', 'category': '-57kg' },
{ 'year': '1996', 'type': 'G', 'city': 'Atlanta', 'event': 'Olympic Games', 'category': '-57kg' },
// ...
],
},
// ...
];
export default athletes;
Layout component
// src/components/Layout.js
import React from 'react';
import { Link } from 'react-router';
const Layout = (props) => (
<div className="app-container">
<header>
<Link to="/">
<img className="logo" src="/img/logo-judo-heroes.png"/>
</Link>
</header>
<div className="app-content">{props.children}</div>
<footer>
<p>
This is a demo app to showcase
<strong>universal Javascript</strong>
with <strong>React</strong> and
<strong>Express</strong>.
</p>
</footer>
</div>
);
export default Layout;
IndexPage component
// src/components/IndexPage.js
import React from 'react';
import AthletePreview from './AthletePreview';
import athletes from '../data/athletes';
const IndexPage = (props) => (
<div className="home">
<div className="athletes-selector">
{athletes.map(
athleteData => <AthletePreview
key={athleteData.id}
{...athleteData} />
)}
</div>
</div>
);
export default IndexPage;
AthletePreview component
// src/components/AthletePreview.js
import React from 'react';
import { Link } from 'react-router';
const AthletePreview = (props) => (
<Link to={`/athlete/${props.id}`}>
<div className="athlete-preview">
<img src={`img/${props.image}`}/>
<h2 className="name">{props.name}</h2>
<span className="medals-count">
<img src="/img/medal.png"/> {props.medals.length}
</span>
</div>
</Link>
);
export default AthletePreview;
AthletePage component
// src/components/AthletePage.js
import React from 'react';
import { Link } from 'react-router';
import NotFoundPage from './NotFoundPage';
import AthletesMenu from './AthletesMenu';
import Medal from './Medal';
import Flag from './Flag';
import athletes from '../data/athletes';
const AthletePage = (props) => {
const id = props.params.id;
const athlete = athletes.find((athlete) => athlete.id === id);
if (!athlete) {
return <NotFoundPage/>;
}
const headerStyle = { backgroundImage: `url(/img/${athlete.cover})` };
return (
<div className="athlete-full">
<AthletesMenu/>
<div className="athlete">
<header style={headerStyle}/>
<div className="picture-container">
<img src={`/img/${athlete.image}`}/>
<h2 className="name">{athlete.name}</h2>
</div>
<section className="description">
Olympic medalist from <strong><Flag {...athlete.country} showName="true"/></strong>,
born in {athlete.birth} (Find out more on <a href={athlete.link} target="_blank">Wikipedia</a>).
</section>
<section className="medals">
<p>Winner of <strong>{athlete.medals.length}</strong> medals:</p>
<ul>{
athlete.medals.map((medal, i) => <Medal key={i} {...medal}/>)
}</ul>
</section>
</div>
<div className="navigateBack">
<Link to="/">« Back to the index</Link>
</div>
</div>
);
};
export default AthletePage;
// src/components/AthletePage.js
// ...
const AthletePage = (props) => {
const id = props.params.id;
const athlete = athletes.find((athlete) => athlete.id === id);
if (!athlete) {
return <NotFoundPage/>;
}
const headerStyle = { backgroundImage: `url(/img/${athlete.cover})` };
return (
<div className="athlete-full">
<AthletesMenu/>
<div className="athlete">
<header style={headerStyle}/>
<div className="picture-container">
<img src={`/img/${athlete.image}`}/>
<h2 className="name">{athlete.name}</h2>
</div>
// ...
// src/components/AthletePage.js
// ...
<section className="description">
Olympic medalist from
<strong><Flag {...athlete.country} showName="true"/></strong>,
born in {athlete.birth}
(Find out more on <a href={athlete.link} target="_blank">Wikipedia</a>).
</section>
<section className="medals">
<p>Winner of <strong>{athlete.medals.length}</strong> medals:</p>
<ul>{
athlete.medals.map((medal, i) => <Medal key={i} {...medal}/>)
}</ul>
</section>
</div>
<div className="navigateBack">
<Link to="/">« Back to the index</Link>
</div>
</div>
);
};
export default AthletePage;
AthletesMenu component
// src/components/AthletesMenu.js
import React from 'react';
import { Link } from 'react-router';
import athletes from '../data/athletes';
const AthletesMenu = (props) => (
<nav className="atheletes-menu">
{athletes.map(athlete => {
return <Link key={athlete.id}
to={`/athlete/${athlete.id}`}
activeClassName="active">
{athlete.name}
</Link>;
})}
</nav>
);
export default AthletesMenu;
Flag component
// src/components/Flag.js
import React from 'react';
const Flag = (props) => (
<span className="flag">
<img className="icon"
title={props.name}
src={`/img/${props.icon}`}/>
{props.showName && <span className="name"> {props.name}</span>}
</span>
);
export default Flag;
Medal component
// src/components/Medal.js
import React from 'react';
const medalTypes = {
'G': 'Gold',
'S': 'Silver',
'B': 'Bronze'
};
const Medal = (props) => (
<li className="medal">
<span className={`symbol symbol-${props.type}`}
title={medalTypes[props.type]}>
{props.type}
</span>
<span className="year">{props.year}</span>
<span className="city"> {props.city}</span>
<span className="event"> ({props.event})</span>
<span className="category"> {props.category}</span>
</li>
);
export default Medal;
NotFoundPage component
// src/components/NotFoundPage.js
import React from 'react';
import { Link } from 'react-router';
const NotFoundPage = (props) => (
<div className="not-found">
<h1>404</h1>
<h2>Page not found!</h2>
<p>
<Link to="/">Go back to the main page</Link>
</p>
</div>
);
export default NotFoundPage;
Index Page: /
Athlete Page: /athlete/:id
// src/Routes.js
import React from 'react';
import { Route, IndexRoute } from 'react-router'
import Layout from './components/Layout';
import IndexPage from './components/IndexPage';
import AthletePage from './components/AthletePage';
import NotFoundPage from './components/NotFoundPage';
const routes = (
<Route path="/" component={Layout}>
<IndexRoute component={IndexPage}/>
<Route path="athlete/:id" component={AthletePage}/>
<Route path="*" component={NotFoundPage}/>
</Route>
);
export default routes;
// src/components/AppRoutes.js
import React from 'react';
import { Router, hashHistory } from 'react-router';
import routes from '../Routes';
const AppRoutes = (props) => (
<Router history={hashHistory}
routes={routes}
onUpdate={() => window.scrollTo(0, 0)}/>
);
export default AppRoutes;
// src/app-client.js
import React from 'react';
import ReactDOM from 'react-dom';
import AppRoutes from './components/AppRoutes';
window.onload = () => {
ReactDOM.render(<AppRoutes/>,
document.getElementById('main'));
};
// src/static/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0">
<title>Judo Heroes - A Universal JavaScript demo application
with React</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div id="main"></div>
<script src="/js/bundle.js"></script>
</body>
</html>
(BABEL + WEBPACK)
.babelrc
import webpack from 'webpack';
import path from 'path';
const config = {
entry: { js: './src/app-client.js' },
output: {
path: path.join(__dirname, 'src', 'static', 'js'),
filename: 'bundle.js'
},
module: {
loaders: [{
test: path.join(__dirname, 'src'),
loaders: [{ loader: 'babel-loader' }]
}]
},
};
export default config;
.webpack.config.babel.js
{
"presets": ["react", "es2015"]
}
// src/server.js
import path from 'path';
import { Server } from 'http';
import Express from 'express';
const app = new Express();
const server = new Server(app);
// define the folder that will be used for static assets
app.use(Express.static(path.join(__dirname, 'static')));
// start the server
const port = process.env.PORT || 3000;
const env = process.env.NODE_ENV || 'production';
server.listen(port, err => {
if (err) {
return console.error(err);
}
console.info(`Server running on http://localhost:${port} [${env}]`);
});
Static Express server
What we learned so far
Using browser history
// src/views/index.ejs
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0">
<title>Judo Heroes - A Universal JavaScript demo application
with React</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div id="main"><%- markup -%></div>
<script src="/js/bundle.js"></script>
</body>
</html>
Converting static index.html into a template
Updating the server app
// ...
// universal routing and rendering
app.get('*', (req, res) => {
match(
{ routes, location: req.url },
(err, redirectLocation, renderProps) => {
// in case of error display the error message
if (err) {
return res.status(500).send(err.message);
}
// in case of redirect propagate the redirect to the browser
if (redirectLocation) {
return res.redirect(302, redirectLocation.pathname +
redirectLocation.search);
}
// generate the React markup for the current route
let markup;
if (renderProps) {
// if the current route matched we have renderProps
markup = renderToString(<RouterContext {...renderProps}/>);
} else {
// otherwise we can render a 404 page
markup = renderToString(<NotFoundPage/>);
res.status(404);
}
// render the index template with the embedded React markup
return res.render('index', { markup });
}
);
});
What we learned so far
api-proxy & async-props
(COMPLETE CHAPTER in Node.js Design Patterns)
Redux
from here...
(Special thanks to @cirpo, @andreaman87, Aleksandar Čambas & @quasi_modal)