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