Day 2, Segment 3, Part 2

In notebook:
FrontEndMasters React Intro 2
Created at:
2016-12-27
Updated:
2016-12-28
Tags:
libraries React JavaScript
45:00

Redux dev tools

Branch v2-21

Redux has great dev tools. Install React Dev Tools for Chrome, Firefox. 
Even React Native can use it, since you can embed it in React. 

Instrumenting Redux Dev Tools

You need to activate it in your Redux Store:
  // ****   store.js    ****

import { createStore, compose } from 'redux'
import rootReducer from './reducers'

// 1. ++++
// we only want to run it in the browser
// not universal rendering
const store = createStore(rootReducer, compose(
  // if we're in the browser
  // and check if the tools are installed
  typeof window === 'object' && typeof window.devToolsExtension !== 'undefined' ? window.devToolsExtension() : (f) => f
))

export default store
The above code is direct from the documentation. eg. ​(f) => f
Now you get the devtools icon in the browsers address bar. 

Time travelling debugging

You can visualise all the actions happening (dispatched) and observe them. Now you can also start walking backwards to replay the actions and see the results.  
You can also remove any past actions. 

Q&A: Learning materials for React?

React is a very small library so hard to recommend more resources. Maybe going through some open source React projects such as the mobile version of Reddit which is open-source and well written. 
Also Electron and the Firefox Dev Tools are both written in React. debugger.html.
These are really great libraries written by brilliant people. 
Anything that Dan Abromov wrote (my note: loved his Egghead.io course on Redux)

Q&A: What is ​compose​ in the above code?

Not really sure. If you want to run multiple Store enhancers. 
For production: master build tools, so the courses on FrontendMasters

Q&A: How to implement show filtering with Redux?

You would of course move everything (the shows) to Redux. And do the filtering in ​mapStateToStore​. 

Asynchronous Redux

We've seen the core of Redux. Actions in Redux are synchronous (unless you really rewrite it or add addons).

Saving the AJAX request to Redux

In Details.js we're doing an AJAX request. We could dispatch this data to Redux, but it's not ideal. Better to do all this in Redux. 
All the data fetching should live in Redux. 

Middlewares

With middlewares, you can dispatch other data to Redux than actions. 
We'll use a middleware to add data fetching. 
  • redux-promise 
  • redux-observables
  • redux-thunks

Redux Thunk

(explained in Kyle's course. Thunk is basically an ES5 promise. That is it gives back it's value in a later time)
He explains thunk as a function you have call and it returns a value (like promise.then). So the value is determined when the expression is called instead of when it was defined.
When it was defined: ​var conversionRate = 1.1​ → You use it like this: ​var euros = 1000 * conversionRate
When it was called: ​conversionRate()​ (returns the current conversion rate) → You use it like this: ​var euros = 1000 * conversionRate()
Since the thunk can return its value at a later time, we can introduce asynchronicity in Redux. 
Add the thunk module
  // ****   store.js    ****

// 2. ++++ import applyMiddleware
import { createStore, compose, applyMiddleware } from 'redux'
// 1. ++++ add thunk
import thunk from 'redux-thunk'
import rootReducer from './reducers'

// 3. add applyMiddleware
const store = createStore(rootReducer, compose(
  applyMiddleware(thunk),
  typeof window === 'object' && typeof window.devToolsExtension !== 'undefined' ? window.devToolsExtension() : (f) => f
))

export default store
Now we can use thunks in Redux. 

Update action definitions

  // ****   actions.js    ****

export const SET_SEARCH_TERM = 'SET_SEARCH_TERM'
export const ADD_OMDB_DATA = 'ADD_OMDB_DATA'
It's still actions that modify the store, and it's still synchronous. Thunks just delay the dispatching of actions. 

Update reducers to use omdb data

  import { SET_SEARCH_TERM, ADD_OMDB_DATA } from './actions'

const DEFAULT_STATE = {
  searchTerm: '',
  // 1. ++++
  omdbData: {}
}

const setSearchTerm = (state, action) => {
  const newState = {}
  Object.assign(newState, state, {searchTerm: action.searchTerm})
  return newState
}

// 2. ++++ merge the old state with the new
// example of "mixing in data", multiple levels deep
const addOMDBData = (state, action) => {
  const newOMDBData = {}
  Object.assign(newOMDBData, state.omdbData, {[action.imdbID]: action.omdbData})
  const newState = {}
  Object.assign(newState, state, {omdbData: newOMDBData})
  return newState
}

const rootReducer = (state = DEFAULT_STATE, action) => {
  switch (action.type) {
    case SET_SEARCH_TERM:
      return setSearchTerm(state, action)
      // 3. ++++ handle the ADD_OMDB_DATA action
    case ADD_OMDB_DATA:
      return addOMDBData(state, action)
    default:
      return state
  }
}

export default rootReducer

Create the action handlers

First the synchronous 
  // ****   actionCreators.js   ****

// 1. ++++ import ADD_OMDB_DATA as well
import { SET_SEARCH_TERM, ADD_OMDB_DATA } from './actions'
// 3. ++++ import axios
import axios from 'axios'

export function setSearchTerm (searchTerm) {
  return { type: SET_SEARCH_TERM, searchTerm }
}

// 2. ++++
// so far addOMDBData is only used inside this module
// Brian usually uses functions to return these objects
export function addOMDBData (imdbID, omdbData) {
  return { type: ADD_OMDB_DATA, imdbID, omdbData }
}

// 3. ++++ we're adding a thunk here
export function getOMDBDetails (imdbID) {
  // thunk here (return the function)
  // (we don't use getState here but good to know)
  // Redux will call this inner function
  return function (dispatch, getState) {
    // just paste it from the Details.js component
    axios.get(`http://www.omdbapi.com/?i=${imdbID}`)
      .then((response) => {
        // once done...
        dispatch(addOMDBData(imdbID, response.data))
      })
      .catch((error) => console.error('axios error', error))
  }
}
Then use it in Details.js
  // ****   Details.js    ****

import React from 'react'
import axios from 'axios'
// 1. ++++ import these 2 modules
import { connect } from 'react-redux'
import { getOMDBDetails } from './actionCreators'
import Header from './Header'
// 2. ++++ add func 
const { shape, string, func } = React.PropTypes

const Details = React.createClass({
  propTypes: {
    show: shape({
      ... }),
    // 3. ++++ 
    omdbData: shape({
      imdbID: string
    }),
    dispatch: func
  },
  componentDidMount () {
    // 4. ++-- rewrite completely
    // if we navigate the second time
    // to this page, then don't need to request the data again
    if (!this.props.omdbData.imdbRating) {
      this.props.dispatch(getOMDBDetails(this.props.show.imdbID))
    }
  },
  render () {
    const { title, description, year, poster, trailer } = this.props.show
    let rating
    // 5. ++++ the rating comes from props now
    if (this.props.omdbData.imdbRating) {
      rating = <h3>{this.props.omdbData.imdbRating}</h3>
    } else {
      rating = <img src='/public/img/loading.png' alt='loading indicator' />
    }
    return (
      <div className='details'>
        <Header />
        <section>
          <h1>{title}</h1>
          <h2>({year})</h2>
          {rating}
          ...
      </div>
    )
  }
})

// 6. ++++
// possible to add a second parameter (ownProps)
// We will need the imdbID property
const mapStateToProps = (state, ownProps) => {
// this conditional format is called ternary
  const omdbData = state.omdbData[ownProps.show.imdbID] ? state.omdbData[ownProps.show.imdbID] : {}
  return {
    omdbData
  }
}

// 7. ++++ then connect
export default connect(mapStateToProps)(Details)

​ownProps​ 

The props that Details (the React component) receives (defined in the ​propTypes​ property above). We will need the ​imdbID​ property. 

Q&A Do periodic refresh of the data?

React-observables is great for this.