Cabin – React & Redux Example App – Redux

7 min read
Nick P.
Nick P.
Published May 25, 2016 Updated November 8, 2019

_This is the 3rd post for Cabin, the React & Redux Example App Tutorial series created by Stream. The final result of following this series is your own feature-rich, scalable social network app built with React and Redux!_ Visit getstream.io/cabin for an overview of all the tutorials, as well as a live demo. The source code can be found on the Stream GitHub repository for Cabin, and all blog posts can be found at their respective links below:

  1. Introduction
  2. React
  3. Redux
  4. Stream
  5. Imgix
  6. Keen
  7. Algolia
  8. Mapbox
  9. Best Practices and Gotchas

Introduction

We have all heard of Redux (unless you’ve been living in a cabin for the past several months), and, probably, at least pretended that we understand this popular tool. Since its initial debut by Dan Abramov, it’s popularity has soared to nearly 20k stars on GitHub. As an open source project, this amount of growth is impressive, really. We, at Stream, felt it was time to show you how you can actually utilize it, instead of just talking about it so you sound like you know what you’re doing when you go to Startup Events. In this blog post, we’ll outline why we’ve decided to use the oh-so-popular React state container, Redux, to help us keep our code for Cabin clean and tidy. In addition, we’ll dig into the inner workings of Redux so that you better understand when and how to apply Redux to your current (or next) React application.

So, What’s the Big Deal?

To put it simply, wrangling state in a single page application (aka SPA) is flat out hard to handle — and when I say state, I’m not only referring to the data coming from the API, but also the UI state in an application. It’s “state” that determines which navigational element is considered “active”, what view is currently showing, etc. Generally, when building a SPA, you would simply read the DOM. However, doing so simply doesn’t scale well. If you’re storing state in the DOM, you’ll likely make a mess of your application (spaghetti code), causing a ton of headaches as your application scales… ick. This is where Redux really shines. Read the following definition from redux.js.org: “[Redux] helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test. On top of that, it provides a great developer experience, such as live code editing combined with a time traveling debugger. You can use Redux together with React, or with any other view library.” On top of that, it’s only 2kB, including dependencies. Amazing, right?

Redux doesn’t care about the DOM!

Redux doesn’t render DOM elements, it doesn’t tell you how to handle routing, etc. It’s all about maintaining application state. It’s like the purple unicorn of state management for SPAs. It does this by holding onto every change in the “store”. The store holds the whole state tree of your application, and the only way to change the state inside is to dispatch an action on it. A store is not a class, it’s simply an object with a few methods on it. Take a look at each of the methods:

How Do I Actually

There are several core concepts that you will need to understand. Go through these core concepts of Redux:

1. Single Source of Truth

Redux uses only one store for all its application state. Since all state resides in one place, Redux calls this a single source of truth. The data structure of the store is ultimately up to you, but it’s typically a deeply nested object for a real application. Note: Deeply nested objects can sometimes be hard to work with, so we suggest taking a look at Normalizr.

2. State is Read-Only

According to Redux docs, "The only way to mutate the state is to emit an action, an object describing what happened." This means that the application cannot modify the state directly. Instead, “actions” are dispatched to express an intent to change the state in the store. Remember: The store object itself has a very small API with only four methods:

  • store.dispatch(action)
  • store.subscribe(listener)
  • store.getState()
  • replaceReducer(nextReducer)

So as you can see, there’s no method of setting state. Therefore, dispatching an action is the only way for the application code to express a change of state. Check out a code example for dispatching an action::

const action = {
  type: 'ADD_USER',
  user: { name: 'Nick' }
};
// assuming a store object has already been created
store.dispatch(action);

“Actions are payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using store.dispatch().”

3. Changes Made With Pure Functions

As just described, Redux doesn't allow the application to make direct changes to the state. Instead, the dispatched action "describes" the state change and an intent to change state. Reducers, on the other hand, are functions that you write which handle dispatched actions and can actually change the state. A reducer takes in the current state as an argument and can only modify the state by returning a new state. Check out this example of a reducer:

const reducer = function(state, action) { // reducer function
  …
return state;
}

Pure Functions

Reducers should be written as pure functions. Read and understand what a pure function is:

  • It does not make outside network or database calls.
  • Its return value depends solely on the values of its parameters.
  • Its arguments should be considered “immutable”, meaning they should not be changed.
  • Calling a pure function with the same set of arguments will always return the same value.

Extra Credit: Get a more in-depth overview of pure functions reading the Wikipedia article.

Getting Started With Our First Action

Open app/modules/actions/Comments.js and check out this code block:

import * as axios from 'axios';
import config from 'config';
export const ADD_COMMENT = 'PHOTOS_ADD_COMMENT';
export const _addCommentRequest = id => ({ type: ADD_COMMENT, id });
export const _addCommentResponse = (id, comment, user) => ({
    type: ADD_COMMENT,
    id,
    comment,
    user,
});
export function addComment(id, text) {
    return (dispatch, getState) => {
        const user = getState().User;
        dispatch(_addCommentRequest(id));
        const data = {
            user_id: user.id,
            upload_id: id,
            comment: text,
        };
        axios
            .post(${config.api.baseUrl}/comments, data, {
                headers: {
                    Authorization: Bearer ${localStorage.getItem('jwt')},
                },
            })
            .then(res => {
                dispatch(_addCommentResponse(id, res.data, user));
            });
    };
}

Here’s a brief summary of what’s happening in the code above:

  • _addCommentRequest is dispatched by our action whenever we are in a “loading” state. We want the reducer to know that we are about to send it a payload. This lets you show a loading icon if you want to provide feedback to the user. You could also show a disabled state on the button that the user just clicked in order to prevent repeat clicks.
  • We make an API call using Axios to get our comments from the server. We are passing in our token (all typical API/Client stuff), but the magic is what happens next...
  • Now that we have our response from the database, we dispatch a second event “_addCommentResponse” which contains our actual payloads so that our application can then present it.

Moving on to the Reducer

In Cabin’s case, there are a dozen or more reducers, so we’ll focus on one of the easiest to comprehend in this example application, our Comment reducer. Take a look at this example of a reducer::

const initialState = {
  comments: [],
  uploadID: null,
}
function Comments(state = initialState, action) {
    switch (action.type) {
        case CommentActions.LOAD_COMMENTS:
            if (action.comments) {
                return Object.assign({}, state, {
                    comments: [...action.comments.map(c => camelizeKeys(c))],
                    uploadID: action.postID,
                });
            }
            return initialState;
        case CommentActions.ADD_COMMENT:
            if (action.comment) {
                const user = camelizeKeys(action.user);
                return Object.assign({}, state, {
                    comments: [
                        Object.assign({}, camelizeKeys(action.comment), {
                            firstName: user.firstName,
                            lastName: user.lastName,
                            createdAt: new Date(),
                        }),
                        ...state.comments,
                    ],
                });
            }
            return state;
    }
    return state;
}

Let’s break down what is going on in the code above:

  • We first define an initial state for the application so that we can gracefully merge the payloads received from the actions into the state.
  • Our reducer is now defined and we have the state and action to work with. The state is provided by Redux and the action is the action that we trigger from our component.
  • Next, we check the type of the action that has been provided (the type is mandatory). If the type matches an event that is relevant to this store (EVERY event/action is passed to every reducer, so it is up to you to sort out what you need -- this could be helpful for having several reducers react to a single action), then we catch it and carry out our logic.
  • Once we get into our switch/case we have to check to see if the payload has been passed (remember that we submit two actions and one is for loading), and if it has been passed, we will merge that into our state.
  • Important: We mentioned before that our reducers cannot modify the state directly. Javascript handles everything by reference, so when we merge our new state with the old one, we have to be careful to not modify the state directly. Adding the empty object as the first parameter to Object.assign allows us to modify that empty object rather than the state (aka immutability).

Putting It All Together

Through our “smart” components, we are connected to the Redux store. Using this connection, we are able to dispatch our action; in our example, we perform the action of adding a comment to a post. When a comment form is submitted, we simply have to dispatch the action with the data. Look at how we do this:

this.props.dispatch(CommentActions.addComment(this.props.photo.id, this.state.text))

Viola! Now you understand how to combine an action, a reducer and a dispatch event with Redux.

Conclusion

In the next post we’ll cover how we’re using Stream to power the feed for Cabin. Add your email on getstream.io/cabin or follow @getstream_io on Twitter to stay up to date about the latest Cabin tutorials. This tutorial series has been created by getstream.io. Try this 5m interactive tutorial to learn how Stream works.

Hungry for More Redux?

You’re in luck. We’ve compiled a list of our favorite tutorials and resources, just for you: