top of page
Writer's pictureTandid Alam

Redux: A Powerful State Management Library

Updated: Oct 29, 2022

A crash course on Redux and its importance in state management


Contents


- Introduction to Redux

- Getting Started

- The Redux Architecture

- Designing the Store

- Defining Actions

- Reducers

- Creating the Store

- Dispatching

- Subscribing to the Store

- Action Types

- Action Creators

- Folder Structure

- Redux Toolkit

- Designing the Store

- Redux Thunks/Middleware

- Connecting to App

- Manual Setup

- Connecting with React-Redux

- Connecting with React Hooks

- Redux Devtools

 

Introduction To Redux


Redux is a state management library for Javascript applications that creates one single source of truth for an application's state.


Getting Started


To get started, install redux in your terminal with node package manager

npm i redux

The two libraries you will get familiar with in Redux are:

  • react-redux

  • redux-toolkit


Note: You should have some knowledge in function programming concepts before you dive deeper into redux


The Redux Architecture


The three building blocks to any redux applications are:

  1. Actions (Events)

  2. Store (Holds the application state)

  3. Reducers (Event handlers)


How do they all work together? Let's take a look a look at the example below:


For an E-Commerce app, pretend a user adds an item to the shopping cart, this is considered an action. Once the action is made, a dispatch method is triggered, and the store forwards it to the reducer to have it called. The reducer identified what is being changed with the dispatch method and returns a new state for the shopping cart. The new state is shared with the store. From there, the store will make necessary changes to update its state and notify the UI component to change through a refresh.

There are four steps to setting up redux in an application:

  1. Design the store

  2. Define the actions

  3. Create one or more reducers

  4. Set up the store

We will take a look at these in the next few sections.


 

Designing the Store


First, we'll need to decide what we want to keep in our store. Say we had a E-Commerce Store, our state would look something like this:

state = {
    categories: [],
    products:[],
    cart: {},
    user: {}
}


You can consider each property state as a slice, so there would be four slices of state here.


 

Defining Actions


An action is a plain JavaScript object that has a type field. You can think of an action as an event that describes something that happened in the application.


An action would look something like this:

{
    type: "ADD_BUG",
    description: "..." 
}


Typically the naming convention can be with capital letters and underscores or camel case.

"ADD_BUG" or "bugAdded"

An action object can have other fields with additional information about what happened. By convention, we put that information in a field called payload.

{
    type: "ADD_BUG",
    payload: {
        description: "..."
    }
     
}


 

Reducers


A reducer is a function that receives the current state and an action object, decides how to update the state if necessary, and returns the new state: (state, action) => newState. You can think of a reducer as an event listener which handles events based on the received action (even


t) type. Keep in mind that you'll need to have multiple reducers to take care of each slice of a given state.


Here's a small example of a reducer, showing the steps that each reducer should follow:


const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  // Check to see if the reducer cares about this action
  if (action.type === 'increment') {
    // If so, make a copy of `state`
    return {
      ...state,
      // and update the copy with the new value
      value: state.value + 1
    }
  }
  // otherwise return the existing state unchanged
  return state
}


We can also use the switch case statement if you prefer this method for cleaner code.

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  switch(action.type){
    case 'increment':
        return {
            ...state,
            value: state.value + 1
          }
    case 'decrement':
        return {
            ...state,
            value: state.value - 1
          }
    default: 
          return state
  }
}


 

Creating the Store


The current Redux application state lives in an object called the store.


Without using any other library, you can create the store using createStore and passing in a reducer to give it the initial state. However, there's another way you can create a store using the redux toolkit which we will take a look at later.



import {createStore} from 'redux'

const store = createStore(reducer)

export default store

Once we create our store, we'll have several methods that are built in by default such as:

- subscribe()

- dispatch()

- getState()

- replaceReducer()


 


Dispatching


The Redux store has a method called dispatch


. The only way to update the state is to call store.dispatch() and pass in an action object. The store will run its reducer function and save the new state value inside, and we can call getState() to retrieve the updated value:


store.dispatch({ type: 'increment' })

console.log(store.getState())
// {value: 1}

store.dispatch({ type: 'decrement' })

console.log(store.getState())
// {value: 0}

You can think of dispatching actions as "triggering an event" in the application. Something happened, and we want the store to know about it. Reducers act like event listeners, and when they hear an action they are interested in, they update the state in response.


What happens inside the hood?

1. Dispatch stores the state we get from reducer(state, action)

2. A notification is sent to subscribers of the store to update the UI


 

Subscribing to the Store


The subscribe method allows you to listen for changes in the state within your app.

After an event is handled, the UI unsubscribes, but you'll have to call this within the componentWillUnmount() hook. If not, there will be memory leakage.

 

Action Creators


An action creator is a function that creates and returns an action object. We typically use these so we don't have to write the action object by hand every time. This makes the code for dispatching a lot more elegant:


const addTodo = text => {
    return {
      type: 'todoAdded',
      payload: text
    }
  }

store.dispatch(addTodo('Study')) 


 

Action Types


To follow a DRY approach to coding, it's ideal to create action types. We can do so by following this convention:


./actionTypes

export const TODO_ADDED = 'todoAdded'
export const TODO_REMOVED = 'todoRemoved'

./actions

const addTodo = text => {
    return {
      type: TODO_ADDED, //Action Type 
      payload: text
    }
  }

store.dispatch(addTodo('Study')) 

 

Selectors


Selectors are functions that know how to extract specific pieces of information from a store state value. As an application grows bigger, this can help avoid repeating logic as different parts of the app need to read the same data:


const selectCounterValue = state => state.value

const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2

 

Folder Structure


Typically your folder structure for Redux can look something like this.


> src
    > store
        > users
            - actionTypes.js
            - actions.js
            - reducer.js
        > posts
            - actionTypes.js
            - actions.js
            - reducer.js    
        - configureStore.js
        - index.js

However, when you have multiple slices you'll want to start following the Ducks Pattern. Essentially, the actionTypes, actions, and reducers get placed into the slice's file but you should write comments to divide the sections within the file. Also, you should remember to export the default root reducer as well as the individual action creators. The folder structure will eventually look something like this.


> src
    > store
        - users.js
        - posts.js
        - configureStore.js
        - customStore.js
    - index.js

// posts.js
const CREATE_POST = 'CREATE_POST'
const UPDATE_POST = 'UPDATE_POST'
const DELETE_POST = 'DELETE_POST'

export function addPost(id, title) {
  return {
    type: CREATE_POST,
    payload: { id, title },
  }
}

const initialState = []

export default function postsReducer(state = initialState, action) {
  switch (action.type) {
    case CREATE_POST: {
      // Omit actual code
      break
    }
    default:
      return state
  }
}


We created configureStore.js as a new naming convention to avoid having the same name as the store folder, but this file should have the following code.

export default function configureStore(){
    const store = createStore(reducer, middleware)
    return store
}

 

Redux Toolkit


The Redux Toolkit provides helper functions for simplifying redux code. To install, use the following code:

npm i @redux/toolkit@1.2.5

Creating the Store

There's a simpler way to create store using Redux Toolkit. Inside the library there's a fucntion called configureStore which we can use instead of createStore().


import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'

const store = configureStore({
  reducer: rootReducer,
})

export default store

We can also pass in slices as reducers:

import { configureStore } from '@reduxjs/toolkit'
import usersReducer from './usersReducer'
import postsReducer from './postsReducer'

const store = configureStore({
  reducer: {
    users: usersReducer,
    posts: postsReducer,
  },
})

export default store

Using this, we can automatically talk to redux devtools for debugging and we can dispatch using async actions.


Creating Actions


Writing action creators by hand can get tedious. Redux Toolkit provides a function called createAction, which simply generates an action creator that uses the given action type, and turns its argument into the payload field:


import { createAction} from '@reduxjs/toolkit'

function addTodo(text) {
    return {
      type: 'ADD_TODO',
      payload: { text },
    }
  }

vs. 

const addTodo = createAction('ADD_TODO')
addTodo({ text: 'Buy milk' })
// {type : "ADD_TODO", payload : {text : "Buy milk"}})

Creating Reducers


The createReducer utility has some special "magic" that makes it even better than typical reducer statements. It uses the Immer library internally, which lets you write code that "mutates" some data, but actually applies the updates immutably. This makes it effectively impossible to accidentally mutate state in a reducer and you don't have to worry about the default case anymore.


const todosReducer = createReducer([], (builder) => {
    builder
      .addCase('ADD_TODO', (state, action) => {
        // "mutate" the array by calling push()
        state.push(action.payload)
      })
      .addCase('TOGGLE_TODO', (state, action) => {
        const todo = state[action.payload.index]
        // "mutate" the object by overwriting a field
        todo.completed = !todo.completed
      })
      .addCase('REMOVE_TODO', (state, action) => {
        // Can still return an immutably-updated value if we want to
        return state.filter((todo, i) => i !== action.payload.index)
      })
  })

Creating Slices


Redux Toolkit includes a createSlice function that will auto-generate the action types and action creators for you, based on the names of the reducer functions you provide.


const postsSlice = createSlice({
    name: 'posts',
    initialState: [],
    reducers: {
      createPost(state, action) {},
      updatePost(state, action) {},
      deletePost(state, action) {},
    },
  })
  
  console.log(postsSlice)
  /*
  {
      name: 'posts',
      actions : {
          createPost,
          updatePost,
          deletePost,
      },
      reducer
  }
  */
  
  const { createPost } = postsSlice.actions
  
  console.log(createPost({ id: 123, title: 'Hello World' }))
  // {type : "createPost", payload : {id : 123, title : "Hello World"}}
  
  // Extract the action creators object and the reducer
const { actions, reducer } = postsSlice
// Extract and export each action creator by name
export const { createPost, updatePost, deletePost } = actions
// Export the reducer, either as a default or named export
export default reducer

 

Store Design Convention


When deciding on your store design, you need to choose between storing some slices in a global state or storing in all states. However, it's more ideal to store the entire state of an app in redux to take full advantage of it. The only exceptions are form states which don't really affect the main state that much.


When creating states, sometimes its best to store the state as an object for fast access, but other times, you want the order functionality from arrays. You have to decide which is better for your application.



You should split up your state into slices because it prevents your whole app from updating when one small piece of state changes. To do this, create multiple reducers and combine them using combineReducers from Redux. From there, you can create an entities folder to split up the structure further and allow a nested structure of reducers which can be placed inside the root reducer.


import {combineReducers} from "redux"

export default combineReducers({
    users: usersReducer,
    projects: projectsReducer
})

 

Redux Thunks/Middleware



Middleware are pieces of code that occur between the process of dispatching and reaching the reducer. Typically they follow the concept of currying, where they pass in functions that call other functions with one parameter. A middleware function will have three paramters being state, next, and action, but be sure to call next() at the end of the function.


When using the configureStore method from toolkit, we can add middleware by adding the middleware property along with the name. However, if not, you have to use applyMiddleware(fn) to createStore as another parameter.


What is a "thunk"?

The word "thunk" is a programming term that means "a piece of code that does some delayed work". Rather than execute some logic now, we can write a function body or code that can be used to perform the work later.


For Redux specifically, "thunks" are a pattern of writing functions with logic inside that can interact with a Redux store's dispatch and getState methods.


Using thunks requires the redux-thunk middleware to be added to the Redux store as part of its configuration.


Thunks are the standard approach for writing async logic in Redux apps, and are commonly used for data fetching. However, they can be used for a variety of tasks, and can contain both synchronous and asynchronous logic.



constthunkFunction=(dispatch, getState)=>{
    // logic here that can dispatch actions or read state
    }
    
    store.dispatch(thunkFunction)


Adding the Middleware

The Redux Toolkit configureStore API automatically adds the thunk middleware during store creation, so it should typically be available with no extra configuration needed.

If you need to add the thunk middleware to a store manually, that can be done by passing the thunk middleware to applyMiddleware() as part of the setup process.

export const fetchTodoById = todoId => async dispatch => {
    const response = await client.get(`/fakeApi/todo/${todoId}`)
    dispatch(todosLoaded(response.todos))
  }
 

Connecting to App


Connecting Manually

// storeContext.js

import { createContext } from 'redux'

const StoreContext = createContext

<StoreContext.Provider value={store}>
    <ToDo/> //Do not inject store = {store} here, this is why we create context
</StoreContext.Provider>

We can create context two ways:


1. Bugs.contextType = Store Context 
2. static contextType = StoreContext

We need to subscribe to get list of bugs and render, then we need to dispatch load Bugs

this.unsubscribe = store.subscribe(){
	this.setState(bugs: store.getState().entities.bugs.list)
	store.dispatch(loadBugs()
}

componentWillUnmount(){
	this.unsubscribe
}


This method tends to be very tedious, which is why they introduced react-redux.


React-Redux


import { Provider } from 'react-redux'

<Provider store={store}>
    <ToDo/>
<Provider>

This way, we don't need contextType, we don't need to subscribe, we don't need to unmount, and we can use this.props instead of this.state.



import { connect } from "react-redux";

class App extends React.Component {
    ...
}

const mapStateToProps = ({ user }) => {
    return {
        user,
    };
};
  
const mapDispatchToProps = (dispatch) => {
    return {
        update: (user, push) => dispatch(updateProfile(user, push)),
    };
};
  
export default connect(mapStateToProps, mapDispatchToProps)(Account);


React Hooks


useDispatch()


useSelector()


import {useDispatch, useSelector} from react-redux

selector = useSelector()
dispatch = useDispatch()

useEffect(()=>{
    dispatch(loadBugs())
})

Redux Devtools


To debug your redux application you can download Redux DevTools from the Chrome Extensions. From there, you will get an extended tab in your chrome console that says redux.


You'll need to read the documentation to set up, but you essentially add in a piece of code as middleware to get started.



Comments


bottom of page