Redux Introduction II

Jorge Lucic (jvlucic)

fullstackDev @Witbooking

Give cheap excuse about why I won't say anything about ng-redux

What is this talk about?

  • Middlewares

  • Tools

  • What's next?

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

is a store enhancer

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
        }
    }
}

applyMiddleware(thunk, promise, logger)

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

export default function thunkMiddleware({ dispatch, getState }) {
  return next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState);
    }

    return next(action);
  };
}

 RECAP

  • Middlewares handle cross-cutting concerns

  • Middlewares sit between an action dispatch and reducer

  • Middlewares in Redux are chained like in Express 

But you don't need this . . .

Lets see some code...

Introducing

N&C

N&C gives you 

LIFE-CHANGING ANSWERS

Nolan Comedies?

Hitchcock Romance?

Text

Text

SYNC FILTERS

MOVIE RESULTS

ASYNC FILTERS

Redux Thunk


import { fromJS } from 'immutable';
import { routerMiddleware } from 'react-router-redux';
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';

export default function configureStore(initialState = {}, history) {

  const createStoreWithMiddleware = compose(
    applyMiddleware(routerMiddleware(history), thunk),
    window.devToolsExtension ? window.devToolsExtension() : f => f
  )(createStore);

  const store = createStoreWithMiddleware(
    createReducer(), 
    fromJS(initialState)
  );
  
....

  return store;
}
export function getMovies() {
  return function (dispatch, getState) {
    dispatch(loadMovies());
    const txt = getState()
        .get('filters')
        .get('asyncFilters')
        .get('movieQuery');
    return mdb.searchMovie( {query: txt}, (err, res) => {
      if(err){
        dispatch(moviesLoadingError(err));
      }

      dispatch(moviesLoaded(normalize(res.results, schema.MOVIE_ARRAY)));
    });
  };
}
export function moviesLoaded(movies) {
  return {
    type: LOAD_MOVIES_SUCCESS,
    movies: movies.result,
    entities: movies.entities
  };
}
function homeReducer(state = initialState, action) {
  switch (action.type) {
    case LOAD_MOVIES:
      return state
        .set('loading', 'true')
        .set('error', false)
        .set('movies', false);
    case LOAD_MOVIES_SUCCESS:
      return state
        .set('movies', action.movies)
        .set('loading', false);
    case LOAD_MOVIES_ERROR:
      return state
        .set('error', action.error)
        .set('loading', false);
    default:
      return state;
  }
}

Redux Promise


import { fromJS } from 'immutable';
import { routerMiddleware } from 'react-router-redux';
import { createStore, applyMiddleware, compose } from 'redux';
import promiseMiddleware from 'redux-promise';

export default function configureStore(initialState = {}, history) {

  const createStoreWithMiddleware = compose(
    applyMiddleware(routerMiddleware(history), promiseMiddleware),
    window.devToolsExtension ? window.devToolsExtension() : f => f
  )(createStore);

  const store = createStoreWithMiddleware(
    createReducer(), 
    fromJS(initialState)
  );
  
....

  return store;
}
export function getMovies(txt) {
  return {
    type: LOAD_MOVIES_SUCCESS,
    payload: searchMovie(txt).then(
      (res) => {
        const movies = normalize(res.results, schema.MOVIE_ARRAY);
        return {
          movies: movies.result,
          entities: movies.entities
        }
      }
    )
  }
}
function homeReducer(state = initialState, action) {
  switch (action.type) {
    case LOAD_MOVIES:
      return state
        .set('loading', 'true')
        .set('error', false)
        .set('movies', false);
    case LOAD_MOVIES_SUCCESS:
      return state
        .set('movies', action.payload.movies)
        .set('loading', false);
    case LOAD_MOVIES_ERROR:
      return state
        .set('error', action.error)
        .set('loading', false);
    default:
      return state;
  }
}
export default function thunkMiddleware({ dispatch, getState }) {
  return next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState);
    }

    return next(action);
  };
}
export default function promiseMiddleware({ dispatch }) {
  return next => action => {
    if (!isFSA(action)) {
      return isPromise(action)
        ? action.then(dispatch)
        : next(action);
    }

    return isPromise(action.payload)
      ? action.payload.then(
          result => dispatch({ ...action, payload: result }),
          error => dispatch({ ...action, payload: error, error: true })
        )
      : next(action);
  };
}

 RECAP

  • Redux thunk allows dispatching functions 

  • Redux-promise allows dispatching promises

  • Redux-promise is more restrictive

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.


import { fromJS } from 'immutable';
import { routerMiddleware } from 'react-router-redux';
import { createStore, applyMiddleware, compose } from 'redux';
import promiseMiddleware from 'redux-promise';
import sagaMiddleware from 'redux-saga';
import sagas from './sagas';

export default function configureStore(initialState = {}, history) {

  const createStoreWithMiddleware = compose(
    applyMiddleware(routerMiddleware(history), sagaMiddleware(...sagas)),
    window.devToolsExtension ? window.devToolsExtension() : f => f
  )(createStore);

  const store = createStoreWithMiddleware(
    createReducer(), 
    fromJS(initialState)
  );
  
....

  return store;
}
export function loadMovies() {
  return {
    type: LOAD_MOVIES
  };
}
export function moviesLoaded(movies) {
  return {
    type: LOAD_MOVIES_SUCCESS,
    movies: movies.result,
    entities: movies.entities
  };
}
export function* getMovies() {
  yield* takeLatest(LOAD_MOVIES, fetchMovies);
}
export function* fetchMovies() {
  const filters = yield select(filtersSelector);
  let txt = filters.get('asyncFilters').get('movieQuery');
  try{
    let res = yield cps([mdb, mdb.searchMovie ], {query: txt});
    yield put(moviesLoaded(normalize(res.results, schema.MOVIE_ARRAY)));
  }catch (e){
    yield put(moviesLoadingError(e));
  }
}
function homeReducer(state = initialState, action) {
  switch (action.type) {
    case LOAD_MOVIES:
      return state
        .set('loading', 'true')
        .set('error', false)
        .set('movies', false);
    case LOAD_MOVIES_SUCCESS:
      return state
        .set('movies', action.movies)
        .set('loading', false);
    case LOAD_MOVIES_ERROR:
      return state
        .set('error', action.error)
        .set('loading', false);
    default:
      return state;
  }
}

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);
}

Why use Middlewares at all?

// action creator
// needs to dispatch, so it is first argument
function loadData(dispatch, userId) { 
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  // don't forget to pass dispatch
  loadData(this.props.dispatch, this.props.userId); 
}

 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

What's next

START YOUR OWN PROJECT!

Code Structure

    actions/
      todos.js
    components/
      todos/
        TodoItem.js
        ...
    constants/
      actionTypes.js
    reducers/
      todos.js
    index.js
    rootReducer.js

ENTER DUCKS!

const LOAD   = 'my-app/widgets/LOAD';
const CREATE = 'my-app/widgets/CREATE';
const UPDATE = 'my-app/widgets/UPDATE';
const REMOVE = 'my-app/widgets/REMOVE';

export default function reducer(state = {}, action = {}) {
  switch (action.type) {
    // do reducer stuff
    default: return state;
  }
}

export function loadWidgets() {
  return { type: LOAD };
}

export function createWidget(widget) {
  return { type: CREATE, widget };
}

export function updateWidget(widget) {
  return { type: UPDATE, widget };
}

export function removeWidget(widget) {
  return { type: REMOVE, widget };
}

Going a bit further

Looks Familiar?


    //ANGULAR
    
    controllers/
    directives/
    services/
    templates/
    index.js

    //RAILS
    
    app/
      controllers/
      models/
      views/

Please Read This!

Picking a boilerplate...

My two cents..

START YOUR OWN!!

Know your tools!

  • Tech and frameworks change everyday, underlying principles not so often
  • You may be getting much more that you bargained for.
  • Wow, webpack loading times +40s
  • Boilerplates are opinionated, have your own opinion!
  • You will have to change something at some point.

Thats it for today...

Thank you for coming :)

Advanced-Redux

By Jorge Lucic

Advanced-Redux

  • 174
Loading comments...

More from Jorge Lucic