Witbooker V8

Jorge Lucic (jvlucic)

Brand New Booking 

Harder Better Faster Stronger

Progress

  • Room Selector

  • Image Carousel

  • Booking Form

  • Inventory Line (40%)

  • Footer

  • Hotel Card

Total Progress 55%

  • Boilerplate

  • Routing handler

  • i18n Support

  • Calendar Component

  • Services Selector (75%)

  • Promotion List

What's Left

  • Language Selector

  • Reservation Receipt 

  • Cancelation

  • Booking Cart

  • TPV

  • Breadcrumb

  • SIPAY

  • Save as PDF

  • Messages

  • Collapsable Text

  • Currency Selector

  • Print Confirmation

Unexpected Obstacles

  • Putting together the boilerplate with the stack used

  • Connecting the Model with Selectors

  • Selectors and Immutable 

  • Decoupling components

  • Booking form responsive design 

  • Flex styling for certain components

  • Handling SSR

Previous estimation

18/03/2016 Specification Done 1 week design delay
21/03/2016 Estimation Done
28/03/2016 Development Starts 2 days external delay 
11/04/2016 First Deliverable Unexpected hardships
03/05/2016 Second Deliverable (demo) On Time but expected 75% progress
16/05/2016 Final Delivery Delayed
23/05/2016 Tests Done
06/06/2016 Pilot
20/06/2016 Production Release

Estimated Delivery

29/03/2016 Specification Done Semi-final Design Delivered 
30/03/2016 Estimation Done
01/04/2016 Development Starts 2 days external delay 
16/04/2016 First Deliverable
03/05/2016 Second Deliverable (demo)
31/05/2016 Final Delivery Delayed
06/06/2016 Tests Done
20/06/2016 Pilot
04/08/2016 Production Release

Hourly Calculation

Estimated Total Hours 311.5
Weekly Hours
(External 15, WBK 20)
35
Standard Duration 9 Weeks
Boost Duration 6 Weeks

Hourly Calculation

Estimated Total Hours 311.5
Weekly Hours
(External 15, WBK 15)
30
Standard Duration 10 Weeks
Boost Duration 8 Weeks

DEV TALK!

Redux Recap

Three Principles

  • Single Source of truth
  • State is read only
  • Reducers are pure functions

Three Principles

  • Single Source of truth
  • State is read only
  • Reducers are pure functions

Three Principles

  • Single Source of truth
  • State is read only
  • Reducers are pure functions

SIDE-EFFECTS

Middlewares

Middleware is created by composing functionality that wraps separate cross-cutting concerns which are not part of your main execution task.

ASYNC

 

PERSISTENCE

 

LOGGING

HOW DO THEY WORK?

MIDDLEWARE

CHAIN

ACTION

REDUCER

applyMiddleware(thunk, promise, logger)

thunk( promise ( logger ( dispatch ) ) ) (action)


export default function applyMiddleware(...middlewares) {
    return (createStore) => (reducer, initialState, enhancer) => {
        var store = createStore(reducer, initialState, enhancer)
        var dispatch = store.dispatch
        var chain = []

        var middlewareAPI = {
            getState: store.getState,
            dispatch: (action) => dispatch(action)
        }
        chain = middlewares.map(middleware => middleware(middlewareAPI))
        dispatch = compose(...chain)(store.dispatch)

        return {
            ...store,
            dispatch
        }
    }
}

Redux Saga

What is?

  • Sagas are like background processes


export function* getMovies() {
  yield* takeLatest(LOAD_MOVIES, fetchMovies);
}

What is?

  • Sagas yield effects

  •  

yield call(mdb.searchMovie, query);

// Effect -> call the function mdb.searchMovie with query obj as argument
{
  CALL: {
    fn: mdb.searchMovie,
    args: [query]  
  }
}
//Effects are like actions to the saga middleware!

Sagas use Generators

export function* fetchMovies(getState) {
...
    queryObj = { ...queryObj, ...( yield buildPeopleQuery(peopleQuery) ) };
...
}

function* buildPeopleQuery(people){
  ...
  for (let i = 0; i < people.length; i++) {
    ...
    let castId = yield cps([mdb, mdb.searchPerson], {query: person.query});
    ...
  }
  ...
}

Generators Detour

function *main(){
    var search_terms = yield Promise.all( [
        request( "http://some.url.1" ),
        request( "http://some.url.2" ),
        request( "http://some.url.3" )
    ] );

    var search_results = yield request(
        "http://some.url.4?search=" + search_terms.join( "+" )
    );
    var resp = JSON.parse( search_results );

}

Turn ASYNC into SYNC

It's not so simple...

It's not so simple...

    Q.spawn(function* () {
        let result = yield asyncFunc('http://example.com');
        console.log(result);
    });


    co(function* getResults(){
      var a = read('index.js', 'utf8');
      var b = request('http://google.ca');
      var res = yield [a, b];
    }).catch(function (ex) {
    
    });

What is?

  • Reducers are responsible for handling state transitions between actions.

  • Sagas are responsible for orchestrating complex/asynchronous operations.

Execution Flows

//PARALLEL

const [users, repos]  = yield [
  call(fetch, '/users'),
  call(fetch, '/repos')
]


//RACE 
function* fetchPostsWithTimeout() {
  const {posts, timeout} = yield race({
    posts   : call(fetchApi, '/posts'),
    timeout : call(delay, 1000)
  })
}

//CANCELLATION

export function* getMovies() {
  yield* takeLatest(LOAD_MOVIES, fetchMovies);
}

 RECAP

  • Sagas allow implementing complex flows

  • Sagas embrace generators, hiding callback abstractions

  • Sagas are easier to test

  • If your side-effects are simple, stick to thunks

Redux Tools

Normalizr

Flatten your nested entities

{
  "name": "Season 1",
  "overview": "The first season of the American television drama series Breaking Bad premiered...",
  "id": 3572,
  "season_number": 1,
  "episodes": [
    {
      "name": "Pilot",
      "overview": "When an unassuming high school chemistry...",
      "id": 62085,
      "season_number": 1,
      "vote_average": 8.5,
      "crew": [
        {
          "id": 2483,
          "name": "John Toll",
          "department": "Camera",
          "job": "Director of Photography",
          "profile_path": null
        }
      ...
      ],
      "episode_number": 1,
      "guest_stars": [
        {
          "id": 92495,
          "name": "John Koyama",
          "character": "Emilio Koyama",
          "order": 1,
          "profile_path": "/uh4g85qbQGZZ0HH6IQI9fM9VUGS.jpg"
        }
      ...
      ]
    },
    {"name": "Other Episode 1"},
    {"name": "Other Episode 2"},
    {"name": "Other Episode 3"},
    {"name": "Other Episode 4"},
    {"name": "Other Episode 5"}
  ]
}
const {Schema, arrayOf, } = require('normalizr');

const season = new Schema('seasons');

const episode = new Schema('episodes');

const crewMember = new Schema('crew');

const guestStar = new Schema('guestStars');

season.define({
    episodes: arrayOf(episode)
});

episode.define({
    crew: arrayOf(crewMember),
    guest_stars: arrayOf(guestStar)
});

exports.season  = season;
{
  "result": 3572,
  "entities": {
    "seasons": {
      "3572": {
        "air_date": "2008-01-19",
        "episodes": [62085,62086,62087,62088,62089,62090],
        "name": "Season 1",
        "overview": "The first season of the American television drama series Breaking Bad premiered ...",
        "id": 3572,
        "season_number": 1
      }
    },
    "crew": {
      "2483": {
        "id": 2483,
        "credit_id": "52b7029219c29533d00dd2c1",
        "name": "John Toll",
        "department": "Camera",
        "job": "Director of Photography",
        "profile_path": null
      },
      ...
    }
    "guest_stars":{
      "92495": {
        "id": 92495,
        "name": "John Koyama",
        "credit_id": "52542273760ee3132800068e",
        "character": "Emilio Koyama",
        "order": 1,
        "profile_path": "/uh4g85qbQGZZ0HH6IQI9fM9VUGS.jpg"
      },
      ...
    }
    "episodes": {
      "62085": {
        "episode_number": 1,
        "name": "Pilot",
        "overview": "When an unassuming high school chemistry ...",
        "id": 62085,
        "season_number": 1,
        "vote_average": 8.5,
        "crew": [ 2483, 66633, 66633, 1280071 ],
        "guest_stars": [92495,1223192,1216132,161591,1046460,1223197,61535,115688]
      },
      "62086": {"name": "Other Episode 1"},
      "62087": {"name": "Other Episode 2"},
      "62088": {"name": "Other Episode 3"},
      "62089": {"name": "Other Episode 4"},
      "62090": {"name": "Other Episode 5"},
    },
  }
}

Why?

  • Nested entities are bad for redux.

    • Hard to keep track of mutations

    • Hard to keep components isolated

    • Hard to update shared entities

    • Hard to cache entities

  • Smaller JSON, easier to reason about

Reselect

MapStateToProps

Input: State

Output: Props for component










const mapStateToProps = (state) => {
  const {
    home:{ movies: allMovieIds, selectedMovies, loading, error } = {},
    form: { syncFilters: {genre, score, year} = { } } = {},
    entities: {
      movies: movieEntities
      } = {}
    } = state.toJS() ; // ANTI-PATTERN!

  const allMovies = allMovieIds && allMovieIds.map( id => movieEntities[id]);

  const movies = allMovies && allMovies
    .filter(byGenre(genre.value || null))
    .filter(byScore(score.value || null))
    .filter(byYear(year.value || null));

  return { movies, selectedMovies, loading, error}

};
"filters": {
	"asyncFilters": { "movieQuery": "lego",}
},
"form": {
	"syncFilters": {
		"genre": { "value": "35"},
		"year": { "value": "2015",}
	}
}

MapStateToProps

Executes on each KeyPress

SOLUTION:

Create Memoized Selectors

const homeSelector = (state) => state.get('home');

export const moviesResultsSelector = createSelector(
  homeSelector,
  movieEntitiesSelector,
  (home, movieEntities) => home.get('movies') 
    && home.get('movies').map( id => movieEntities[id] )
);

export const filteredMoviesResultsSelector = createSelector(
  moviesResultsSelector, genreFilterSelector,scoreFilterSelector,
  searchTextFilterSelector,yearFilterSelector,
  (movies, genre, score, search, year) => (
    movies && movies
      .filter(byGenre(genre))
      .filter(byScore(score))
      .filter(byYear(year))
  )
);
"filters": {
	"asyncFilters": { "movieQuery": "lego",}
},
"form": {
	"syncFilters": {
		"genre": { "value": "35"},
		"year": { "value": "2015",}
	}
}
...
export const genreFilterSelector = createSelector(
  syncFiltersSelector,
  (syncFilters) => ( syncFilters && parseInt(syncFilters.genre.value) || null )
);

...

export const filteredMoviesResultsSelector = createSelector(
  moviesResultsSelector, genreFilterSelector,scoreFilterSelector,searchTextFilterSelector,yearFilterSelector,
  (movies, genre, score, search, year) => (
    movies && movies
      .filter(byGenre(genre))
      .filter(byScore(score))
      .filter(byYear(year))
  )
);

Composition

"filters": {
	"asyncFilters": { "movieQuery": "lego",}
},
"form": {
	"syncFilters": {
		"genre": { "value": "35"},
		"year": { "value": "2015",}
	}
}

Why?

  • Increase Performance

  • Keep your state minimal 

  • Reusability

ImmutableJS

var map1 = Immutable.Map({a:1, b:2, c:3});

var map2 = map1.set('b', 2);
assert(map1.equals(map2) === true);

var map3 = map1.set('b', 50);
assert(map1.equals(map3) === false);

We are only interested in doing work when something has changed

Why?

  • Performance Optimizations!

  • Mutations make everything harder to debug

  • Mutations do not trigger selectors

  • Mutations Kill time travel

  • If you are using React it works great with should ComponentUpdate / recompose pure

Redux Forms

function AsyncFilters(props) {
  return (
    ...
    <TextField  hintText="Movie name, plot" onChange={props.onChangeMovieQuery}/>
    ...
  )
}

<AsyncFilters onSearch={onSearch} onChangeMovieQuery={onChangeMovieQuery} ... /> 

function mapDispatchToProps(dispatch) {
  return {
    ...
    onChangeMovieQuery: (evt) => {
      dispatch(changeMovieQueryFilter(evt.target.value))
    },
    ...
  };
}

export function changeMovieQueryFilter(text) {
  return {
    type: CHANGE_MOVIE_QUERY_FILTER,
    text
  };
}

function filtersReducer(state = initialState, action) {
  switch (action.type) {
    case CHANGE_MOVIE_QUERY_FILTER:
      return state
        .setIn(['asyncFilters', 'movieQuery'], action.text);
    ...
  }
}
class SyncFilters extends Component {

  render() {
    const {fields: {searchText, genre, year, score}, handleSubmit, resetForm} = this.props;
    return (
      <SearchBar label="Name" placeholder={"Filter"} {...searchText}/>

      <select type="select" label="Genre" placeholder="Pick a Genre" 
                           value={genre.value || ''} {...genre}>
        <option>Pick a Genre</option>
        { ... }
      </select>

      <select type="select" label="Year" placeholder="Pick a Year" 
             value={year.value || ''} {...year}>
        <option >Pick a Year</option>
        { ...  }
      </select>
    );
  }
}

SyncFilters = reduxForm({
    form: 'syncFilters',
    fields: ['searchText', 'genre', 'year', 'score']
  })(SyncFilters);
{
	"form": {
		"syncFilters": {
			"genre": {
				"visited": true,
				"value": "16",
				"touched": true
			},
			"year": {
				"visited": true,
				"value": "2013",
				"touched": true
			}
		}
	}
}

Recap

  • Tools are a big help for perfomance and DX

  • Normalizr simplifies your nested data models helping with one source of thruth and easy updates

  • Selectors are great for reusability and optimization

  • ImmutableJS helps to keep mutations at bay

  • Redux-form obliterate lots of boilerplate code

Picking a boilerplate...

Thats it for today...

Thank you for coming :)

WitbookerV8

By Jorge Lucic

WitbookerV8

  • 419