DEV Community

Jason Park
Jason Park

Posted on

A Beginner's Guide to Redux

Contents

Introduction

Frontend state management is an unavoidable concept when creating larger web applications. While there are multiple ways to approach this, one tried and true method is by using a predictable, centralized, and flexible tool called Redux. In React, there are other methods such as the useContext hook that may be easier to set up, yet many developers choose to work with Redux due to its ability to handle complex state interactions in larger applications more effectively. This guide will go over the basics of Redux and how it can be easily implemented in a typical React application using the Redux toolkit.

Basics

Developers use Redux to avoid prop drilling and work with a single source of truth for the application state. Before we go over how to use Redux, we must go over some of the key fundamentals.

The store is the single source of truth that holds the entire state of the application. State represents the current data and the way the application behaves at a given moment. Components or other parts of the application can subscribe to the Redux store to receive notifications about state changes.

All applications using Redux must have reducer functions. These are pure functions that define how the state should change based on the current state and a particular action. There can be multiple reducers for a particular state. When working with React, the reducers are defined in a separate file called a slice.

An action is an object describing how to change state. Developers often use action creator functions to generate these action objects. This becomes particularly useful when there are more complex actions or when we want to abstract away the details of action creation, especially in cases where we might have asynchronous operations in the action creation process.

In order to call for these changes, dispatch functions are used. A dispatch function takes in an action as an argument that provides instructions on how to change the state.

The state is typically immutable. This means that you don't directly modify the existing state; instead, you create a new state object when changes are needed. This helps in tracking and debugging state changes more effectively.

Redux allows the use of middleware, which are functions that can intercept and augment the standard behavior of dispatch. Middleware is often used for tasks like logging actions, handling asynchronous operations, etc.

A simple restaurant analogy can help us understand the Redux system. In this analogy, the store is the restaurant menu, the dispatch function is the act of placing an order, the reducer function is the kitchen, and the resulting state is the meal.

The store displays all the items available for ordering. Just as the menu is a central place for all the food options, the store is a central palace for all the application’s state. When we want to order food (make changes to state), we tell the waiter what we want (dispatching an action) and the kitchen (reducer) processes the request to prepare the dish (update the state). The kitchen follows a recipe (reducer logic) to cook a new dish (updated state). Because the state is immutable, the kitchen doesn't modify the dish, but creates a new one.

Here is a simple example of a Redux set-up in vanilla Javascript:

// Action
const incrementAction = { type: 'INCREMENT' };

// Reducer
const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    default:
      return state;
  }
};

// Redux Store
const store = createStore(counterReducer);

// Dispatching an action to update the state
store.dispatch(incrementAction);
Enter fullscreen mode Exit fullscreen mode

Implementing with React

Redux can be implemented in React applications easily through the help of some dependencies, namely redux, react-redux, and @reduxjs/toolkit. Here is a step by step protocol on how we can do this.

Create a file named store.js. Here we import configureStore from @reduxjs/toolkit and any reducers we want to use. configureStore is a method that helps us consolidate all our reducer functions. In the example below, we have our store defined and added one reducer called counterReducer, which is defined in a separate file ./features/counter/counterSlice.

// store.js
import { configureStore } from "@reduxjs/toolkit"
import counterReducer from "./features/counter/counterSlice"

const store = configureStore({
    reducer: {
        counter: counterReducer
    }
})

export default store
Enter fullscreen mode Exit fullscreen mode

To make this state available, we provide access by wrapping the application with a special context <Provider> component in index.js, similar to the BrowserRouter component for React router. The <Provider> component has an attribute of store={store}.

// index.js
import React from "react"
import ReactDOM from "react-dom"
import { Provider } from "react-redux"
import store from "./store"
import App from "./App"

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>, 
    document.getElementById("root")
)
Enter fullscreen mode Exit fullscreen mode

Now that we set up our store and gave our application access to it, we need to define the initial state, reducers, and action creators, which are coded in a separate file. We extract createSlice and pass an object with keys of name, initialState, and reducers. It is imperative that we export all the action creators and the reducer itself at the bottom of the file.

// counterSlice.js
import { createSlice } from "@reduxjs/toolkit";

// slice
const slice = createSlice({
    name: 'counter',
    initialState: {
        count: 0,
        running: true
    },
    reducers: {
        increment: state => {
            state.count += 1
        },
        decrement: state => {
            state.count -= 1
        },
        toggleRunning: state => {
            state.running = !state.running
        },
        incrementBy: (state, action) => {
            state.count += action.payload
        }
    }
})


export const { increment, decrement, toggleRunning, incrementBy } = slice.actions
export default slice.reducer
Enter fullscreen mode Exit fullscreen mode

Finally, all we have left is to use the dispatch function in the components that need it by importing useDispatch from react-redux. Remember to also import the action creators from our slice file as well.

// Controls.js
import React from "react"
import { useDispatch, useSelector } from "react-redux"
import { decrement, incrementBy, toggleRunning } from "./counterSlice"

function Controls() {
    const dispatch = useDispatch()
    const running = useSelector(state => state.running)

    function handleMinusClick() {
        dispatch(decrement())
    }

    return (
        <div>
            <button id="minus" onClick={handleMinusClick}>-</button>
            <button id="plus" onClick={() => dispatch(incrementBy(10))}>+</button>
            <button id="play" onClick={() => dispatch(toggleRunning())}>{running ? "⏸" : "▶️"}</button>
        </div>
    )
}

export default Controls
Enter fullscreen mode Exit fullscreen mode

In our example above, we have the onClick event listeners defined for each button. Within the event listeners, we call our dispatch function while passing our action creator as the argument.

So now we know how to update state, but how can we access the state to use in our components? In the provided example, we utilize the useSelector hook from react-redux to access and utilize the state in our components. Specifically, we set the variable running using useSelector, and it corresponds to the running key in the initial state object defined in our counterSlice. As per the counterSlice example, the initialState object includes a key running set to true. This mechanism allows us to effortlessly access and utilize state variables throughout our application.

File Organization

Organizing files in a Redux and React application is crucial for maintainability and scalability. A common practice is to structure the project based on features or modules. Each feature typically has its own directory containing related components, actions, reducers, and selectors. For instance, a "counter" feature might have files such as Counter.js for the component, counterSlice.js for the Redux slice (including actions and reducer), and possibly Counter.css for styles. This modular organization promotes encapsulation and makes it easier to locate and update code associated with a specific feature. Additionally, there is often a centralized store directory that houses the Redux store configuration, combining all slices into the root reducer. To enhance readability, consider using subdirectories within features for further categorization. Overall, a well-organized file structure not only improves code maintainability but also facilitates collaboration and ensures a logical separation of concerns in your Redux and React application.

Conclusion

This beginner's guide has provided the basics introducing Redux and its integration with React, offering a robust state management solution for larger web applications. By understanding the fundamental concepts of the Redux architecture, such as the store, reducers, actions, and the dispatch function, developers gain a powerful tool to manage and update application state effectively. By adopting Redux, developers can streamline state management, simplify complex interactions, and build more resilient and scalable React applications.

If you're eager to delve deeper into Redux, refer to the official Redux documentation for comprehensive resources and guidance on advanced topics. Happy coding!

This guide is meant to serve as a learning tool for people who are just starting to learn Redux in React. Please read the official documentation linked below for more information. Thank you for reading and if you have any questions or concerns about some of the material mentioned in this guide, please do not hesitate to contact me at jjpark987@gmail.com.

Resources

Top comments (0)