Type to generate custom UI components with AI

Type to generate UI components from text

OR

Browse thousands of MUI, Tailwind, React components that are fully customizable and responsive.

Explore Components

React Redux: Best Techniques for Great State Management

React is a component-based JavaScript library that offers a robust mechanism for managing a component’s state. React can handle simple and uncomplicated stateful logic at the local component level and application global scope. Managing component state is a crucial feature of React necessary to create dynamic and interactive interfaces. Nevertheless, when the application’s complexity surpasses React’s inherent capacity for global state management, utilizing state management tools like React Redux becomes imperative.

Redux is a standalone Javascript library that integrates well with React. Its ability to simplify complex state logic and predictably manage shared application states through a centralized store makes it flexible and efficient. However, connecting the Redux store to the React component tree requires repetitive code that doesn’t specifically contribute to the application’s functionality. Using Redux with React Redux helps streamline and simplify the process of integrating Redux with React.

This article will briefly discuss the core concepts of Redux, examine techniques for establishing a connection between Redux and React, and address strategies for an app’s state management within the Redux store.

Before we explore these techniques, visit Purecode AI to explore our library of customizable UI components or generate custom UI components with AI tailored to your project requirements.

Understanding Redux in React

Redux, an open-source JavaScript library, was created by Dan Abramov and Andrew Clark to address the challenges associated with state management in large-scale, complex applications. It is a predictable state container that seamlessly integrates with Javascript libraries like React. This library simplifies the logic to organize and manage data flow in sophisticated applications, showcasing flexibility and efficiency. In addition, Redux ensures your application works as expected by guiding you toward writing predictable and testable code.

Redux Basics

Redux provides a centralized store for state needed across an entire application. It streamlines the process of accessing and updating the current state object. Interactive user interface (UI) components can access or update the store state through user-triggered events that propagate changes to relevant parts of the application. Changes in the state are initiated by dispatching actions, notifying the reducer specifically created to listen for that event and calculate the new state. This process triggers a re-render of the component that depends on the updated state. Consider the illustration below:

Now that we have discussed the basic idea behind the Redux state management process, let’s explore the pieces that make up a Redux app.

Redux Components

Three components make up Redux, including the following:

  • Store

  • Reducer

  • Action

Store

The Redux store is the core of a Redux app. The store is a JavaScript object that serves as a container that holds the global state object. Consider the following syntax to create a Redux store:

const store = createStore(reducer);

The store has a few built-in methods including:

  • getState method: This method allows components to retrieve the latest store state.

    const state = store.getState();
  • dispatch method: This method triggers the corresponding reducer to update the state by executing an action to the Redux store.

    store.dispatch({ type: "A-value-corresponding-with-reducer-action-type" });
  • subscribe method: It enables a UI component to listen to state changes in the Redux store.

    const unsubscribe = store.subscribe(() => {
      // Update UI or perform other tasks
    });

Action

A Redux action is a plain object describing an intention to change the application state. It is created by an action creator, a function that returns an action object. It serves to store the information of a user event. This information is made up of an action type and an optional payload. When a user event dispatches an action, the store uses the information to trigger a state update through reducers.

Reducer

Reducers are pure functions that receive the current state of the application store and, depending on the action dispatched, return a new state. This adherence to the principle of immutability ensures the original state remains unchanged while creating a new state object. The reducer function is called every time an action is dispatched to the store, updating the application state in response to the action. This mechanism establishes a predictable data flow that makes it easy to manage state changes.

The two ways to define the content of a reducer based on preference, include the following:

  • Using the switch statement

    const initialState = { counter: 0 };
    
    const reducer = (state = initialState, action) => {
      switch (action.type) {
        case "INCREMENT":
          const currCount = state.counter;
          return { ...state, counter: currCount + action.payload.value };
        default:
          return state;
      }
    };
  • Employing the if statement

    const initialState = { counter: 0 };
    
    const reducer = (state = initialState, action) => {
      if (action.type === "INCREMENT") {
        const currCount = state.counter;
        return { ...state, counter: currCount + action.payload.value };
      }
    
      return state;
    };

Furthermore, the redux community recommends separating complex stateful logic into multiple reducers for enhanced efficiency and maintainability. These individual reducers can be combined into a single root reducer using the combineReducers utility method.

Integrating Reducers into the Redux Store

In the previous section, we briefly discussed the role of the Redux reducer. This sub-section will illustrate integrating the individual reducers with the Redux store:

import {createStore, combineReducers} from 'redux';

const initialCountState = { counter: 0 };

const counterReducer = (state = initialCountState, action) => {
  if (action.type === "INCREMENT") {
    const currCount = state.counter;
    return { ...state, counter: currCount + action.payload.value };
  }

  return state;
};

const initialAlertState = {success: "", error: ""}

const alertReducer = (state = initialAlertState, action) => {
  if (action.type === "SUCCESS_ALERT") {
    return { ...state, success: action.payload.message };
  }

  return state;
};

const rootReducer = combineReducers({
  counter: counterReducer,
  alert: alertReducer
})

const store = createStore(rootReducer)

Leveraging Redux in React

So far, we have introduced Redux as a standalone library without leveraging its state management capabilities within a React application. Moreover, establishing a connection between React’s UI and Redux can be cumbersome. This process often leads to redundant code that doesn’t specifically contribute to the application’s functionality. Since they operate independently of each other, optimizing UI performance would require complicated logic. Therefore, React Redux streamlines the binding of a React application to the Redux store.

React Redux is a library that serves as the official React UI binding for Redux. It provides a set of tools and conventions that allow React components to read data from the store and dispatch actions to the store. React Redux simplifies the logic enabling seamless interaction between the React UI and the Redux store, initiating a re-render in components dependent on the updated state.

Reasons to Use React Redux

Understanding why React Redux is the recommended approach for binding React UI to Redux is crucial for the scalability of an application. Here are a few reasons:

  • It is the official React UI binding for Redux, designed to connect React components to the store.

  • React Redux implements performance optimizations internally, ensuring a component only re-renders when needed.

  • It ensures that each connected components within the React component tree easily extract specific data, improving the React component architecture.

Connecting React App to the Redux Store

React Redux ensures that the Redux store and its functionalities are globally available in a React application. React Redux provides a Provider component that wraps the entire App component and connects to an instance of the store. Consider the following example code:

import React from 'react'
import ReactDOM from 'react-dom/client'

import { Provider } from 'react-redux'
import store from './store'

import App from './App'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <Provider store={store}>
    <App />
  </Provider>
)

Accessing and Updating Redux Store State

From the setup above, the React app now knows about the store’s existence. However, this knowledge does not automatically grant it access to the state tree nor a mechanism to trigger an update. By utilizing React Redux, React components can interact with the store in one of two ways, including:

  • Utilizing the connect function.

  • Leveraging React Redux hooks.

Utilizing the connect function

React Redux provides the connect function, to give components the ability to read values from the store. Being a higher-order function (HOF), the connect function employs the principle of currying. It returns a container component that receives the necessary segment of the store and a dispatch function as props. This approach enables the React component to respond to state modifications and interact with the store.

The connect function takes two optional arguments, including:

  • mapStateToProps: This parameter gets called whenever the store state changes. It receives the store state and props directly passed to the connected component, returning an object with the necessary data the component needs. Consider the illustrations below to understand how it works:

  • mapDispatchToProps: Depending on preference, this parameter can be a function or an object. If it is a function, the connect function calls it once when the component is first connected to the store and receives dispatch as an argument, which the component utilizes for dispatching Redux actions. On the other hand, if it’s an object, it will pass an object of action creators to the component as props functions, allowing the component to automatically dispatch the corresponding actions when called.

import {connect} from 'react-redux';

const mapStateToProps = (state, ownProps) => ({
  // ... computed data from state and optionally ownProps
})

const mapDispatchToProps = {
  // ... normally is an object full of action creators
}

// `connect` returns a new function that accepts the component to wrap:
const connectToStore = connect(mapStateToProps, mapDispatchToProps)
// and that function returns the connected, wrapper component:
const ConnectedComponent = connectToStore(Component)

// We could do both in one step, like this:
export default connect(mapStateToProps, mapDispatchToProps)(Component)

Leveraging React Redux Hooks (useSelector and useDispatch)

With the introduction of React Hook came the need to improve the way React components access and interact with the application’s store. This improvement allowed the Redux team to create React custom Hooks that allow React components to interact with the Redux store.

The React Redux library provides two Hooks that allow React functional components to interact with the store, including:

  • useSelector: This Hook retrieves data from the store state and subscribes to updates. The useSelector Hook allows a React functional component to select a slice of state from the Redux store.

  • useDispatch: This Hook returns a reference of the dispatch function from the Redux store. It enables a functional component to execute an action to the store.

The code example below shows how to use the React Redux Hooks:

import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { createStore } from "redux";
import { useSelector, useDispatch, Provider } from "react-redux";

const INCREMENT = "INCREMENT";
const DECREMENT = "DECREMENT";

function increment() {
  //action creator
  return { type: INCREMENT }; //action
}
function decrement() {
  return { type: DECREMENT };
}

//reducer
function counterReducer(state = 0, action) {
  switch (action.type) {
    case INCREMENT:
      return state + 1;
    case DECREMENT:
      return state - 1;
    default:
      return state;
  }
}

var store = createStore(counterReducer, enableDevTools());

function enableDevTools() {
  return (
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
  );
}

function Result() {
  const count = useSelector((state) => state);
  return (
    <React.Fragment>
      <div>Count: {count}</div>
    </React.Fragment>
  );
}

function Actions() {
  const dispatch = useDispatch();
  const btnStyles = {
    padding: "5px 10px",
    fontSize: "1.25rem",
    display: "inline-block",
  };
  return (
    <div>
      <button style={btnStyles} onClick={() => dispatch(increment())}>
        +
      </button>
      <button style={btnStyles} onClick={() => dispatch(decrement())}>
        -
      </button>
    </div>
  );
}

function CounterPage() {
  return (
    <div
      style={{
        display: "flex",
        height: "100vh",
        justifyContent: "center",
        alignItems: "center",
        flexDirection: "column",
        rowGap: "0.625rem",
        fontFamily: "serif",
        fontSize: "1.5rem",
      }}
    >
      <Actions />
      <Result />
    </div>
  );
}

function App() {
  return <CounterPage />;
}

const element = (
  <StrictMode>
    <div>
      <Provider store={store}>
        <App />
      </Provider>
    </div>
  </StrictMode>
);

const rootElement = document.getElementById("root");
createRoot(rootElement).render(element);

Elevating Your Redux Logic with Redux Toolkit

Utilizing the Redux core ensured flexible and efficient data flow management in complex applications. Although the Redux core addressed some of the issues associated with state management, it had some limitations, including:

  • Complicated store configuration.

  • Verbose boilerplate code.

  • Installing multiple packages for specific functionalities.

The Redux team created the Redux toolkit (RTK) to tackle these concerns and simplify the logic to interact with the store. It provides utilities built on the Redux core that simplifies many typical Redux tasks, making it the recommended package for writing Redux applications. Furthermore, the Redux toolkit includes functionalities like robust data fetching and caching capabilities, which help eliminate the need for verbose code. This approach ensures an efficient and maintainable Redux application codebase.

Components of Redux Toolkit

The Redux toolkit provides APIs necessary to streamline the Redux logic, including:

  • configureStore

  • createSlice

  • createAyncThunk

  • createReducer

configureStore

The Redux toolkit configureStore function is a function that wraps the store simplifying the creation of the store. This function provides a default setup that includes common middleware and Redux devtools extension integration.

const store = configureStore({
  reducer: rootReducer,
  middleware: [/* additional middleware */],
});

createSlice

The createSlice is a function that receives an object of reducers, slice name, and initial state value. This function automatically generates a slice reducer with corresponding action creators and action types:

import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: 0,
  reducers: {
    increment: state => state + 1,
    decrement: state => state - 1,
  },
});

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

createAsyncThunk

The createAsyncThunk is a utility function that simplifies the management of asynchronous operations. It takes an action type string and a function that returns a promise. The generated thunk automatically dispatches the corresponding action creator for the pending, fulfilled, and rejected cases resolved from that promise. It is commonly used for handling data fetching, API request, or other asynchronous operations. Consider the following code example:

import { createAsyncThunk } from '@reduxjs/toolkit';
import api from 'api';

export const fetchData = createAsyncThunk('data/fetchData', async () => {
  const response = await api.getData();
  return response.data;
});

createReducer

The createReducer is a utility function that streamlines the process of creating Redux reducers. Internally, tt utilizes Immer to significantly simplify immutably updating stateful logic. By directly mapping specific action types to the corresponding reducer, createReducer ensures it updates the state when that action gets dispatched. Below is an example code:

import { createAction, createReducer } from '@reduxjs/toolkit'

const increment = createAction('counter/increment')
const decrement = createAction('counter/decrement')
const incrementByAmount = createAction('counter/incrementByAmount')

const initialState = { value: 0 }

const counterReducer = createReducer(initialState, (builder) => {
  builder
    .addCase(increment, (state, action) => {
      state.value++
    })
    .addCase(decrement, (state, action) => {
      state.value--
    })
    .addCase(incrementByAmount, (state, action) => {
      state.value += action.payload
    })
})

Lets refactor the example from the Redux core section to utilize the Redux toolkit and React Redux in a React app:

import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { useSelector, useDispatch, Provider } from "react-redux";

//reducer slice
const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
  },
});

const { increment, decrement } = counterSlice.actions;

const counterReducer = counterSlice.reducer;

var store = configureStore({ reducer: { counter: counterReducer } });

function Result() {
  const count = useSelector((state) => state.counter.value);
  return (
    <React.Fragment>
      <div>Count: {count}</div>
    </React.Fragment>
  );
}

function Actions() {
  const dispatch = useDispatch();
  const btnStyles = {
    padding: "5px 10px",
    fontSize: "1.25rem",
    display: "inline-block",
  };
  return (
    <div>
      <button style={btnStyles} onClick={() => dispatch(increment())}>
        +
      </button>
      <button style={btnStyles} onClick={() => dispatch(decrement())}>
        -
      </button>
    </div>
  );
}

function CounterPage() {
  return (
    <div
      style={{
        display: "flex",
        height: "100vh",
        justifyContent: "center",
        alignItems: "center",
        flexDirection: "column",
        rowGap: "0.625rem",
        fontFamily: "serif",
        fontSize: "1.5rem",
      }}
    >
      <Actions />
      <Result />
    </div>
  );
}

function App() {
  return <CounterPage />;
}

const element = (
  <StrictMode>
    <div>
      <Provider store={store}>
        <App />
      </Provider>
    </div>
  </StrictMode>
);

const rootElement = document.getElementById("root");
createRoot(rootElement).render(element);

Comparing Redux Core and Redux Toolkit.

We have talked a great deal about the Redux core and Redux toolkit, but how do they differ from each other. The table below throws more light into their differences:

Redux coreRedux toolkit
It requires a lot of boilerplate code.It provides a simplified and opinionated approach to handling Redux logic.
Complicated store configuration.Streamlined store configuration
Asynchronous operations are typically handled by handwritten custom action creators and middleware.It provides utility functions that simplify the process of handling asynchronous operations.
It requires a lot of packages to perform common Redux tasks.It eliminates the need for extra packages as it comes with built-in APIs that perform common Redux tasks.

Use cases of React Redux Integration

The React Redux library offers benefits that ensure efficient and maintainable code. A variety of digital user interfaces use it to simplify state complexities and enhance scalability, including the following:

  • Task Management applicationsRedux toolkit simplifies the logic to manage task lists, workflows, and task statuses in a centralized store. By optimizing the interaction of components responsible for displaying task and workflow with the store, React Redux ensures a consistent data throughout the application.

  • E-commerce applicationsReact Redux is employed to manage how the components responsible for handling shopping cart, user authentication and product data interact with the state in the store.

  • Dashboard applications: Dashboards often have multiple components that need to share and display real-time data, including complex data visualizations. These applications often require a centralized state for managing filters, data sources, and display settings. Redux toolkit and React Redux simplifies how these components manage and connect to the shared state.

  • Real-time Chat applicationsRedux toolkit streamlines the process to maintain chat state, user messages, and online status. React Redux connects chat components to the store, enabling real-time updates and interactions.

Best Practices in React Redux Development

Consider the following best practices in React and Redux development to ensure maintainability, scalability, and efficiency:

  • Avoid directly interacting with the store instance 

  • Create presentation and container component

  • Optimize components render

  • Ensure memoization with reselect

Avoid Directly Interacting With the Store Instance

Manual subscription to the store impacts performance negatively. The Redux team recommends using React Redux hooks to ensure optimal and efficient interaction of functional components with the store.

Create Presentation and Container Component

It is essential to separate components into container and presentational components. This practice ensures only React components that need to display dynamic state data are connected to the store, improving the performance of Redux apps.

Optimize Components Render

Use the React memo API to prevent unnecessary container component re-render, particularly when props remain the same.

Ensure Memoization With Reselect

Selector functions allow components to access and extract pieces of state from the store. This approach causes performance issues. By utilizing the Reselect library to create memoized selectors, developers can significantly improve performance by avoiding unnecessary recalculations of derived data.

const count = createSelector(
  (state) => state.counter.value
)

Final Thoughts

Efficient state management is crucial in sophisticated applications developed using React and Redux. The React Redux library plays a role in facilitating a seamless connection between React components and the store, making it an integral part of the React and Redux ecosystem. Thus, it is imperative to adhere to best practices when working with these libraries to ensure enhanced efficiency, scalability, and performance.

Finally, discover our extensive collection of customizable UI components tailored to your project’s requirements at Purecode AI. If exploring proves tedious, you can generate these customizable UI components effortlessly.

Additional Resources for Continuous Learning

If you enjoyed this article, consider reading the following articles to strengthen your React knowledge:

Ofili Chukwuemeka Timothy

Ofili Chukwuemeka Timothy