Day 2, Segment 3
Branch v2-19
Refactor the above using
The search field still doesn't work. We will fix this next.
// **** Landing.js ****
import React from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router'
// 2. ++++ import our action
import { setSearchTerm } from './actionCreators'
const { string, func, object } = React.PropTypes
const Landing = React.createClass({
propTypes: {
searchTerm: string,
// 4. ++++ declare the dispatch
dispatch: func
},
// 1. ++++
handleSearchTermChange (event) {
this.props.dispatch(setSearchTerm(event.target.value))
},
render () {
return (
<div className='landing'>
<h1>svideo</h1>
// 3. ++++ add the onChange handler
<input onChange={this.handleSearchTermChange} value={this.props.searchTerm} type='text' placeholder='Search' />
<Link to='/search'>or Browse All</Link>
</div>
)
}
})
const mapStateToProps = (state) => {
return {
searchTerm: state.searchTerm
}
}
export default connect(mapStateToProps)(Landing)
The connect
from React-redux library automatically adds a dispatch
function that lets you dispatch actions to Redux. Q&A: it's recommended to always declare and pull out the just the minimum of what you need from the Redux state, as in
mapStateToProps
above. Otherwise it can get very messy. With the above pattern it's still props down, actions up.
Dispatch
You create and action and dispatches it to the rootReducer.
Overview of the Redux cycle
- type in the search field
- fires the onChange in the input
handleSearchTermChange
will call our actionCreator- that creates the properly formatted action definition ({type:[correct_name], searchTerm:searchTerm} (the type name is defined in action.js)
- then dispatches this action definition to Redux rootReducer
- then rootReducer creates (through the appropriate reducer) the new state with the new searchTerm.
- It gets back to the store.
- Redux has a new store and the
connect
method above is subscribed to this store. And it callsforceUpdate
in React - that re-renders the app
Refactor the above using mapDispaptchToProps
You create a
mapDispatchToProps
function, that you pass to connect
, so now dispatchSetSearchTerm
injected as a prop to Landing.
// **** Landing.js ****
import React from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router'
import { setSearchTerm } from './actionCreators'
const { string, func, object } = React.PropTypes
const Landing = React.createClass({
contextTypes: {
router: object
},
propTypes: {
searchTerm: string,
dispatchSetSearchTerm: func
},
handleSearchTermChange (event) {
// 3. ++++
this.props.dispatchSetSearchTerm(event.target.value)
},
render () {
return (
<div className='landing'>
<h1>svideo</h1>
<input onChange={this.handleSearchTermChange} value={this.props.searchTerm} type='text' placeholder='Search' />
<Link to='/search'>or Browse All</Link>
</div>
)
}
})
const mapStateToProps = (state) => {
return {
searchTerm: state.searchTerm
}
}
// 1. ++++
const mapDispatchToProps = (dispatch) => {
return {
dispatchSetSearchTerm (searchTerm) {
dispatch(setSearchTerm(searchTerm))
}
}
}
// 2. ++++ map it here
// now dispatchSetSearchTerm is injected as a property
export default connect(mapStateToProps, mapDispatchToProps)(Landing)
16:00The advantage of this pattern is that it's less dependent on Redux, since you're not using
dispatch
directly. Now you could use Landing outside of Redux. This is more common in real life.
Q&A: Keep the old states?
You would store it as a separate value in Redux. (newSearchTerm and oldSearchTerm for example).
Navigating the browser to the search results
The easiest is just adding a form:
// **** Landing.js ****
import ...
const { string, func, object } = React.PropTypes
const Landing = React.createClass({
// declare the context types
// see explanation below
contextTypes: {
router: object
},
propTypes: {..},
handleSearchTermChange (event) {..},
// 2. ++++ add the handler
handleSearchSubmit (event) {
// prevent the form from submitting
event.preventDefault()
// want to navigate search page
// 4. ++++ then navigate with React router
this.context.router.transitionTo('/search')
},
render () {
return (
<div className='landing'>
<h1>svideo</h1>
// 1. ++++ add the form
<form onSubmit={this.handleSearchSubmit}>
<input onChange={this.handleSearchTermChange} value={this.props.searchTerm} type='text' placeholder='Search' />
</form>
<Link to='/search'>or Browse All</Link>
</div>
)
}
})
const mapStateToProps = (state) => {..}
const mapDispatchToProps = (dispatch) => {..}
export default connect(mapStateToProps, mapDispatchToProps)(Landing)
context
The third type after props and state. This is much less used, and much more dangerous.
You rarely use it directly, but usually through libraries.
Context is available everywhere in your React app. It's a global for your app. This is why it can be dangerous, as you can easily loose track of what's coming from where (when debugging).
contextTypes
It's very hard to read or modify context. You have to create and declare
contextTypes
otherwise it doesn't show up in contexts. BrowserRouter actually uses context internally.
Navigate programatically with React Router (v4)
The above snippet:
this.context.router.transitionTo('/search')
And of course all the concept about context, as just explained above.
So now, we do get sent to the search page, but not getting the filtered list yet.
branch v2-20Q&A: Can you do routing in Redux? There's a library called Redux-Router, maintained by the same people as React-Router.
Make Search read from Redux
29:00
// **** Search.js ****
import React from 'react'
// 1. ++++ import connect
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
})),
// 2. ++++ add searchTerm
searchTerm: string
},
// 5. ---- remove these
// no longer handled by React
// getInitialState () {
// return {
// searchTerm: ''
// }
// },
// handleSearchTermChange (event) {
// this.setState({searchTerm: event.target.value})
// },
render () {
return (
<div className='search'>
// 3. ---- remove the old header
// since it's not reading
// from redux
// <Header
// showSearch
// searchTerm={this.state.searchTerm}
// handleSearchTermChange={this.handleSearchTermChange}
// />
// 4. ++++ the new header
<Header showSearch />
<div>
{this.props.shows
// 6. ++-- read the searchTerm from props
// this.props.searchTerm.toUpperCase()
.filter((show) => `${show.title} ${show.description}`.toUpperCase().indexOf(this.props.searchTerm.toUpperCase()) >= 0)
.map((show) => {
return (
<ShowCard key={show.imdbID} {...show} />
)
})}
</div>
</div>
)
}
})
// 7. ++++
// add the map
// pull out the searchTerm subset
const mapStateToProps = (state) => {
return {
searchTerm: state.searchTerm
}
}
// 8. ++++ connect
export default connect(mapStateToProps)(Search)
Now, Search is correct, butUpdate Header to read from Redux
import React from 'react'
// 1. ++++
import { connect } from 'react-redux'
// 2. ++++
import { setSearchTerm } from './actionCreators'
import { Link } from 'react-router'
class Header extends React.Component {
// 6. ++++
// make sure `this` is pointing to the
// right context in
// handleSearchTermChange
constructor (props) {
// call my parent constructor with this
super(props)
this.handleSearchTermChange = this.handleSearchTermChange.bind(this)
}
// 4. ++++ add the handler
handleSearchTermChange (event) {
// see step 6. to make sure this is pointing to
// the right context
this.props.dispatch(setSearchTerm(event.target.value))
}
render () {
let utilSpace
if (this.props.showSearch) {
// 5. ++--
utilSpace = <input onChange={this.handleSearchTermChange} value={this.props.searchTerm} type='text' placeholder='Search' />
} else {
utilSpace = (
<h2>
<Link to='/search'>
Back
</Link>
</h2>
)
}
return (
<header>
<h1>
<Link to='/'>
svideo
</Link>
</h1>
{utilSpace}
</header>
)
}
}
const { func, bool, string } = React.PropTypes
Header.propTypes = {
// 3. ++++ proptype for dispatch
dispatch: func,
showSearch: bool,
searchTerm: string
}
// 7. ++++ create the subset
const mapStateToProps = (state) => {
return {
searchTerm: state.searchTerm
}
}
// 8. ++++ connect
export default connect(mapStateToProps)(Header)
Note : in ES6 classes, there's no "," between methods. Q&A: Migrating from an existing ES5 app?
React does not need to own your entire app. You can start with just the leaf nodes. Then start replacing more and more components. Maybe marry Backbone and React together.
Branch v2-21
Now the app is complete. We will migrate the AJAX call to Redux as well.
45:00