Luciano Mammino PRO
Cloud developer, entrepreneur, fighter, butterfly maker! #nodejs #javascript - Author of https://www.nodejsdesignpatterns.com , Founder of https://fullstackbulletin.com
Rome 24-25 MARCH 2017
{ Universal JS Web Applications with React
Luciano Mammino
Keep using React/JS paradigms also to generate "static" websites
Speed up content loading with linkprefetch
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...
v 2.0
curl -sS "https://judo-heroes.herokuapp.com/athlete/teddy-riner"
v2
v15.4
v4
v5-alpha
Dependencies 😺
yarn add \
babel-cli@6.18.0 \
babel-core@6.18.2 \
babel-preset-es2015@6.18.0 \
babel-preset-react@6.16.0 \
ejs@2.5.2 \
express@5.0.0-alpha.5 \
react@15.4.2 \
react-dom@15.4.2 \
react-router-dom@4.0.0 \
webpack@2.2.1
React components
Data files
Static resources
Views (HTML templates)
Client side app
Server side app
Config, Metadata, etc.
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: [
{ id: 1, year: '1992', type: 'B', city: 'Barcelona', event: 'Olympic Games', category: '-57kg' },
{ id: 2, year: '1993', type: 'B', city: 'Hamilton', event: 'World Championships', category: '-57kg' },
{ id: 3, year: '1995', type: 'G', city: 'Chiba', event: 'World Championships', category: '-57kg' },
{ id: 4, year: '1995', type: 'G', city: 'Mar del Plata', event: 'Pan American Games', category: '-57kg' },
{ id: 5, year: '1996', type: 'G', city: 'Atlanta', event: 'Olympic Games', category: '-57kg' },
// ...
],
},
// ...
];
export default athletes;
Layout component
IndexPage component
AthletePage component
NotFoundPage component
AthletePreview component
AthletesMenu component
Flag component
Medal component
// src/components/Layout.js
import React from 'react';
import { Link } from 'react-router-dom';
export 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;
// src/components/IndexPage.js
import React from 'react';
import { AthletePreview } from './AthletePreview';
export const IndexPage = ({ athletes }) => (
<div className="home">
<div className="athletes-selector">
{
athletes.map( athleteData =>
<AthletePreview
key={athleteData.id}
{...athleteData} />
)
}
</div>
</div>
);
export default IndexPage;
// src/components/AthletePreview.js
import React from 'react';
import { Link } from 'react-router';
export 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;
Index Page: / Athlete Page: /athlete/:id
// src/components/App.js
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { Layout } from './Layout';
import { IndexPage } from './IndexPage';
import { AthletePage } from './AthletePage';
import { NotFoundPage } from './NotFoundPage';
import athletes from '../data/athletes';
// ...
export const App = () => (
<Layout>
<Switch>
<Route exact path="/" render={renderIndex} />
<Route exact path="/athlete/:id" render={renderAthlete} />
<Route component={NotFoundPage} />
</Switch>
</Layout>
);
export default App;
// src/components/App.js
// ...
const renderIndex = () => <IndexPage athletes={athletes} />;
const renderAthlete = ({ match, staticContext }) => {
const id = match.params.id;
const athlete = athletes.find(current => current.id === id);
if (!athlete) {
return <NotFoundPage staticContext={staticContext} />;
}
return <AthletePage
athlete={athlete}
athletes={athletes} />;
};
// src/app-client.js
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import { App } from './components/App';
const AppClient = () => (
<Router>
<App />
</Router>
);
window.onload = () => {
render(
<AppClient />,
document.getElementById('main')
);
};
// 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>
(BABEL + WEBPACK)
.babelrc
import path from 'path';
const config = {
entry: {
js: './src/app-client.js',
},
output: {
path: path.join(__dirname, 'src', 'static', 'js'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: path.join(__dirname, 'src'),
use: { 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);
// use ejs templates
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// define the folder that will be used for static assets
app.use(Express.static(path.join(__dirname, 'static')));
// render the index for every non-matched route
app.get('*', (req, res) => {
let markup = '';
let status = 200;
return res.status(status).render('index', { markup });
});
// start the server
const port = process.env.PORT || 3000;
const env = process.env.NODE_ENV || 'production';
server.listen(port);
"Static" Express server
What we learned so far
Updating the server app
// ...
import { renderToString } from 'react-dom/server';
import { StaticRouter as Router } from 'react-router-dom';
import { App } from './components/App';
// ...
app.get('*', (req, res) => {
let markup = '';
let status = 200;
const context = {};
markup = renderToString(
<Router location={req.url} context={context}>
<App />
</Router>,
);
// context.url will contain the URL to
// redirect to if a <Redirect> was used
if (context.url) {
return res.redirect(302, context.url);
}
if (context.is404) {
status = 404;
}
return res.status(status).render('index', { markup });
});
What we learned so far
api-proxy & async-props
(COMPLETE CHAPTER in Node.js Design Patterns)
Redux
from here...
Code: loige.link/judo-heroes-2
(Special thanks to @cirpo, @andreaman87, Aleksandar Čambas & @quasi_modal)
By Luciano Mammino
Tech talk about Universal JavaScript with a complete example built using React, React Router, Webpack, Babel and Express
Cloud developer, entrepreneur, fighter, butterfly maker! #nodejs #javascript - Author of https://www.nodejsdesignpatterns.com , Founder of https://fullstackbulletin.com