Next.js

5/3/17

Universal React Apps made simple

Charlie King - @thebringking

Who am I?

What is Next.js?

Convention driven Universal React "Framework" 

https://zeit.co

Handles the "hard" parts of Server Rendering React

Data Fetching, Client reconciliation, Routing

https://zeit.co

https://zeit.co

Problem: Bundle Size

Rails Views

Angular 1.x

Our bundle size

~2MB

~5MB

~3MB

Next

Bundle split by Page

/

/deals

/careers

Size

~1MB

+= 200KB

+= 210KB

Problem: Initial Page Load 

Time to First Paint, Time to First Meaningful Paint (TTFP, TTFMP)

Rails Views

Content in ~4s - 3G

Interactive in ~8s

Angular 1.x

Content in ~10s - 3G

Interactive right after paint

Next.js

Content in ~5s - 2G!

Interactive in ~8s

Problem: Unused CSS

Flash of un-styled content (FOUC)

Analyzing Usage

.wm-slide-row-container {
  height: auto;
}

.wm-slide-row-header {
  align-items: center;
  display: flex;
  justify-content: space-between;
  padding-left: 15px;
  padding-right: 15px;

  .wm-icon {
    height: 21px;
    margin-right: 9px;
    width: 21px;

    &.wm-icon-verified {
      margin-right: 5px;
    }

    @include media($desktop) {
      height: 26px;
      margin-right: 11px;
      width: 26px;
    }

    svg {
      height: 21px;
      @include media($desktop) {
        height: 26px;
      }
    }
  }

  .pushup {
    margin-bottom: 1px;
  }

  h2 {
    align-items: center;
    color: $charcoal;
    display: flex;
    font-size: 22px;
    font-weight: 600;
    line-height: 1;
    margin: 0;

    @include media($medium) {
      font-size: 24px;
    }

    @include media($desktop) {
      font-size: 26px;
    }

    a {
      color: $charcoal;
      text-decoration: none;

      &:hover,
      &:focus {
        color: $teal;
      }
    }

    .pushup {
      margin-bottom: 1px;
    }
  }

  nav {
    white-space: nowrap;

    > div {
      display: inline-block;
      vertical-align: middle;

      + div {
        margin-left: 7.5px;
      }
    }

    @include media($mobile) {
      .arrows {
        display: none;

        .desktop & {
          display: inline-block;
        }
      }
    }
  }

  .arrows {
    font-size: 0;
    height: $scroll-button-size;
    width: $scroll-button-size * 2;

    a {
      background: $white;
      border: 1px solid $gainsboro;
      border-radius: 5px 0 0 5px;
      color: $steel;
      cursor: pointer;
      display: block;
      float: left;
      font-size: 24px;
      height: $scroll-button-size;
      line-height: $scroll-button-size;
      position: relative;
      text-align: center;
      width: $scroll-button-size;

      + a {
        border-left: 0;
        border-radius: 0 5px 5px 0;
        float: right;
      }

      &.disabled {
        color: $gainsboro;
        cursor: default;
        pointer-events: none;
      }

      &.loading {
        color: transparent;
      }

      .spinner {
        left: 0;
        position: absolute;
        right: 0;
        top: 4px;
      }
    }
  }

  .btn-rounded {
    align-items: center;
    background: $white;
    border-color: $gainsboro;
    color: $steel;
    display: flex;
    font-size: 14px;
    height: 40px;
    line-height: $scroll-button-size - 2px;
    padding: 0 15px;

    span {
      display: none;

      @include media($desktop) {
        display: inline-block;
      }
    }

    .wm-icon {
      margin-right: 0;

      @include media($desktop) {
        margin-right: 1px;
      }
    }
  }

  .displayname-short {
    display: inline-block;

    @media screen and (min-width: 375px) {
      display: none;
    }
  }

  .displayname-long {
    display: none;

    @media screen and (min-width: 375px) {
      display: inline-block;
    }
  }
}

.wm-slide-row-content {
  width: 100%;
  max-width: 100%;
  position: relative;
  overflow-x: scroll;
  white-space: nowrap;
  overflow-y: hidden;
  @include media($medium) {
    overflow: hidden;
  }
}

.wm-slide-row-track {
  position: relative;
  top: 0;
  left: 0;
  display: block;
  transition: transform ease 250ms;
}

.wm-slide-page {
  display: inline-block;
  width: 100%;
}

.test-card {
  width: 33.333%;

  @include media($medium) {
    width: 25%;
  }

  @include media($desktop) {
    width: 20%;
  }
}

component.scss

<div class="wm-slide-row-container">
  <div class="wm-slide-row-header">
    <div ng-transclude="header" ng-if="vm.showCustomHeader"></div>
    <nav>
      <div ng-if="vm.showCount" class="category-count">
        {{ vm.totalCount }} {{ vm.countName }}
      </div>
      <div class="extra" ng-transclude="controls" ng-if="vm.showExtraControls">
      </div>
      <div class="arrows" ng-if="vm.showArrows">
        <a
          ng-click="vm.onPreviousPage()">
          <i class="icon ion-ios-arrow-back"></i>
        </a>
        <a
          ng-click="vm.onNextPage()">
          <i class="icon ion-ios-arrow-forward"></i>
        </a>
      </div>
    </nav>
  </div>
  <div class="wm-slide-row-content">
    <div ng-transclude="content" class="wm-slide-row-track"
         ng-style="{transform:vm.currentTrackTransform}">
    </div>
  </div>
</div>

some_view.html

@component({
  name: 'wmSlideRow',
  template: 'js/components/wm_slide_row/templates/wm_slide_row.html',
  bindings: {
    name: '@',
    itemCount: '<',
    icon: '@',
    hasMore: '<',
    loadMore: '=?',
    totalCount: '<',
    useCount: '<',
    countName: '@'
  },
  transclude: {
    header: '?wmSlideRowHeader',
    content: 'wmSlideRowContent',
    controls: '?wmSlideRowControls'
  }
})
class WmSlideRow { // eslint-disable-line no-unused-vars

  static $inject = ['$element', 'wmLogger', '$transclude'];

  constructor(...injected) {
    WmSlideRow.$inject.forEach((dep, idx) => (this[dep] = injected[idx]));
    this.$log = this.wmLogger.loggerFor(WmSlideRow);
    this.pages = [];
    this.currentPage = 0;
  }

  $onInit() {
    const { $transclude } = this;
    this.showExtraControls = $transclude.isSlotFilled('controls');
    this.showCustomHeader = $transclude.isSlotFilled('header');
    this.setShowArrows();
  }

  setCurrentTrackTransform() {
    // return a negative percentage based on the current page divided by the
    // total number of pages. e.g. 2 pages would be -100% because (1/2 * 100) * 2
    this.currentTrackTransform = `translate3d(-${(this.currentPage / this.pages.length * 100) * 2}%, 0, 0)`;
  }

  setShowArrows() {
    // return true if we have multiple pages, or the user tells
    // us they can lazy load more
    this.showArrows = this.pages.length > 1 || this.hasMore;
  }

  onNextPage() {
    this.currentPage = Math.min(this.pages.length - 1, this.currentPage + 1);
    this.setCurrentTrackTransform();
  }

  onPreviousPage() {
    this.currentPage = Math.max(0, this.currentPage - 1);
    this.setCurrentTrackTransform();
  }

  registerPage(page) {
    this.pages.push({ page });
    const result = { totalPages: this.pages.length, index: this.pages.length - 1 };

    // update the arrow state
    this.setShowArrows();

    return result;
  }
}

some_js.js

Next

import Document from 'next/document';
import { flush } from '@ghostgroup/universal-sass-loader/runtime';

export class WeedmapsDocument extends Document {
  static async getInitialProps({ req }) {
    const page = renderPage();
    const styles = flush(); // flush the styles used in the page render
    return { ...page, styles}
  }

  render() {
    return (
      <html lang="en">
        <Head>
           <style id="all-the-styles" dangerouslySetInnerHTML={{ __html: this.props.style.contents }}/>
        </Head>
        <body>
          <Main/>
          <NextScript/>
        </body>
      </html>
    );
  }
}

_document.js

Next

Result

Other Notable 💯

React is faster than Angular 1.x (duh)

Reusable Components across the org

Active Community for quick fixes and many examples -

https://github.com/zeit/next.js/tree/master/examples

Questions / Comments?

Made with Slides.com