3/1/19
2019 edition
Clayton Ferris
Doug Waltman
Charlie King
React is a library for creating user interfaces
import React from "react";
import WmSdk from "@ghostgroup/wm-sdk";
function TodoItem(props) {
return <div>{props.todo}</div>;
}
class TodoList extends React.Component {
state = {
todos: [
{
subject: "Give training class"
}
]
};
async componentDidMount() {
// fetch data
const sdk = new WmSdk();
const todos = await sdk.todos.fetchTodos();
this.setState({ todos });
}
render() {
return (
<ul>
{this.state.todos.map(todo => (
<TodoItem todo={todo} />
))}
</ul>
);
}
}
We tried rolling our own SSR Project.
Every JS file in the "page" directory becomes a route. e.g. detail.js -> /detail
The components directory contains shared components used in pages
Contains all the other modules and utilities
Card
As a email user
I would like to see a list of emails
so I can see who is trying to contact me
Conversation
Initial page to render the list of emails. Styling optional, mock data OK.
Confirmation
WHEN viewing localhost
THEN I will see a list of my emails
Card
As a email user
I would like to open an email
so I can read the full contents
Conversation
A detail page so I can read the sender, subject and full body.
Confirmation
WHEN viewing localhost/detail?emailId=<id>
THEN I will see the requested email
Card
As a email user
I would like a preview of the email body
so I get more context without clicking
Conversation
Add a presentational component for header and subheader to visually differentiate and use semantic HTML
Confirmation
WHEN viewing localhost
THEN I will see a body preview in the list
Card
As a email user
I would like to be able to star emails
so I can keep track of what is important
Conversation
Add a star component. Persistence of star state optional.
Confirmation
WHEN viewing localhost
THEN I will see a star by each email
AND I can click the start to toggle
Card
As a email user
I would like the UI to not be hot garbage
so I enjoy using the application
Conversation
This ticket covers styling the application to match the comps.
Confirmation
WHEN viewing the app
THEN It will look beautiful
Card
As a email user
I would like there to be no FOUC
so the app doesn't feel jank
Conversation
This ticket covers collecting our styles and ensuring they are rendered properly on the server
Confirmation
WHEN viewing localhost
THEN I will see no flash of un-styled content
SASS
BEM
Styled
<style data-styled-components>
/* sc-component-id: email-detail__Detail-sc-1rhtos4-0 */
.lgAiXo{background:#ccc;color:#333;padding:1rem;}
</style>
<div className="lgAiXo">
Hello Weed
</div>
Card
As a Weedmaps engineer
I would like the email app to serve more requests
so it doesn't fall over during high load
Conversation
Add Concurrency to our email Application using PM2. This should be setup to use all CPUs - 1
Confirmation
WHEN viewing localhost
THEN the application works as normal
WHEN viewing the application processes
THEN It will be using more than 1
Card
As a email user
I would like the email app to load faster
so I can get my work done and get on with my day
Conversation
Add Caching with Memcached to the email application so page loads are super quick
Confirmation
WHEN viewing localhost
WHEN it is the first request
THEN the app will work as normal and there will be a cache MISS header
WHEN viewing localhost
WHEN it is the second request
THEN the app will work as normal and there will be a cache HIT header
Server Side Rendering in the React context is rendering your React Application on a Server, then sending the HTML to a client browser
const express = require("express");
const React = require("react");
const ReactDOMServer = require("react-dom/server");
const Page = require("./pages/index");
function handler(req, res, next) {
res.type('html')
.send(ReactDOMServer.renderToString(<Page />));
next();
}
const expressApp = express();
expressApp.use(handler).listen(3000);
Well no. The client side HTML must be "re-hydrated" with the same data and application state.
What are the forms of state we have to re-hydrate?
Node
JS
Data
Html
Render
Hydrate
Browser
Ionic
Prerender
Convergence
Is Mobile? Is ionic? Is bot?
Time to first Contentful Paint (TTFCP). Server side rendering makes this trivial to get content to users eyes.
Moonshot ~1s
Ionic ~3-4s
import { inject, observer } from 'mobx-react';
export class Page {
static async getInitialProps(ctx, store) {
// Call some store actions
await store.fetchData();
// Serialize the Store state
const storeInitialState = store.dehydrate();
// Return as props
return { storeInitialState };
}
constructor() {
// On Page creation, rehydrate the store
this.props.store.rehydrate(this.props.storeInitialState);
}
render() {
// Render the page with the Store Data
return (<div>{this.props.store.someData}</div>);
}
}
export default inject('store')(observer(Page));
Why does Time to Interactive Suffer?
Avoid making multiple calls in serial
import React from "react";
export class MySlowPage extends React.Component {
static async getInitialProps(ctx, store) {
const user = await store.getUser(); // 200ms
const emails = await store.fetchEmails(); // 200ms
const folders = await store.fetchEmailFolders(); // 200ms
return { user, emails, folders }; // 600ms render!
}
render() {
return <div>Hello</div>;
}
}
export class MyFastPage extends React.Component {
static async getInitialProps(ctx, store) {
const userPromise = store.getUser(); // 200ms
const emailsPromise = store.fetchEmails(); // 200ms
const foldersPromise = store.fetchEmailFolders(); // 200ms
const [user, emails, folders] = Promise.all([
userPromise,
emailsPromise,
foldersPromise
]);
return { user, emails, folders }; // ~200ms render!
}
render() {
return <div>Hello</div>;
}
}
Remember the double payload cost
import React from "react";
export class MyHeavyPage extends React.Component {
static async getInitialProps(ctx, store) {
const userPromise = store.getUser(); // 200ms
const emailsPromise = store.fetchEmails(); // 200ms
const foldersPromise = store.fetchEmailFolders(); // 200ms
const [user, emails, folders] = Promise.all([
userPromise,
emailsPromise,
foldersPromise
]);
return { user, emails, folders }; // ~200ms render!
}
render() {
return <div>Hello</div>;
}
}
import React from "react";
export class MyLightPage extends React.Component {
static async getInitialProps(ctx, store) {
const userPromise = store.getUser(); // 200ms
const emailsPromise = store.fetchEmails(); // 200ms
const [user, emails] = Promise.all([
userPromise,
emailsPromise,
]);
return { user, emails }; // ~200ms render!
}
async componentDidMount() {
// Let's fetch the folder client side,
// since they aren't required to deliver a
// good experience
const { store } = this.props;
await store.fetchEmailFolders();
}
render() {
return <div>Hello</div>;
}
}
Remember the TTI Cost
import React from "react";
import leftPad from "left-pad";
export class MyHeavyPage extends React.Component {
static async getInitialProps(ctx, store) {
const userPromise = store.getUser();
const emailsPromise = store.fetchEmails();
const [user, emails] = Promise.all([
userPromise,
emailsPromise,
]);
return { user, emails };
}
render() {
return <div>{leftPad("Hello", 5)}</div>;
}
}
import React from "react";
export class MyLightPage extends React.Component {
static async getInitialProps(ctx, store) {
const userPromise = store.getUser();
const emailsPromise = store.fetchEmails();
const [user, emails] = Promise.all([
userPromise,
emailsPromise,
]);
return { user, emails };
}
render() {
return <div>{'Hello'.padStart(5, ' ')}</div>;
}
}
Advanced, production process manager for Node.js
Your friendly neighborhood memory cache daemon.
Card
As a Weedmaps engineer
I would like the email app to serve as little requests as possible
so it doesn't fall over during high load
Conversation
Add NGINX to our Application to handle serving static files so the application doesn't have to
Confirmation
WHEN viewing localhost
THEN static files will be served by NGINX
Card
As a email user
I would like the email app to use less bandwidth
so my data plan isn't destroyed
Conversation
Add GZIP to the NGINX server to ensure our JS bundles are smaller over the wire
Confirmation
WHEN viewing localhost
THEN static files will be served with GZIP encoding
NodeJS
JS / HTML / Images
NodeJS
JS / Images
NGINX
HTML
Exchange
JS / Images
Convergence
HTML
Moonshot
Logistics FE
HTML
HTML
NodeJS
NGINX
Irvine
New Jersey
Frankfurt
Oregon (AWS US-west)
200ms
236ms
284ms
NodeJS
NGINX
Irvine
New Jersey
Frankfurt
Oregon (AWS US-west)
200ms
200ms
200ms
CDN
CDN
CDN
Card
As a Weedmaps engineer
I would like the email app to be live in production
so we can launch it to customers
Conversation
Add the required files to deploy the email app to Production. Use Nectar Collector to ship to the first environment
Confirmation
WHEN viewing the email app URL
THEN it will serve the email application
Rancher is our "Container Management Platform" or in other words, Docker Container Orchestration.
Docker Image
Rancher
Engineer
Frontend System Architecture
https://weedmaps.com/deals
AWS Cloudfront (CDN)
AWS Load Balancer
API Gateway
Convergence
App
App
Rancher
App
// User actionable Errors should set a UI state, so they can act upon it
class Page extends React.Component {
componentDidCatch(error, info) {
// Show an error state to user, let them self-correct
this.setState({ hasError: true });
// Optionally notify us, if this is truly un-expected
global.Honeybadger.notify(error);
}
}
// Non user-actionable errors should report to Weedmaps
export const async function SomeMiddleware(req, res, next) {
try {
const data = await fetchSomeData();
} catch(e) {
Honeybadger.notify(e);
}
}
// Don't do this!
class Page extends React.Component {
componentDidCatch(error, info) {
logger.error(error);
}
}
// Or This
export const async function SomeMiddleware(req, res, next) {
try {
const data = await fetchSomeData();
} catch(e) {
logger.error(e);
}
}