This post is part of the series 30 Days of React.

In this series, we're starting from the very basics and walk through everything you need to know to get started with React. If you've ever wanted to learn React, this is the place to start!

Redux actions

Edit this page on Github

With Redux in place, let's talk about how we actually modify the Redux state from within our applications.

Yesterday we went through the difficult part of integrating our React app with Redux. From here on out, we'll be defining functionality with our Redux setup.

As it stands now, we have our demo application showing the current time. But there currently isn't any way to update to the new time. Let's modify this now.

Triggering updates

Recall that the only way we can change data in Redux is through an action creator. We created a redux store yesterday, but we haven't created a way for us to update the store.

What we want is the ability for our users to update the time by clicking on a button. In order to add this functionality, we'll have to take a few steps:

  1. Create an actionCreator to dispatch the action on our store
  2. Call the actionCreator onClick of an element
  3. Handle the action in the reducer

We already implemented the third step, so we only have two things to do to get this functionality working as we expect.

Yesterday, we discussed what actions are, but not really why we are using this thing called actionCreators or what they are.

As a refresher, an action is a simple object that must include a type value. We created a types.js file that holds on to action type constants, so we can use these values as the type property.

export const FETCH_NEW_TIME = 'FETCH_NEW_TIME';
export const LOGIN = 'USER_LOGIN';
export const LOGOUT = 'USER_LOGOUT';

As a quick review, our actions can be any object value that has the type key. We can send data along with our action (conventionally, we'll pass extra data along as the payload of an action).

{
  type: types.FETCH_NEW_TIME,
  payload: new Date().toString()
}

Now we need to dispatch this along our store. One way we could do that is by calling the store.dispatch() function.

store.dispatch({
  type: types.FETCH_NEW_TIME,
  payload: new Date().toString()
})

However, this is pretty poor practice. Rather than dispatch the action directly, we'll use a function to return an action... the function will create the action (hence the name: actionCreator). This provides us with a better testing story (easy to test), reusability, documentation, and encapsulation of logic.

Let's create our first actionCreator in a file called redux/actionCreators.js. We'll export a function who's entire responsibility is to return an appropriate action to dispatch on our store.

import * as types from './types';

export const fetchNewTime = () => ({
  type: types.FETCH_NEW_TIME,
  payload: new Date().toString(),
})

Now if we call this function, nothing will happen except an action object is returned. How do we get this action to dispatch on the store?

Recall we used the connect() function export from react-redux yesterday? The first argument is called mapStateToProps, which maps the state to a prop object. The connect() function accepts a second argument which allows us to map functions to props as well. It gets called with the dispatch function, so here we can bind the function to call dispatch() on the store.

Let's see this in action. In our src/views/Home/Home.js file, let's update our call to connect by providing a second function to use the actionCreator we just created. We'll call this function mapDispatchToProps.

import { fetchNewTime } from '../../../redux/actionCreators';
  // ...
const mapDispatchToProps = dispatch => ({
  updateTime: () => dispatch(fetchNewTime())
})
  // ...
export default connect(
  mapStateToProps,
  mapDispatchToProps,
)(Home);

Now the updateTime() function will be passed in as a prop and will call dispatch() when we fire the action. Let's update our <Home /> component so the user can press a button to update the time.

const Home = (props) => {
  return (
    <div className="home">
      <h1>Welcome home!</h1>
      <p>Current time: {props.currentTime}</p>
      <button onClick={props.updateTime}>
        Update time
      </button>
    </div>
  );
}

Although this example isn't that exciting, it does showcase the features of redux pretty well. Imagine if the button makes a fetch to get new tweets or we have a socket driving the update to our redux store. This basic example demonstrates the full functionality of redux.

Multi-reducers

As it stands now, we have a single reducer for our application. This works for now as we only have a small amount of simple data and (presumably) only one person working on this app. Just imagine the headache it would be to develop with one gigantic switch statement for every single piece of data in our apps...

Ahhhhhhhhhhhhhh...

Redux to the rescue! Redux has a way for us to split up our redux reducers into multiple reducers, each responsible for only a leaf of the state tree.

We can use the combineReducers() export from redux to compose an object of reducer functions. For every action that gets triggered, each of these functions will be called with the corresponding action. Let's see this in action.

Let's say that we (perhaps more realistically) want to keep track of the current user. Let's create a currentUser redux module in... you guessed it: src/redux/currentUser.js:

touch src/redux/currentUser.js

We'll export the same four values we exported from the currentTime module... of course, this time it is specific to the currentUser. We've added a basic structure here for handling a current user:

import * as types from './types'

export const initialState = {
  user: {},
  loggedIn: false
}

export const reducer = (state = initialState, action) => {
  switch (action.type) {
    case types.LOGIN:
      return {
        ...state, user: action.payload, loggedIn: true};
    case types.LOGOUT:
      return {
        ...state, user: {}, loggedIn: false};
    default:
      return state;
  }
}

Let's update our configureStore() function to take these branches into account, using the combineReducers to separate out the two branches

import { createStore, combineReducers } from 'redux';

import { rootReducer, initialState } from './reducers'
import { reducer, initialState as userInitialState } from './currentUser'

export const configureStore = () => {
  const store = createStore(
    combineReducers({
      time: rootReducer,
      user: reducer
    }), // root reducer
    {
      time: initialState,
      user: userInitialState
    }, // our initialState
  );

  return store;
}

export default configureStore;

Let's also update our Home component mapStateToProps function to read it's value from the time reducer

// ...
const mapStateToProps = state => {
  // our redux store has `time` and `user` states
  return {
    currentTime: state.time.currentTime
  };
};
// ...

Now we can create the login() and logout() action creators to send along the action on our store.

export const login = (user) => ({
  type: types.LOGIN,
  payload: user
})
  // ...
export const logout = () => ({
  type: types.LOGOUT,
})

Now we can use the actionCreators to call login and logout just like the updateTime() action creator.

Phew! This was another hefty day of Redux code. Today, we completed the circle between data updating and storing data in the global Redux state. In addition, we learned how to extend Redux to use multiple reducers and actions as well as multiple connected components.

However, we have yet to make an asynchronous call for off-site data. Tomorrow we'll get into how to use middleware with Redux, which will give us the ability to handle fetching remote data from within our app and still use the power of Redux to keep our data.

Good job today and see you tomorrow!

Learn React the right way

The up-to-date, in-depth, complete guide to React and friends.

Download the first chapter

Ari Lerner

Hi, I'm Ari. I'm an author of Fullstack React and ng-book and I've been teaching Web Development for a long time. I like to speak at conferences and eat spicy food. I technically got paid while I traveled the country as a professional comedian, but have come to terms with the fact that I am not funny.

Connect with Ari on Twitter at @auser.