Redux Fundamentals

Redux is a state manager that has one store that allows us to store all of our app’s state in one place and know what the state is at any point.

But what is state? Concisely explained, information that we can access and change throughout the lifetime of our program is state. State allows us to create stateful applications. Stateful applications contain logic partly based on the current state because we can remember old events and user interactions. For example, an app that displays the text "button is activated" if a button has been clicked on. With state, we could create a state variable called isButtonPushed which helps us know if we should hide or display the text.

An analogy could be us, humans. We, also have our state, we can be happy, tired, full of energy, etc. Depending on our state we might act differently, if we’re hungry, we maybe will eat something, if we’re tired we maybe will go to sleep, etc.

How Redux Works

Stateful applications are great, but when your application grows the management of state can become be a real challenge. This is where state managers can help us out. Redux helps us manage the state by storing all state in one object, which is called the store.

But before we look at how to create and use the store, we must be somewhat familiar with some terms. I’ve created a concise list to help us out:

  • Action: is a simple object that contain information. Actions must have type property that holds a unique value that indicates what happened.
  • Action Creator: is a function that create an action.
  • Reducer: is a pure function that creates a new state based on the current state and sent action.
  • Middleware: is a higher-order function that let’s us run code before an action is passed to a reducer

Working with the Redux Store

As previously mentioned, Redux has a store. We create the store with the help of createStore. The method createStore takes three arguments, the first is a reducer, which is required. The other two arguments are optional, initial state and store enhancer.

Changing the state of a store can only be done by sending an action. To send actions, we use the store’s dispatch(action) method. The store will pass the dispatched action and current state to a reducer. The reducer will then decide how the state will change. When a reducer receives the state object and action, it'll return a new state. The type property on the action object is used by the reducer to decide how to act. If the action object contains additional data, then the reducer can use that data when creating a new state. A very important thing to remember is that the reducer may not mutate the current state.

Let’s roughly implement what we’ve covered so far:

import { createStore } from 'redux';
 
// Action creator that creates an action object { type: "INCREMENT" }
const addIncrement = () => ({ type: "INCREMENT" });
 
// Reducer that will create a new state object with the
// current state variable "value" being increased by 1
// if it receives an action with the type "INCREMENT".
const reducer = (state, action) => {
   switch (action.type) {
       case "INCREMENT":
           return {...state, value: state.value + 1};
   }
 
   return state;
}
 
const initialState = { value: 0 };
const store = createStore(reducer, initialState);
 
// Send action to store
store.dispatch(addIncrement());

In this simple example, the addIncrement action creator does not add that much value. However, action creators are a great way to keep the code clean, when the project increases and when actions contain additional data and are more complex.

As we now know, reducers create a new state and return it to the store. As the application grows, the reducer’s switch statement grows, making it harder to maintain. Luckily, we can avoid this by having a root reducer that manages multiple reducers. With the function combineReducers() we can assign a reducer for each state variable like:

import { createStore, combineReducers } from 'redux';
 
// Action creators
const addIncrement = () => ({ type: 'INCREMENT' });
const addDecrement = () => ({ type: 'DECREMENT' });
const addText = (text) => ({ type: 'ADD_TEXT', text });
 
// Counter reducer, using ES6 to assign initial value to its state variable.
const counterReducer = (state = 0, action) => {
   switch (action.type) {
       case 'INCREMENT':
           return state + 1;
 
       case 'DECREMENT':
           return state - 1;
   }
 
   return state;
}
 
const textReducer = (state = 'default string', action) => {
   switch (action.type) {
       case 'ADD_TEXT':
           return action.text;
   }
 
   return state;
}
 
const rootReducer = combineReducers({
   counter: counterReducer,
   text: textReducer
})
 
const store = createStore(rootReducer);

In the example above, we have successfully separated some logic by having two child reducers and one root reducer. We can also see that each child reducer can have their initial state.

What about Middleware?

Now we’ve looked at actions, action creators, and reducers so now we’ll move our focus to redux middleware. With middleware, we can run code before passing a sent action to a reducer. Middleware is a higher-order function that returns another function which also returns a function. The third function is where we put our logic for the middleware. Now, let’s look at how a middleware looks like without arrow functions:

function middleware ({getState, dispatch}) {
   return function (next) {
       return function (action) {
           // do something
           next(action);
           // do something
       }
   }
}

We see that our middleware has access to the stores getState and dispatch methods, note that we do not have access to the entire store. The next() method is used to pass control to another middleware or a reducer. If you do not call next(action) then the reducer will never receive the action.

But how do we connect middleware to our store? A very common way is to use the store enhancer applyMiddleware() while creating the store with createStore(). You can add as many middleware functions as you’d like with applyMiddleware(), which allows us to chain middleware. Middlewares will run in the same order as put. In the example below, we have two middleware functions. I recommend looking at all the console.log()s to get an idea what’s going on:

import { createStore, applyMiddleware } from 'redux';
 
const addIncrement = () => ({ type: 'INCREMENT' });
 
const reducer = (state, action) => {
   switch (action.type) {
       case 'INCREMENT':
           return {...state, value: state.value + 1};
   }
 
   return state;
}
 
const middleware1 = ({ getState, dispatch }) => next => action => {
   console.log('Before next middleware');
   next(action);
}
 
const middleware2 = ({ getState, dispatch }) => next => action => {
   console.log('Before reducer');
   next(action);
   console.log('After reducer');
}
 
const initialState = { value: 0 };
const store = createStore(
    reducer,
    initialState, 
    applyMiddleware(middleware1, middleware2)
);

Creating a Simple Redux App

We will now create a simple application that uses Redux to manage state. We’ll only use one index.html so you can easily copy and paste the code to try it out. The app we will create will allow the user to increment and decrement a counter. We will also have a paragraph element that uses the state variable text as its innerText. Inputs into the input field will change the state variable text.

I recommend examining the code, trying it out in your browser, and then you can read what’s going on below. Also, keep in mind that this code is only for illustration purposes.

<!DOCTYPE html>
<html>
  <head lang='en'>
    <meta charset='UTF-8'>
    <title>Redux Example</title>
    <script src='https://unpkg.com/redux@latest/dist/redux.min.js'></script>
    <style>
        h1 {
            color:#764ABC;
        }
        div {
            width: 50%;
            margin: 10% auto;
            text-align: center;
            border: 1px solid;
            border-radius: 0.4em;
            padding: 10px;
        }
    </style>
  </head>

  <body>
    <div>
        <h1>Redux Example</h1>

        <p id='text'></p>
        <input id='input' />
        
        <p id='counter'></p>
        <button id='inc'>Increment</button>
        <button id='dec'>Decrement</button>
    </div>
    <script>

        // Action creators
        const addIncrement = () => ({ type: 'INCREMENT' });
        const addDecrement = () => ({ type: 'DECREMENT' });
        const addText = (text) => ({ type: 'ADD_TEXT', text });

        // Reducers
        const counter = (state = 0, action) => {
            switch (action.type) {
                case 'INCREMENT':
                    return state + 1;

                case 'DECREMENT':
                    return state - 1;
            }

            return state;
        }

        const text = (state = 'default string', action) => {
            switch (action.type) {
                case 'ADD_TEXT':
                    return action.text;
            }

            return state;
        }

        const rootReducer = Redux.combineReducers({ 
            counter, 
            text 
        })


        // Middleware
        const logActionMiddleware = ({ getState, dispatch }) => next => action => {
            console.log(`${action.type} got dispatched!`);
            next(action);
        }

        // Create store
        const store = Redux.createStore(
            rootReducer,
            Redux.applyMiddleware(logActionMiddleware)
        );

        // UI logic
        const render = () => {
            document.getElementById('counter').innerText = store.getState().counter;
            document.getElementById('text').innerText = store.getState().text;
        }
        
        render(); // render with initial state
        store.subscribe(render); // render after each dispatch

        document.getElementById('inc').addEventListener('click', () => {
            store.dispatch(addIncrement())
        });

        document.getElementById('dec').addEventListener('click', () => {
            store.dispatch(addDecrement())
        });

        document.getElementById('input').addEventListener('input', (event) => {
            store.dispatch(addText(event.target.value));
        });

    </script>
  </body>
</html>

Most of this should look familiar because we’ve covered most of it. Let’s focus on the UI logic. We have a render function that changes the innerText of two elements. With getState() we base the element’s innerText on state. We call render in the beginning so that the elements have their initial state. Then we use the store´s subscribe(listener) method which allows us to set up callbacks that get executed after each state update. In this case, we chose to render, so after each dispatch, we’ll re-render our UI. Lastly, we added event handlers that’ll dispatch an action in response to their corresponding triggered event.

Are you interested in learning more? Then I'd highly recommend looking at the redux docs and functional programming concepts like pure functions and mutation.