Skip to content

Instantly share code, notes, and snippets.

@chadfurman
Last active May 1, 2017 07:54
Show Gist options
  • Save chadfurman/7a117f39680586bfb68fade59871e94f to your computer and use it in GitHub Desktop.
Save chadfurman/7a117f39680586bfb68fade59871e94f to your computer and use it in GitHub Desktop.
4-24.md

Dispatching Actions in Relay

  • our input isn't changing anymore, but we wrote the action, the action creator, the reducer, and the root reducer
  • We must now take the last step of dispatchind an aciton to redux
  • Let's go to landing.js and add a couple of things. (note that connect adds dispatch to props)
import React from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router'
import { setSearchTerm } from './actionCreators'
const { string, func } = React.PropTypes

const Landing = React.createClass({
  propTypes: {
    searchTerm: string,
    dispatch: func
  },
  handleSearchTermChange (event) {
    this.props.dispatch(setSearchTerm(event.target.value)) // we could have used mapDispatchToProps instead, but we didn't
  }.
  render () {
    return ( // note our input has an onChange which dispatches our value to redux
      <div className='landing'>
        <h1>svideo</h1>
        <input value={this.props.searchTerm} type='text' placeholder='Search' onChange={this.handleSearchTermChange} />
      </div>
    )
  }
})
  • dispatch == (create action -> root reducer)

Redux Review

  • start with an interface w/ input, type a letter
  • event "onChange" triggers on input which calls "handlesearchtermChange"
  • take in event in handlesearchTermChange and passes event.target.value to setSearchTerm
  • setSearchTerm is our action creator -- it tkaes in our searchTerm and returns an object (our action)
  • our action is { type: SET_SEARCH_TERM, searchTerm } -- we export SET_SEARCH_TERM from our actions
  • we get our action object back and pass it to dispatch
  • our action ends up in the reducer via dispatch, which has a case for each action and how to handle
  • mapDispatchToProps exists if you want it
  • action creators, actions, and reducers are in separate files. What about ducks? Dunno, lots of ways to organize

Search Submit Event (branch v2-19)

  • What is context? It's available everywhere in your react app
  • Context is a thing that libraries use -- you probably shouldn't use it. It's a 'fuut gun'
  • How to programatically navigate w/ react router?
contextTypes: { router: object }
...
this.context.router.transitionTo('/search')
  • Now, if we type in the input in the previous interface and "submit", we will be redirected to search

Using Redux in the Search Component

  • in search.js, import connect:
import React from 'react'
import { connect } from 'react-redux'
import ShowCard from './ShowCard'
import Header from './Header'
const { arrayOf, shape, string } = React.PropTypes

const Search = React.createclass({
  propTypes: {
    shows: arrayOf(shape({
      title: string,
      description: string
    })),
    searchTerm: string
  },
  render () {
    return (
      <div className='search'>
        <Header showSearch />
        <div>
          {this.props.shows
          .filter((show) => `${show.title} ${show.description}`.toUppercase().indexOf(this.props.searchTerm.toUppercase()) >= 0)
          .map((show) => {
            return (
              <ShowCard key={show.imdbID} {...show} />
            )
          })}
        </div>
      </div>
    )
  }
})

const mapStateToProps = (state) => {
  return {
    searchTerm: state.searchTerm
  }
}

export default connect(mapStateToProps)(Search)

Using Redux in the Header Component (v2-20)

  • in header.js
import React from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router'
import { setSearchTerm } from './actionCreators'
class Header extends React.component {
  handleSearchTermChange (event) {
    this.props.dispatch(setSearchTerm(event.target.value))
  } yu
  render () {
    let utilSpace
    if (this.props.showSearch) {
      utilspace = <input onChange={this.props.handleSearchtermChange} value={this.props.searchTerm} type='text' placeholder='Search' />
    } else {
      utilSpace = ''
    }
  }
}

const { func, bool, string } = React.PropTypes
Header.proptypes = {
  dispatch: func,
  showSearch: bool,
  searchTerm: string
}

export default Header

Redux DevTools

  • Redux devtools different from react devtools
  • you have to add something special to your store.js for the devtools to hook in
const store = createStore(rootReducer, compose(
  typeof window === 'object' && typeof window.devtoolsExtension !== 'undefined' ? window.devToolsExtension : (f) => f
))
  • You can embed the devtools in your app so you can use them i.e. on react native
  • Once installed, you can access it via green electron in bar.
  • DevTools can let you time-travel through all your actions, seeing each action's payload etc and stepping forward/backward

Q&A: Using Compose & Why to use Redux

  • What does compose do? If you want to do multiple "store enhancers" in a row
  • don't forget to set your node_env, build react in production mode
  • Use redux when you have lots of components reading from the same data

Middleware & Thunks

  • Async Redux
  • componentDidMount ajax calls are not okay w/ redux because React should only care about UI, redux should care about data
  • Right now, redux can only accept actions. Middleware enhances that.
  • You can dispatch "functions" using redux-thunks
  • What is a thunk?
const dollars = 10
const conversionRate = 1.1
const euros = dollars * conversionRate
// value of euros == 11
// converstionRate doesn't change in the above example
// but if we defined conversion rate as a function:

// now conversionRate is determined by this function, so it can be set dynamically
let conversionRate = () => 1.1
  • A thunk is a function that allows you to do things later instead of now.

Using Middleware & Thunks in Redux

  • store.js
import { createStore, compose, applyMiddleware } from 'redux
import thunk from 'redux-thunk'
import rootReducer from './reducers'

const store = createStore(rootReducer, compose(
  applyMiddleware(thunk), // now Redux can understand functions, or thunks, in addition to actions
  typeof window === 'object' && typeof window.devtoolsExtension !== 'undefined' ? window.devToolsExtension : (f) => f
))

export default store
  • actions.js
export const ADD_OMDB_DATA = 'ADD_OMDB_DATA'
  • thunks don't modify the store, they dispatch actions to modify the store, theu just dispatch them later
  • reducers.js
import { SET_SEARCH_TERM, ADD_OMDB_DATA } from './actions'

const DEFAULT_STATE = {
  searchTerm: '',
  omdbData: {}
}

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

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)
    case ADD_OMDB_DATA:
      return addOMDBData(state, action)
    default
      return state
  }
}

export default rootReducer

Async Actions

  • actionCreators.js
import { SET_SEARCH_TERM, ADD_OMDB_DATA } from './actions'
ipmort axios from 'axios'

export function setSearchTerm (searchTerm) {
  return { type: SET_SEARCH-TERM, searchTerm }
}

export function addOMDBData (imdbID, omdbData) {
  return { type: ADD_OMDB_DATA, imdbID, omdbData }
}

export functoin getOMDBDetails (imdbID) {
  return function (dispatch, getState) {  // redux calls this function for us when we call getOMDBDetails
    axios.get(`http://www.omdbapi.com/?i=${imdbID}`).then((response) => {
      dispatch(addOMDBData({imdbID, omdbData, response.data} )) // dispatch our action when axios returns w/ response data
    })
    .catch((error) => console.error(['axios error', error]))
  }
}

Adding Async Actions to the Details Component (v-2-21)

  • Details.js
import React from 'react'
import axios from 'axios'
import { connect } from 'react-redux'
import { getOMDBDetails } from './actionCreators'
import Header from './Header'
const {shape, string, func} = React.PropTypes

const Details = React.createClass({
  propTypes: {
    show: shape({
      title: string,
      year: string,
      poster, string,
      trailer: string,
      description: string,
      imdbID: string
  }),
  omdbData: shape({
    imdbID: string
  }).
  dispatch: func
},
componentDidMount () {
  if (!this.props.omdbData.imdbRating) { // if the data isn't there, request new data
    this.props.dispatch(getOMDBDetails(this.props.show.imdbID))  
  }
},
render () {
  const { title, description, year, poster, trailer } = this.props.show
  let raiting
  if (this.props.omdbData.imdbRating) {
    rating = <h1>{this.props.omdbData.imdbRating}</h1>
  else {
    rating = <img src="/public/img/loading.png' alt='loading indicator' />
  }
  
  return (
    <div className='details'>
      <Header />
      <section>
        <h1>{title}</h1>
        <h2>({year})</h2>
        <img src={`/public/img/posters/${poster}`} />
        <a>{description}</a>
      </section>
      <div>
        <iframe src={`https://www.youtube-nocookie.com/embed/${trailer}?rel=0&controls=0&showinfo=0`} frameBorder='0' allowFullScreen />
      </div>
    </div>
  )
}

const mapStateToProps = (state, ownProps) => {
 const omdbData = state.omdbData[ownProps.show.imdbID] ? state.omdbData[ownProps.show.imdbID]  : {}  
 return {
   omdbData
 }
}

export default connect(mapStateToProps)(Details)
  • redux-observables is a good way to do periodic updates
  • We no longer need to request the data all the time -- it's being handled by redux

Testing Redux: Updating the Snapshot

  • All of our tests are failing
  • Redux threw a lot of mess into our tests because our components depend on connect etc
  • redux has to be there or connect will fail
export const Unwrapped = Search
...
import { Unwrapped as UnwrappedSearch } from './Search'
...
const component = shallow(<UnwrappedShows ... > )
  • Our snapshot is failing because we added in the header, so npm run update-test

Testing Redux: Dispatching Actoins

  • We need to make redux think it's there
import { Provider } from 'react-redux'
import store from './store'
import { shallow, render } from 'enzyme'
import { setSearchTerm } from './actionCreators'

test('Search should render correct amount of shows based on search', () => {
  const searchWord = 'house'
  store.dispatch(setSearchTerm(searchWord))
  const component = render(<Proivder store={store}><Search shows={preload.shows} /></Provider>)
  const showCount = preload.shows.filter((show) => ${show.title} ${show.description}`.toUpperCase().indexOf(searchWord.toUppercase()) >= 0).length
  expect(component.find('.show-card').length).toEqual(showCount)
}

Testing Reducers

  • Reducers are much easier to test -- so easy to test, devtools will generate tests for us
  • Brian doesn't test thunks

Universal Renering

  • Our client doesn't have to play catch-up and render the react after it gets it
  • node does our rendering
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment