Universal JavaScript

Luciano Mammino

Athens, 27 May 2017

Universal JavaScript

Luciano Mammino

Who is LUCIANO

🔥

mt. Etna

📍Νάξος

NeBK20FUA

-20% eBook

 

NpBK15FUA

-15% Print

 

⚡️Contains A whole chapter about Universal JavaScript ⚡️

</shameless-self-promotion>

😅

Agenda

  1. The term "Universal" JS
  2. Who & Why
  3. Common problems and technologies
  4. Building a frontend only Single Page App
  5. Making it "Universal"

ISOMORPHIC

UNIVERSAL... Wait, WHAT?

not only

for the web...

Desktop applications

Mobile applications

Hardware!

Advantages

of universal javascript

"JavaScript-only" development

Maintainability

Better SEO

Faster "perceived" load time

Advantages...

🙉 Moar

Keep using React/JS paradigms also for "static" websites

Speed up content loading with linkprefetch

In the wild

it Looks great but...

Module Sharing

Use Node.js modules in the browser

UMD

Universal Rendering

Render the views of the application from the server (first request) and then in the browser (next requests)

Universal ROUTING

Recognise the view associated to the current route from both the server and the browser.

Universal DATA RETRIEVAL

Access data (and APIs) from both the server and the browser.

Universal STATE MANAGEMENT

Manage changes on the state tree both on the server and the client...

Futuristic/ALTERNATIVE JS?!

OK...
LET'S Stop complaining and build something!

What are we going to build?

v 2.0

curl -sS "http://localhost:3000/athlete/teddy-riner"

What tools are we going to use?

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
    

src/components
React components

src/data
Data files

src/static
Static resources

src/views
Views (HTML templates)

src/app-client.js
Client side app

src/server.js
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;

React components

Layout component

IndexPage component

AthletePage component

NotFoundPage component

AthleteCard 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;

ROUTING

2 routes

  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} />;
};

Client APP

// 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')
  );
};

HTML TEMPLATE

// 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>

BUILD CONFIG

(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"]
}

Let's build it!

// 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

Ready... LET's TEST IT

RECAP

What we learned so far

  1. Define views combining React components
  2. Add routing using React Router
  3. Compile the client bundle with Babel and Webpack
  4. Run the app with a static Express server

SERVER SIDE
RENDERING AND ROUTING

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 });
});

That's it!

LET's TEST again

RECAP

What we learned so far

  1. Create a Single Page Application with React and React Router
  2. Add server side routing and rendering using React and React Router libraries in the Express app

universal data retrieval

api-proxy & async-props

(COMPLETE CHAPTER in Node.js Design Patterns)

 

universal state management

Redux

 

Progressive Web Apps (PWA)

@addyosmani's tutorial + Create React App

Where do we go

from here...

THANKS!

(Special thanks to @cirpo, @andreaman87, Aleksandar Čambas & @quasi_modal)