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.html
→ bucket-list.component.js
→ bucket-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
react2angular
github.com/coatue-oss/react2angular
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
ng-redux
github.com/angular-redux/ng-redux
{ 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