DEV Community

Andrew Evans
Andrew Evans

Posted on • Originally published at rhythmandbinary.com on

How to get Started with React Redux

The Mandalorian and Baby Yoda working with Redux on their ship the Razor Crest. The original image was copied from here

Redux is one of the most popular patterns that is in use in the frontend world today. You see the same pattern in not only React, but Angular and Vue as well. Redux is very powerful as it provides a routine way that you can manage state in your applications. Moreover, Redux scales as your projects get larger. So it works great for both small and enterprise applications.

This post is going to walkthrough how to use Redux in your React applications. I'm going to assume that you understand some basics about React Hooks as I'm going to be using the useState, useEffect, useSelector and useDispatch hooks respectively.

I'm also going to be walking through a sample project that I've setup at my GitHub repo here. We will be walking through different phases of the same project. I'm going to walk you through (1) setting up Redux, (2) adding actions and reducers, and (3) creating side effects.

As a Mandalorian fan myself, the sample project will be a mini fan site with pages for episode info, quotes, and more.

Redux Concepts

So before we dive into using Redux, we should cover some vocabulary that we'll be using in the subsequent sections.

image was copied from my article "How to Start Flying with Angular and NgRx" here

Redux is a way to centrally organize your applications state in what's called a store (in the diagram that's the block in pink). The idea is that everything about your application will be stored there, and then you'll use selectors in your components to access this state. The store is immutable which means that it cannot change. When you "change" the store, you are actually generating a new version. This is a concept you see in functional programming, and sometimes can be hard for newer folks to understand. I highly recommend watching Russ Olsen's talk on Functional Programming here for more on this concept.

Your components fire what are called actions that then go through reducers to modify the values in the store. The idea behind reducers is that the state is reduced from an action. An action can be any event that your application does from initial loading of data to responding to a button click. The reducers in your application handle the changes to the store that result.

Your components also subscribe to selectors which basically listen for any type of state change. Whenever the store updates, the selectors receive the updates and allow you to render your components accordingly.

Some actions can generate "side effects" which are usually HTTP calls. This would be when you want to call an API to get values to put in the store. The flow there is that you would (1) fire an action, (2) call an API through an effect, and then return an action that goes through a reducer to modify the store.

I know that this is a lot of vocabulary to start, but it will make more sense as we begin to add Redux to our application.

Starting Out

So if you view my sample project, you'll find that it has the following folders:

  1. start
  2. redux-setup
  3. redux-actions
  4. redux-http

We're going to walkthrough the folders in the project in this order. We will begin in the start folder, as that's a version of the application with no Redux at all. Then the three other folders are completed phases of this project:

  1. redux-setup is the start with redux added and an initial set of actions, reducers, selectors, and effects for the episodes page.
  2. redux-actions is the start with the episodes and quotes actions, reducers, selectors, and effects setup.
  3. Finally, redux_http includes a set of actions, reducers, selectors and an effect that makes an actual HTTP call.

When you're finished, you'll have a mini Mandalorian fan page that includes a page for episodes, quotes, a video of Season 2, and even a way to send a contact message.

Initial Setup

We'll start by cloning the project, and then going into the start folder.

The initial project looks like this:

.
├── README.md
├── ReduxFlow.png
├── package-lock.json
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── manifest.json
│   └── robots.txt
└── src
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── components
    │   ├── Header.js
    │   └── index.js
    ├── config
    │   ├── episodes.json
    │   └── quotes.json
    ├── index.css
    ├── index.js
    ├── logo.svg
    ├── pages
    │   ├── ContactPage.js
    │   ├── EpisodesPage.js
    │   ├── HomePage.jpg
    │   ├── HomePage.js
    │   ├── QuotesPage.js
    │   ├── Season2Page.js
    │   └── index.js
    ├── serviceWorker.js
    ├── setupTests.js
    └── styles
        ├── _contact.scss
        ├── _episodes.scss
        ├── _header.scss
        ├── _home.scss
        ├── _quotes.scss
        ├── _season2.scss
        └── styles.scss

Enter fullscreen mode Exit fullscreen mode

The first step is to add Redux to your application and then install the necessary libraries. Go ahead and install the libraries with npm by doing the following:

npm i react-redux
npm i redux
npm i redux-devtools-extension
npm i redux-thunk

Enter fullscreen mode Exit fullscreen mode

Now, I also recommend the Redux DevTools extension for Chrome as that will help you see what happens with your store. I recommend installing that at this phase as well.

So now with your libraries installed, let's go over to the src/index.js file to setup our store.

To add Redux to React, you first need to wrap your entry component with a Provider as you see here:

// step 1 add these imports
import { Provider } from 'react-redux';
import configureStore from './redux/configureStore';

const initialState = {};
const { store } = configureStore(initialState);

ReactDOM.render(
    // step 2 wrap your app in the Provider here
    // <React.StrictMode>
    // <App />
    // </React.StrictMode>,
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
);

Enter fullscreen mode Exit fullscreen mode

Now, you'll notice that we're referencing a redux folder that hasn't been created yet. You'll need to ahead and set that up so we can begin the actions, reducers, and eventually effects that we'll be using.

Go ahead and create a src/redux folder as this will be where we put our Redux implementation. Now create the src/redux/configureStore.js file as you see here:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from './reducers/index';

const middleware = [thunk];
const enhancers = [];

// create enhancers to include middleware
// thunk allows you to dispatch functions between the actions
const composedEnhancers = composeWithDevTools(
    applyMiddleware(...middleware),
    ...enhancers
);

// create the store and return it to the application onload
// note that here we are including our reducers to setup our store and interactions across the application
export default function configureStore(initialState) {
    const store = createStore(rootReducer, initialState, composedEnhancers);

    return { store };
}

Enter fullscreen mode Exit fullscreen mode

As the comments point out, we first use the redux-devtools-extension library to create enhancers that we will use with Redux. This is a common way to start building your store, but there are other methods and enhancers you can include.

Then we create the configureStore method by using the createStore to build a root reducer and an initial state with our enhancers. Also note that we are using the redux thunk middleware so that we can return functions instead of just actions with our flows. There are a lot of options with middleware beyond thunk, but this is all we'll need for our application.

Once you've got configureStore all setup, let's go ahead and create our reducers folder in src/redux. Inside that folder create src/redux/reducers/index.js file with the following:

import { combineReducers } from 'redux';

export default combineReducers({
});

Enter fullscreen mode Exit fullscreen mode

Now we've got the basic shell setup, and we have basically an empty store with no initial state except for {}.

Setting up the Actions

So with this basic shell, we now can go ahead and add actions. We're going to setup the episodes actions for the site.

Go ahead and create an actions and actionTypes folder in the src/redux folder that we created before.

Inside actionTypes folder create an Episodes.js file and copy and paste the following:

export const GET_EPISODES = 'episodes/GET_EPISODES';
export const SET_EPISODES = 'episodes/SET_EPISODES';
export const EPISODES_ERROR = 'episodes/EPISODES_ERROR';

export const initialEpisodesState = {
    episodes: [],
    errors: [],
};

Enter fullscreen mode Exit fullscreen mode

I'm also using JavaScript modules, so add a index.js file next to it with:

import * as EpisodesActionTypes from './Episodes';

export { EpisodesActionTypes };

Enter fullscreen mode Exit fullscreen mode

What is this doing? This is defining the action types we'll be using in our application. Notice that it is very simple and we have a GET_EPISODES and SET_EPISODES action along with an EPISODES_ERROR message. The initialEpisodesState is just defining what our store will look like when the application loads.

Next lets actually define the actions in a file src/redux/actions/Episodes.js file like so:

import { EpisodesActionTypes } from '../actionTypes';
import episodes from '../../config/episodes';

export function getEpisodes() {
    return { type: EpisodesActionTypes.GET_EPISODES };
}

export function setEpisodes(episodes) {
    return { type: EpisodesActionTypes.SET_EPISODES, episodes };
}

export function episodesError() {
    return { type: EpisodesActionTypes.GET_EPISODES };
}

// here we introduce a side effect
// best practice is to have these alongside actions rather than an "effects" folder
export function retrieveEpisodes() {
    return function (dispatch) {
        // first call get about to clear values
        dispatch(getEpisodes());
        // return a dispatch of set while pulling in the about information (this is considered a "side effect")
        return dispatch(setEpisodes(episodes));
    };
}

Enter fullscreen mode Exit fullscreen mode

I'm also using JavaScript modules, so add a index.js file next to it with:

import * as EpisodesActions from './Episodes';

export { EpisodesActions };

Enter fullscreen mode Exit fullscreen mode

So as you see here, we're defining a getEpisodes function that corresponds to the GET_EPISODES action, a setEpisodes function that corresponds to the SET_EPISODES action, a episodesError that corresponds to the EPISODES_ERROR action, and finally a side effect to retrieveEpisodes which will pull them from a local configuration file.

There are differing opinions as to where to place side effects in React projects. From the documentation I found on React Redux I found it was recommended to place them alongside your actions. In practice, I've experienced that having the side effects near your actions makes it easy as a developer to find and maintain them. In a more general sense, since React is a library, you can organize your application as you see fit and put them wherever it best works for you.

So now that we've defined our action types and actions, let's add reducers that use those actions. Create a src/redux/reducers/Episodes.js file as you see here:

import { EpisodesActionTypes } from '../actionTypes';

function Episodes(state = EpisodesActionTypes.initialEpisodesState, action) {
    switch (action.type) {
        case EpisodesActionTypes.GET_EPISODES:
            return Object.assign({}, state, {
                loading: true,
                episodes: [],
            });
        case EpisodesActionTypes.SET_EPISODES:
            return Object.assign({}, state, {
                ...state,
                loading: false,
                episodes: action.episodes,
            });
        case EpisodesActionTypes.EPISODES_ERROR:
            return Object.assign({}, state, {
                ...state,
                errors: [...state.errors, action.error],
            });
        default:
            return state;
    }
}

export default Episodes;

Enter fullscreen mode Exit fullscreen mode

Since I'm using JavaScript modules, go ahead and modify the index.js file we had before to include the Episodes.js file as you see here:

import { combineReducers } from 'redux';
import Episodes from './Episodes';

export default combineReducers({
    Episodes,
});

Enter fullscreen mode Exit fullscreen mode

What is all of this doing? The reducers are keyed based on action type. If you notice, the value that is returned from the action is then applied to the necessary place in the state. So in the case of SET_EPISODES you'll note that it is taking the action payload and putting it into the episodes portion of the state as you see here:

case EpisodesActionTypes.SET_EPISODES:
    return Object.assign({}, state, {
        ...state,
        loading: false,
        episodes: action.episodes,
    });

Enter fullscreen mode Exit fullscreen mode

Connecting Redux to Your Components

So now we have all the pieces together, but we still need to add Redux to our actual components. So let's modify the src/pages/EpisodesPage.js as you see here:

import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { EpisodesActions } from '../redux/actions';
import '../styles/styles.scss';
// import episodes from '../config/episodes';

// const episodes = [
// { key: 'first', value: 'something here' },
// { key: 'second', value: 'something there' },
// ];

function EpisodesPage(props) {
    const dispatch = useDispatch();

    // first read in the values from the store through a selector here
    const episodes = useSelector((state) => state.Episodes.episodes);

    useEffect(() => {
        // if the value is empty, send a dispatch action to the store to load the episodes correctly
        if (episodes.length === 0) {
            dispatch(EpisodesActions.retrieveEpisodes());
        }
    });

    return (
        <section className="episodes">
            <h1>Episodes</h1>
            {episodes !== null &&
                episodes.map((episodesItem) => (
                    <article key={episodesItem.key}>
                        <h2>
                            <a href={episodesItem.link}>{episodesItem.key}</a>
                        </h2>
                        <p>{episodesItem.value}</p>
                    </article>
                ))}
            <div className="episodes__source">
                <p>
                    original content copied from
                    <a href="https://www.vulture.com/tv/the-mandalorian/">
                        here
                    </a>
                </p>
            </div>
        </section>
    );
}

export default EpisodesPage;

Enter fullscreen mode Exit fullscreen mode

As you'll note there are a few changes that make Redux possible. First note that we are pulling in the necessary hooks at the top with:

import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { EpisodesActions } from '../redux/actions';

Enter fullscreen mode Exit fullscreen mode

Next you'll note that we commented out the pull of the episodes information locally and instead are retrieving it from a selector:

// import episodes from '../config/episodes';

// const episodes = [
// { key: 'first', value: 'something here' },
// { key: 'second', value: 'something there' },
// ];

function EpisodesPage(props) {
    const dispatch = useDispatch();

    // first read in the values from the store through a selector here
    const episodes = useSelector((state) => state.Episodes.episodes);

Enter fullscreen mode Exit fullscreen mode

Next you'll notice the use of useEffect which dispatches a retrieveEpisodes action as on load:

    useEffect(() => {
        // if the value is empty, send a dispatch action to the store to load the episodes correctly
        if (episodes.length === 0) {
            dispatch(EpisodesActions.retrieveEpisodes());
        }
    });

Enter fullscreen mode Exit fullscreen mode

So now, if you run the application, and then go to the Episodes page you should see it in action. If you open the Redux Devtools Extension you'll see the flow:

So what happened and how does this work?

  1. On load, you initialized your store with an area for episodes
  2. The EpisodesPage component has subscribed to the store to listen for any new state changes
  3. When you click on the "Episodes" page the retrieveEpisodes action fired which then actually triggers a side effect to first call GET_EPISODES to clear the episodes in the store and then SET_EPISODES which retrieves them from the config file and returns them to the component
  4. The EpisodesPage component receives the new store and renders the component

If your end result did not do the above flow, please check out the redux-setup folder and see the end product there.

Adding Quotes

So now that you've got the episodes covered, you can now add quotes. The process is very similar and you'll create:

  • src/redux/actions/Quotes.js
  • src/redux/actionsTypes/Quotes.js
  • src/redux/actions/reducers/Quotes.js

Then in the QuotesPage component you'll setup the same action --> effect --> action --> reducer flow that we did before.

const dispatch = useDispatch();

// first read in the values from the store through a selector here
const quotes = useSelector((state) => state.Quotes.quotes);

useEffect(() => {
    // if the value is empty, send a dispatch action to the store to load the episodes correctly
    if (quotes.length === 0) {
        dispatch(QuotesActions.retrieveQuotes());
    }
});

Enter fullscreen mode Exit fullscreen mode

To attempt to keep this post manageable, I won't add the implementation details here. I'll refer you to look at the redux-actions folder for what the finished product looks like.

Adding HTTP

So up until now the two flows that you've seen for episodes and quotes used local files and did not make any HTTP calls. One of the most common usecases you see with React Redux is to make HTTP calls to handle interactions with APIs.

If you go into the redux-http folder you'll see an example where we add HTTP calls for the "contact" page of the site.

The contact page actually adds messages to the page here. So when you've got this setup, you can see it in action by opening that page up alongside your local application.

When making HTTP calls with React Redux, the general best practice is to put the side effect alongside the actions. If you look in the redux folder you'll see Contact Actions, ActionTypes, and Reducers that are created.

A good convention to use with redux is to have an action that initializes the process, a second action that actually calls the process, and then a success and failure action to suit. You can see this here:

// here we introduce a side effect
// best practice is to have these alongside actions rather than an "effects" folder
export function sendContact(contact) {
    return function (dispatch) {
        // first call sending contact to start the process
        dispatch(sendingContact(contact));
        // actually call the HTTP endpoint here with the value to send
        return axios
            .post(contactEndpoint, contact)
            .then((response) => {
                dispatch(contactSuccess(response));
            })
            .catch((error) => {
                dispatch(contactError(error));
            });
    };
}

Enter fullscreen mode Exit fullscreen mode

If you notice the sendContact action is called, then it calls sendingContact and then it makes the HTTP call and responds with either a contactSuccess or contactError response.

Once you've built out the redux parts, you can connect it to your component like so:

const dispatch = useDispatch();

// when you make the rest call, the response can be seen in the selector here
const response = useSelector((state) => state.Contact.response);

// when an error occurs it should appear here
const errors = useSelector((state) => state.Contact.errors);

const handleSubmit = (event) => {
    setProgress(true);
    event.preventDefault();
    const sendMessage = { firstName, lastName, message };
    dispatch(ContactActions.sendContact(sendMessage));
    // axios
    // .post(messageEndpoint, sendMessage)
    // .then((response) => {
    // alert('success');
    // setProgress(false);
    // })
    // .catch((error) => {
    // alert('error');
    // setProgress(false);
    // });
};

useEffect(() => {
    if (response !== undefined) {
        setProgress(false);
    }

    if (errors.length > 0) {
        setProgress(false);
    }
}, [response, errors]);

Enter fullscreen mode Exit fullscreen mode

Then in your template you can catch the response or errors with a check on the selectors as happens with the following:

{
    response !== undefined && (
        <article className="contact__response">
            Success with a return of {response.status.toString()}
        </article>
    );
}
{
    errors.length > 0 && (
        <article className="contact__error">
            Error occured with message "{errors[0].message}"
        </article>
    );
}

Enter fullscreen mode Exit fullscreen mode

This pattern scales well, and can be used throughout the HTTP calls in your components.

Again, to keep this post necessarily brief I'll refer you to the implementation in the redux-http folder.

Closing Thoughts

So as you see with this project, once you understand the parts to Redux it's not hard to follow the pattern. In our project we setup episodes, quotes, and even a contact page that used Redux in the process.

As I stated in the intro, this pattern enables you to have a common method of handling your applications state as you build more features and move it through its lifecycle. I have personally found that this pattern makes maintenance much easier than manually handling application state through custom services and event interactions.

I hope that this post and my sample project helped you in your journey to learn more about Redux. I recommend playing with the example project I have here, and building out additional pages or features to learn the process.

Thanks for reading my post! Follow me on andrewevans.dev and, feel free to message me on Twitter at @AndrewEvans0102 if you have any questions or wanted to learn more.

Top comments (0)