The next thing we want to do with our app is make the front page’s search work so that when you type in a search query and hit enter it will automatically have searched for that on the Search page. Right now you have all the necessary tools to do that via state. You could just push the query term up to the ClientApp level and then pass that down to the Search and you’d be done. And that’s the way you should do it given how small our app is.

But when these demo apps all the fun is in over engineering it and that’s precisely what we’re going to do: we’re going to add redux. redux is a fantastic tool and a cool blending of the ideas of Facebook’s Flux and the Elm architecture.

As a side-note, there are some super rad new tools out there like [Mobx][mobx] that you can check out, but we’re sticking to Redux. Mobx is incredible but with more power comes more complexity. If you learn Redux then learn Mobx (and reactive programming) you’ll really appreciate and/or fear the power that comes from Mobx.

So what is Redux? Redux is a predictable state container for JavaScript apps. The best part about it while the concept is at first hard, I’d argue it’s also very simple and elegant. Redux is great because it will run both client and server side, it’s easy to test, and easy to debug. While Redux does not not follow the Flux pattern, you can easily see the similarities and once you’ve done one the other isn’t hard to adapt to.

With Redux you a single store which stores your entire app state in a single tree. This is not like Flux where you’ll have many stores for many different parts of your app; all data lives in a single store. You cannot directly modify the tree of data stored in this tree by typical assignment (ie tree.prop = 'foo' doesn’t work.) Rather, every time you want to modify the tree, you emit an action. Your action then kicks off what’s called a reducer. A reducer is a special function that take a tree and parameter(s) and returns a new tree after applying whatever transformations it deems fit. The way it gets away with just one store is when you need more data you just add more branches to your data tree. Like React? You only have one tree of components and when you need more you just add more nodes (branches) to your components.

So let’s do the most basic addition of Redux to our app and convert the Search to use Redux. Again, this is using a sledgehammer to solve a tiny nail problem: huge overkill.

Create a reducers.js, put this in there:

const DEFAULT_STATE = {
  searchTerm: ''
}

const rootReducer = (state = DEFAULT_STATE, action) => {
  switch (action.type) {
    default:
      return state
  }
}

export default rootReducer

Create a store.js and put this:

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

const store = createStore(rootReducer)

export default store

This is about as bare bones as Redux gets: we boot strapped a Redux store with a single top-level reducer and exported that. One thing you’re going to find with Redux is there’s a long path to follow to follow how your state changes. A very predicatble and consistent path, but it’s still way longer than it used to be when we were just dealing with React state. This will often not be worth it. Evaluate this yourself on a per-project basis.

So like we said, each store starts with one reducer: the root reducer. This root reducer in turn will dispatch to other reducers. A few keys to notice here:

  1. You must return the finished state each time.
  2. You must handle action types you’ve never seen before (which why we have the default clause.)
  3. You take in state, you copy it, and you return a new state. That’s what any reducer does. If you return the same state, Redux thinks nothing happened and won’t inform React of any changes.
  4. You must have a default state.
  5. Redux by itself has no way of dealing with async actions. You need to pull in another library like redux-thunk. We’ll use that later.

Okay make a new file called actions.js and put in there:

export const SET_SEARCH_TERM = 'SET_SEARCH_TERM'

Create a file called actionCreators.js:

import { SET_SEARCH_TERM } from './actions'

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

Now back to reducers.js:

// import at top
import { SET_SEARCH_TERM } from './actions'

// new reducer above rootReducer
const setSearchTerm = (state, action) => {
  const newState = {}
  Object.assign(newState, state, {searchTerm: action.searchTerm})
  return newState
}

// add new case before default inside rootReducer
case SET_SEARCH_TERM:
  return setSearchTerm(state, action)

More files! This should be it for our simple project. Actions is just going to a bunch of exporting of constants. Why do we do this? The way Redux’s root reducer decides to dispatch it to one of various reducers is by the action type. Thus it needs to match in both the action creator and the reducer. Rather than having magic strings, we have one central source of truth both file read from. Makes refactoring easy too.

The actionCreator is what the UI is actually going to interact with to make changes to the Redux store. In other words, your UI never directly interacts with the store nor the reducers. It only interacts with action creators which then are handled in the reducers which then change the store which then inform the UI of the changes. One way data flow!

If you haven’t seen the syntax const x = { searchTerm } it just means const x = { searchTerm: searchTerm }. It’s just a shortcut.

The rootReducer uses the same SET_SEARCH_TERM constant to hinge in the rootReducer. Also note we return a new object every time when we make a new object. This lets Redux know to inform any subscribers (in this case your React app) that changes happened.

Okay, so let’s go make landing interact with the store. But first we need to connect Redux to React via the react-redux package. Go to ClientApp.js.

// import react-redux and your new store
import { Provider } from 'react-redux'
import store from './store'

// wrap everything in router in provider
render () {
  return (
    <HashRouter>
      <Provider store={store}>
        []
      </Provider>
    </HashRouter>
  )
}

Provider connects React to Redux for you. Now you can magically use a connect function (also provided from react-redux) that allows you to pull in the pieces of state you need in each component. Let’s got make Landing.js read and write to Redux.

// imports at the top
import { connect } from 'react-redux'
const { string } = React.PropTypes

// add propType
propTypes: {
  searchTerm: string
},

// replace input
<input value={this.props.searchTerm} type='text' placeholder='Search' />

// at the bottom
const mapStateToProps = (state) => {
  return {
    searchTerm: state.searchTerm
  }
}

export default connect(mapStateToProps)(Landing)

Connect is a function that allows your component to tap into the Redux store’s state. The mapStateToProps allows you to select which pieces of state are passed into your component which helps keep thing clean. At the bottom we export a connected version of the component. Now if you reload the page the input doesn’t work for the same reason it didn’t with React previously: we are never sending the typed text to Redux to update its state. Let’s do that now.

// at top
import { connect } from 'react-redux'
import { setSearchTerm } from './actionCreators'

// inside React.createClass
propTypes: {
  searchTerm: string,
  dispatch: func
},
handleSearchTermChange (event) {
  this.props.dispatch(setSearchTerm(event.target.value))
},

// change input
<input onChange={this.handleSearchTermChange} value={this.props.searchTerm} type='text' placeholder='Search' />

// at the bottom
const mapStateToProps = (state) => {
  return {
    searchTerm: state.searchTerm
  }
}

export default connect(mapStateToProps)(Landing)

We’re importing the actionCreator we created to be able to dispatch the correct action. We’re then tying the input to a change handler and the prop that’s going to be passed in. At the bottom we’re connecting this component to Redux via connect. mapStateToProps takes in the whole state tree via the state param and returns which props you want passed into the component. Connect makes that magic happen and also passes in a dispatch function which allows to dispatch actions to Redux via our actionCreators. Inside the change handler we do just that: call this.props.dispatch and call the actionCreator here. While this seems like a weird contract to deal with, it’s worth it. The contract is that you can always pass dispatch the result of a actionCreator. For normal, synchronous actions like this it’s a little convuluted but once we start dealing with async actions this contract helps a lot to simplify your UI code and contain the chaos to your actionCreators. Let’s make it actually transition to Search.js once you hit enter.

// add contextTypes
contextTypes: {
  router: object
},

// add method to Landing
goToSearch (event) {
  event.preventDefault()
  this.context.router.transitionTo('/search')
},

// surround input with form
<form onSubmit={this.goToSearch}>
  <input onChange={this.handleSearchTermChange} value={this.props.searchTerm} type='text' placeholder='Search' />
</form>

So we’re introducing a new concept here from React: context. This is a dangerous tool and I will tell you I personally have never put anything on context. I’ve only consumed things from context that libraries like react-router and react-redux (which both do use context) put on there. Use at your own peril.

Context is basically global state: anywhere inside a React app can read and write to state. If this sounds nightmarish to you then you have good sense: it defeats a lot of the benefits to React. However, with something like react-router it’s very useful because the whole app does care about routing, as it does about Redux.

Notice the contextTypes are like propTypes. However, contextTypes are even more important to React than propTypes: if you don’t have them the object you’re looking for won’t be there. In other words, you must identify in contextTypes the properties the component cares about or they will not be available on context.

Okay, so we’re using a form to take care of when hits enter: this is good for accessibility and a good way to take care of submitting. Once a user hits enter, it calls goToSearch where we imperatively call the router to take us to search. This will preserve our Redux state; however Search.js is not yet reading from Redux. Let’s go fix that.

// import at top
import { connect } from 'react-redux'

// add propType not inside shows
searchTerm: string

// change header
<Header showSearch />

// change state to props inside of filter
.filter((show) => `${show.title} ${show.description}`.toUpperCase().indexOf(this.props.searchTerm.toUpperCase()) >= 0)

// add at bottom, replace export
const mapStateToProps = (state) => {
  return {
    searchTerm: state.searchTerm
  }
}

export default connect(mapStateToProps)(Search)

Notice we got to delete a lot of code. Always feels good! We’re externalizing our state management so that’ll happen more as well. Also notice that Search no longer cares about modifying searchTerm since it itself doesn’t need to. This is cool; having concerns live where they happen is a really positive thing. Otherwise not much new here. This will work now if you go to Landing and submit a search term from there. However we’ve broke the header. Let’s go fix that.

// import at top
import { connect } from 'react-redux'
import { setSearchTerm } from './actionCreators'

// delete handleSearchTerm propType
// add to propTypes
dispatch: func

// add method to Header
handleSearchTermChange (event) {
  this.props.dispatch(setSearchTerm(event.target.value))
},

// change input to not call this.props.handleSearchTermChange but this.handleSearchTermChange (not on the state object)
utilSpace = <input type='text' placeholder='Search' value={this.props.searchTerm} onChange={this.handleSearchTermChange} />

// at the bottom
const mapStateToProps = (state) => {
  return {
    searchTerm: state.searchTerm
  }
}

export default connect(mapStateToProps)(Header)

Since Header does care about modifying searchTerm we bring in that logic here. Otherwise not much changes!