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)
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)
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 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
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
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
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
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)
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)
}
Reducers are much easier to test -- so easy to test, devtools will generate tests for us
Brian doesn't test thunks
Our client doesn't have to play catch-up and render the react after it gets it
node does our rendering