This is Part 3 of 3 in the series. Here are the previous articles, if needed:

For the front end we’ll use some great technologies.

  • React as the main framework
  • Redux as an alternative to Flux
  • Webpack as a module bundler/build system
  • Babel for ES6 language support

Actions, Stores, and Reducers, Oh My!

Redux has different concepts from other Flux implementations, or straight React for that matter. These concepts have a learning curve associated with them, but offer a lot of benefits. The first part of the article explains a few of them.

Single Source of Truth

The entire state of your application is stored in a single place. This is a big departure from each component managing its own state. The benefit is that applications become more predictable. We can even reproduce an application at any point. Redux also comes with a nice developer tool that allows you to view all actions that have happened and jump to different states of the application. If we’re trying to find a bug, having the ability to move back in time to a point before the bug occurred and look at the state is very useful. This capability allows you to see what action and state change may have triggered the bug.

State Is Read-Only

In order to make all of the above "fanciness" happen, we need to be careful not change the state directly. With React we're already used to using this.setState({ name: ‘mulder’ }), instead of this.state.name = ‘mulder’. We also have to be careful not to change objects stored in state. For example, say our state looks like this:

{
user: {
  name: 'mulder'
},
...
}

If we want to change the name, it might look like this:

var user = this.state.user;
user.name = 'fox mulder';
this.setState({ user: user });

This would work fine in React, but it changes the this.state.user object directly. So, there’s no difference between the state before and after the setState command.

Since state in Redux needs to be immutable (can’t be changed once it’s set) we need to return a new object for any state changes. For example, (in React not Redux):

var user = Object.assign({}, this.state.user, {name: 'fox mulder'});
this.setState({ user: user });

Only Actions Can Trigger State Changes

In Redux you emit an action when you want something to happen. Then, your reducer handles the action and changes the state accordingly.

Actions

An action is an event that happens in your application. This can be something simple like a name change, or more complex like hitting an API and retrieving data. Actions are simply a function that returns an object. Take the following for example:

function beAwesome() {
return {
 type: 'BE_AWESOME',
};
}

The Action Creator

An Action Creator is a function that is called to create an action, like the following:

function makeMeAwesome() {
return (dispatch) => dispatch(beAwesome());
}

Store

Redux only has one store. This holds all of the application’s state. But luckily, you can map reducers to different parts of the store to keep different parts of the application separate.

Reducers

A reducer receives an action and makes a change to the state. Reducers don’t perform any action (like pulling data from an API). Reducers only handle the result of an action. That way, their function is very clear and they are easily testable. If we put X in we should always get Y out, no matter when and where it’s called.

Getting Started with Our Application

The first step is build our actions and reducers. That enables us to have the main logic of our application in place. After that, we can easily build out our components and have them work. For searching our API, we’ll start by setting up two actions:

  • requestSearch
  • receiveSearch

  • When the user enters a search term and presses the Enter key, that triggers the requestSearch action. That action tells our application to show the loader and tell the user that we’re searching.

    export const REQUEST_SEARCH = 'REQUEST_SEARCH';
    
    export function requestSearch(search) {
    return {
    type: REQUEST_SEARCH,
    search,
    };
    }
    
    function fetchSearchFromServer(search) {
    return dispatch => {
    dispatch(requestSearch(search));
    return fetch(`http://0.0.0.0:5000/api/search/${search.term}/${search.next || 0}`)
      .then(req => req.json())
      .then(json => dispatch(receiveSearch(search, json)));
    }
    }
    
    function shouldFetchSearch(state, search) {
    if (!search || !search.term) return false;
    
    const results = state.searchResults[search.term];
    
    if (results && results.isFetching) {
    return false;
    } else {
    return true;
    }
    }
    
    export function fetchSearch(search) {
    return (dispatch, getState) => {
    if (shouldFetchSearch(getState(), search)) {
      return dispatch(fetchSearchFromServer(search));
    }
    };
    }
    
  • Now in our application, we can call fetchSearch({ term: ‘react’ }). It first checks that the search isn’t already fetching, then calls fetchSearchFromServer(), which dispatches the requestSearch action (so we show the user that we’re searching). Then, it makes the API call. Once the API call is finished, it dispatches the receiveSearch action, which handles the results.

  • Let’s look at the receiveSearch action. It simply creates a new action object with the parts of the API response we want to pass to our reducer, which actually updates the state.

    export const RECEIVE_SEARCH = 'RECEIVE_SEARCH';
    
    function receiveSearch(search, json) {
    return {
    type: RECEIVE_SEARCH,
    items: json.packages,
    receivedAt: Date.now(),
    search: Object.assign({}, search, { next: json.next, start: search.next }),
    };
    }
    
  • Now that we have those actions set up, let’s write our reducer. Reducers are simple functions that get passed an action and the current state. The reducer then returns a new state. A reducer doesn't perform any actions; so it can be run with any arbitrary data and will always return the same thing. Our packagesBySearch function will get called when an action happens. It’ll then look at the action and, if it’s RECEIVE_SEARCH, it updates the state for that search term.

    import { RECEIVE_SEARCH, REQUEST_SEARCH } from '../actions/search';
    
    export function packagesBySearch(state = {}, action) {
    switch (action.type) {
    case RECEIVE_SEARCH:
    return Object.assign({}, state, {
      [action.search.term]: searchResults(state[action.search.term], action)
    });
    default:
    return state;
    }
    }
    
    function searchResults(state = {
    isFetching: false,
    didInvalidate: false,
    items: [],
    search: {},
    }, action) {
    
    switch (action.type) {
    case REQUEST_SEARCH:
    return Object.assign({}, state, {
      isFetching: true,
      didInvalidate: false,
      search: action.search,
    });
    
    case RECEIVE_SEARCH:
    return Object.assign({}, state, {
      isFetching: false,
      didInvalidate: false,
      items: action.items,
      lastUpdated: action.receivedAt,
      search: action.search,
    });
    
    default:
    return state;
    }
    }
    
  • Our state now looks something like this:

    {
    react: {
    isFetching: false,
    lastUpdated: 1440713404979,
    items: [{...}, {...}],
    search: { term: 'react'},
    }
    }
    
  • Now we can initial the search like this:

    dispatch(fetchSearch({ term: 'react' }));
    

Building the React Components

Now that the basic search logic is set up we can start building our React components. All of the components except the root component will receive props for the full application state.

  1. We’ll start with the <ScoutApp /> component and use react-redux to bind the Redux store to our component’s props. React-redux exposes a connect method that we call with functions as arguments. Our state is then passed through those functions to props of our component as in the following:

    function mapStateToProps(state) {
    const { search, searchResults, packages, selectedPackages } = state;
    
    const singleSearchResult = searchResults[search.term] || {};
    const selectedPackagesData = selectedPackages.map((id, index)=>packages[id]);
    
    return {
    search,
    searchResults: singleSearchResult,
    selectedPackages: selectedPackagesData,
    };
    }
    
    export default connect(mapStateToProps)(ScoutApp);
    
  2. The component now has search, searchResults, and selectedPackages all as props. The props are then passed down the hierarchy to the other components.

  3. The rest of the components are considered “dumb”, meaning they just receive props and render accordingly.

Summary

That's how you build a React frontend with Redux. What a combination! Bring together NMP, GitHub, React, and Redux -- you're off and running. You can view the full source here: asamiller/scoutjs-frontend.

Don't have an account on CenturyLink Cloud? No problem. Get started for free and receive a substantial credit toward any of our products or services. Sign up for our Developer-focused newsletter CODE. Designed hands-on by developers, for developers. Keep up to date on topics of interest: tutorials, tips and tricks, and community building events.