A first-hand journey from the developers leading the charge at Hotel Engine.

Speakers:

Michelle Lim, Frontend Engineer

Clayton Weller, Backend Engineer

John Siladie, Vice President of Engineering

 

From Angular to React:

A JavaScript Journey

 

Who are we?

Michelle Lim

Clayton Weller

John Siladie

Hotel Engine is a Denver-based,
high-growth travel tech company.

 

What we do:

  • Simple business travel lodging
  • Offer up to 60% off rates
  • Over 200,000 hotels World wide
  • Lodging management tools
  • FREE!

About Hotel Engine

The Rundown

  • Who are we?
  • Chat with John
  • Give you the Rundown
  • Why we are even giving
    this presentation?

  • What did we decide to do?

  • How are we doing it?

  • When?

NOW!

Why are we
even giving this presentation?

The year was 2008...

Only surviving version of 2008 logo

Fast forward to 2014...

Current Logo... much better

Now it's 2019...

How we chose to move forward

Alternatives to AngularJS

Why React?

Our Situation

  • Small team

  • Large app

  • Need to keep pushing out new features

  • Currently working in a CI/CD framework

  •  

Solution

Replace one component at a time

Demo time!

Replace an Angular component with React equivalent

0. Walk through Angular app
1. Build React component
2. Embed it in Angular

<!DOCTYPE html>
<html ng-app="demo.app">

<head>
    <meta charset="utf-8">
    <title>Trip Bucket List</title>
    <link rel="stylesheet" href="./public/app.css">
</head>

<body>

    <bucket-list></bucket-list>

    <script type="text/javascript" src="./public/vendor.js"></script>
    <script type="text/javascript" src="./public/app.js"></script>

</body>

</html>
index.html

{ Angular root document }

(function () {

    'use strict';

    angular.module('demo.app')
        .component('bucketList', {  // aka <bucket-list>
            template: require('./bucket-list.html'),
            controller: BucketListController
        });

    function BucketListController() {
        const $ctrl = this;

        $ctrl.trips = require('../../data/trips').default;
        $ctrl.trip = {};

        $ctrl.addTrip = addTrip;
        $ctrl.updateTrip = updateTrip;

        function addTrip() {
            // does it
        }

        function updateTrip() {
            // does it
        }
    }

})();
bucket-list.component.js

{ Angular component definition }

<div>
  
  <h1>Trip Bucket List</h1>

  <div>
    <!-- Add Trip form -->
    <form>
      <input placeholder="City" ng-model="$ctrl.trip.city"/>
      <input placeholder="Country" ng-model="$ctrl.trip.country"/>
      <button ng-click="$ctrl.addTrip()">Add Trip</button>
    </form>
    
    <!-- Trips -->
    <div ng-repeat="trip in $ctrl.trips track by trip.id">
      
      <img ng-src="{{trip.image}}" alt="{{trip.city}}"/>
      <span>{{trip.city}}, {{trip.country}}</span>
      
      <!-- Trip checkbox -->
      <input type="checkbox" ng-change="$ctrl.updateTrip()" ng-model="trip.completed" />
      <label for="checked"></label>
    
    </div>
  
  </div>
  
</div>
bucket-list.html

{ Angular component template }

Angular files

index.htmlbucket-list.component.jsbucket-list.html

Markup (JSX)

from Angular template

Functionality

from Angular controller

{ Build React component }

BucketList.jsx

{ Build React component }

BucketList.jsx - break out components?
import React from 'react';
import { AddTripForm, TripsTable } from '../../index';

const BucketList = ({ trips }) => (
  
  <div>
  
    <h1>Trip Bucket List</h1>
  
    <AddTripForm />
  
    <TripsTable trips={trips} />
  
  </div>
  
);

export default BucketList;

install library

import { react2angular } from 'react2angular';
import { BucketList } from 'react-components';

export default angular
  .module('demo.app')
  .component('reactBucketList', react2angular(BucketList, ['trips']));
react-bucket-list.component.js

New Angular component from React

{ Expose as Angular component }

import { react2angular } from 'react2angular';
import { BucketList } from 'react-components';

export default angular
  .module('demo.app')
  .component('reactBucketList', react2angular(BucketList, ['trips']));

(function () {

  'use strict';

  angular.module('demo.app')
    .component('reactBucketListWrapper', {
    
      template: `<react-bucket-list trips="$ctrl.trips"></react-bucket-list>`,
      controller: BucketListWrapperController
    
    });

  function BucketListWrapperController() {

    const $ctrl = this;

    $ctrl.trips = require('../../data/trips').default;

  }

})();

Wrap new component & pass in props

Finally, replace the old Angular component

<!DOCTYPE html>
<html ng-app="app">

<head>
    <meta charset="utf-8">
    <title>Trip Bucket List</title>
    <link rel="stylesheet" href="./public/app.css">
</head>

<body>

    <!-- <bucket-list></bucket-list> // original Angular component -->
    
    <react-bucket-list-wrapper></react-bucket-list-wrapper>

    <script type="text/javascript" src="./public/vendor.js"></script>
    <script type="text/javascript" src="./public/app.js"></script>

</body>

</html>
index.html

Demo intermission

#SessionSelfies time!

#DENStartupWeek

What about a whole app?

What about state?

State

REDUX!

Redux can be used with any framework

The full process

Bucket-list-wrapper

Demo time!

Connect a React component to the global Redux
store so it can interact with the Angular app

0. Walk through Redux layer
1. Bind store to Angular component
2. Pass store to React component

trips: [
  {
    id: 1,
    city: 'Seoul',
    country: 'South Korea',
    image: 'https://www.webuildvalue.com/static/upload/seo/seoul-megacity-growth-sustainable.jpg',
    completed: false
  },
  {
    id: 2,
    city: 'Vancouver',
    country: 'British Columbia',
    image: 'https://www.hellobc.com/content/uploads/2018/03/2-6787-vancouver-skyline.jpg',
    completed: true
  },
  {
    id: 3,
    city: 'Lisbon',
    country: 'Portugal',
    image: 'https://www.discoverwalks.com/blog/wp-content/uploads/2018/03/lisbonintwodays-816x538.jpg',
    completed: false
  },
]

Initial state

export const addTrip = (state = initialState, { trip }) => ({
  ...state,
  trips: [
    ...state.trips,
    trip
  ]
});

export const updateTrip = (state = initialState, { trip }) => {
  const updatedTrips = state.trips.map(t => {
    if (t.id === trip.id) {
      return trip;
    }
    return t;
  });

  return {
    ...state,
    trips: updatedTrips
  };
};

Redux actions

install library

{ Bind & pass the store }

import { react2angular } from 'react2angular';
import { BucketList } from 'react-components';

export default angular
  .module('app')
  .component('reactBucketList', react2angular(BucketList, ['store']));

(function () {

  'use strict';

  angular.module('app')
    .component('reactBucketListWrapper', {
    
      template: `<react-bucket-list store="$ctrl.store"></react-bucket-list>`,
      controller: BucketListWrapperController
    
    });

  function BucketListWrapperController($ngRedux) {

    const $ctrl = this;

    $ctrl.store = $ngRedux;

  }
react-bucket-list.component.js

{ Use React <Provider> if children need store }

import React from 'react';
import { AddTripForm, TripList } from '../../index';
import { Provider } from "react-redux";


const BucketList = ({ store }) => (
  
    <Provider store={store}>
  
        <div>
          
            <h1>Trip Bucket List</h1>

            <AddTripForm />
              
            <TripList />
              
        </div>

    </Provider>

);

export default BucketList;
BucketList.jsx

{ Use `connect` to access store from parent }

import React from 'react';
import { connect, useDispatch, useSelector } from "react-redux";
import { Creators as TripActions } from '../../Redux/Actions/Trips';

const TripList = () => {

    const trips = useSelector(state => state.Trips.trips);
    const dispatch = useDispatch();

    const handleCheckbox = trip => {
        dispatch(TripActions.updateTrip({
            ...trip,
            completed: !trip.completed
        }));
    }

    return (
        <div>
            {trips.map(trip =>
                <div key={trip.id}>
                    ...
                </div>
            )}
        </div>
    );
}

export default connect(null, null)(TripList);
TripsTable.jsx

React

AngularJS

Redux

(shared)

+ React Router

Retrospective

 

  • Drawbacks

  • Benefits

  • Lessons learned

It gets complicated

event or query

new data

???

const updateTrip = () => {
  scope.$apply(() => {
    $ctrl.completed = !$ctrl.completed
  });
};

Angular function

const updateTrip = () => {
  $scope.$apply(() => {
    $ctrl.completed = !$ctrl.completed
  });
};

Angular function

Other upgrades

We're hiring!

 

 

View our open positions here:

hotelengine.com/careers

Thanks!

Chat with us.

Add your contact info to the sign-in sheet for resource links, our deck,  & hackathon information

Angular to React: A JavaScript Journey (Hotel Engine)

By Michelle Lim

Angular to React: A JavaScript Journey (Hotel Engine)

Presented at Denver Startup Week 2019 by Michelle Lim, Clayton Weller, and John Siladie

  • 1,208