10 min read

Redux in State Management


Luka Žagar




Redux in State Management

Dan Abramov and Andrew Clark designed Redux, a lightweight state management tool used for JavaScript applications, and it was published in 2015.

For JavaScript and React applications, Redux is a standard data store. Data binding should move in one direction and should be saved as a single source of truth; this was according to the basic concept. The popularity of Redux stems from the design concept's simplicity and the fact that it is reasonably easy to apply.

Redux was developed on three main concepts, according to its official documentation:

  • • Within a single store, the state of your entire application is kept in an object tree.
  • • By emitting a descriptive action, ensure that the application state is read-only and that changes are required.
  • • Write pure reducer functions to specify how actions alter the state tree.

Each component gets direct access to the state since the complete state of your application is concentrated in one area (at least without sending props to child components or callback functions to parent components).

When Can We Use Redux

Redux has recently become one of the most contentious topics in the frontend community. Redux quickly became one of the most talked-about games after its debut. Many people praised it, while others criticized it.

Redux lets you manage your program's state in one location, making updates more predictable and traceable. It makes it easier to understand why your app is changing. All of these advantages, however, come with tradeoffs and limitations. It may appear that it adds boilerplate code, making simple tasks more complex. However, this is dependent on the design selections.

A straightforward solution to this topic is that you will know when you need Redux on your own. If you're still indecisive about whether or not you require it-this usually occurs when your app reaches a point where managing the app state becomes a chore, and you begin looking for ways to make it easier and simpler.

What is State Management in Redux?

State management is fundamentally a means of facilitating data transfer and communication between components. It creates a readable and writable data structure to reflect the current state of your app. You can view normally invisible states while interacting with them this way.

Like React, Angular, and others, most frameworks include a mechanism for components to maintain their state internally, eliminating the need for an additional library or tool. It works fine for applications with a few components, but handling theshared states between components becomes a hassle as the application grows more prominent.

It might be hard to predict the exact place a state should live.

Data in a component should ideally live in only one component, making data transfer between siblings difficult.

To communicate data among siblings in React, a state must reside in the parent component. The parent component provides a method for modifying this state, which is sent as a prop to these sister components.

Here's an example of a React login component. The login component's input has an impact on what is displayed by its sibling component, the status component:

interface User { username: string; password: string; id: number; } enum UserStatus { NotLoggedIn = 'NOT LOGGED IN', LoggedIn = 'LOGGED IN', } const users: User[] = []; function App() { const [userStatus, setUserStatus] = React.useState(UserStatus.LoggedIn); const setStatus = React.useCallback((username: string, password: string) => { const user = users.find((user) => user.username === username && user.password === password); if (user) setUserStatus(UserStatus.LoggedIn); }, []); return ( <div> <Status status={userStatus} />/ <Login handleSubmit={setStatus} /> </div> ); } export default App;

Consider what occurs when a state must be transferred across components separated by a significant distance in the component tree.

The state must be transmitted from one component to the next until it reaches its destination.

Essentially, the state must be hoisted up to the next parent component and then to the next until it reaches an ancestor shared by both components that require the state, at which point it is transferred down. As a result, the state becomes more difficult to maintain and unpredictable. It also entails sending information to components that don't require it.

As the app becomes more complicated, it's evident that state management becomes a disaster. This is why you'll need a state management tool like Redux to help you keep track of your states. Before we look at the benefits of Redux, let's have a basic understanding of the ideas.

How Redux Works

It's easy to understand how Redux works. The complete state of the program is stored in a central store. Without needing to transmit down props from one component to another, each component may access the saved state.

Actions, stores, and reducers are the three main components.

Let's have a glance at what each of them does. This is significant because they assist you in comprehending the benefits of Redux and how to use it. I'm going to construct a similar example to the login component above but in Redux this time.

Types of Data in Redux

One and only source of truth

Within a single store, your application's global state is kept in an object tree.

Because the state from your server can be serialized and hydrated into the client with no additional coding effort, it's simple to design universal apps. A single state tree also makes it easier to debug or inspect an application. It allows you to save the state of your app while developing, resulting in a shorter development cycle. If all of your states are kept in a single tree, some functionality that has historically been difficult to achieve, such as Undo/Redo, becomes straightforward to provide.

The current state is read-only.

The only way to change the state is to issue an action, which is an object that describes what occurred. This guarantees that neither the views nor the network callbacks will ever write to the state directly. Instead, they reflect a desire to change the state. There are no subtle race conditions to be aware of because all changes are centralized and rigidly occur one by one. Because actions are simply objects, they can be logged, serialized, stored, and replayed for debugging and testing.

Pure functions are used to make changes.

Pure reducers are used to indicate how actions alter the state tree. Reducers are simple functions that return the next state after taking the previous state and an action. Instead of changing the prior state, remember to return new state objects. You can start with a single reducer and split it into smaller reducers that control some aspects of the state tree as your app grows. Reducers are merely functions, so you can customize how they're called, pass extra data, and even create reusable reducers for everyday tasks like pagination.

Actions and state should only contain plain JS values.

In Redux state and actions, you should only store pure JavaScript values like objects, arrays and primitives. You should not put instances of classes, functions and other non-serializable values into Redux. Functions should always give the same output for same input, so that means that they are immutable and there is no reason to store them into Redux. As for classes and other non-serializable values the reason is that your UI will update properly and debugging via the Redux DevTools will work as expected. The only exception to this rule is that you could put non-serializable values into actions IF the action will be stopped by middleware before they reached the reducer.

Redux Toolkit

Redux Toolkit is package intended to be standard way of writing Redux. It helps with three most common problems:

  • • Configuring store
  • • Boilerplate
  • • Many packages

Redux Toolkit takes complicated setup and reduces it to the minimum. If you are asking if you have lost some functionalities, don’t worry, you have all functionalities as before but with less code.

The first of many useful functions is configureStore() function. This function creates empty store and as parameter receive object that must contain reducer in which all of reducers are stored. As optional parameters we can pass middleware, enhancers and others.

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

The most useful function of Toolkit is createSlice(). This function allows us to configure our reducer and actions in one single take. It is very easy to use and it has good support for TypeScript. As parameter it receives object that name of slice, initial state and reducer. Initial state defines what kind of types could be stored in the state. It takes type of initial state and applies it to the reducer. In reducer we are creating the actions for the current reducer, so there is no more need to have switch case statement. In actions we are mutating state of reducer, but not exactly. The code that we write may seem that we are mutating the state but in fact function recognize that we are mutating state and returns new state instead. This is only true when we are writing in createSlice() function.

import { createSlice, PayloadAction } from '@reduxjs/toolkit'; interface AuthState { status: UserStatus; } const initialState: AuthState = { status: UserStatus.NotLoggedIn, }; const authSlice = createSlice({ name: 'auth', initialState, reducers: { setStatus: (state, action: PayloadAction<UserStatus>) => { state.status = action.payload; }, }, }); export const { setUser } = authSlice.actions; export const authReducer = authSlice.reducer; // const store = configureStore({ // reducer: { // auth: authReducer, // }, // });

Actions in Redux

To put it another way, actions are occurrences. They're the only way for your app to deliver data to your Redux store. User interactions, API calls, or even form submissions can all provide data.

The store.dispatch() method is used to dispatch actions.

Actions are simple JavaScript objects with a type property that specifies the type of action that will be performed.

They must also have a payload containing the data that the action should process. An action creator is used to build actions.

Reducers in Redux

Reducers are simple functions that take an application's current state, execute an action, and return a new state. These states are objects that specify how an application's state changes in response to an action made to the store.

It is based on the reduce function in JavaScript, which calculates a single value from many values following the execution of a callback function.

Reducers take the app's initial state and return a new one based on its action.

They don't affect the data in the object supplied to them, and they don't have any side effects in the application like pure functions.

They should consistently deliver the same result when given the same object.

Store in Redux

The app's state is stored in the store. In any Redux application, it is strongly advised to keep only one store. The modern redux uses Provider to pass the store to children.

Through helper methods, you may get at the stored state, update it, and register or unregister listeners.

Let's make a store for our authentication app:

function App() { const [userStatus, setUserStatus] = React.useState(UserStatus.LoggedIn); const setStatus = React.useCallback((username: string, password: string) => { const user = users.find((user) => user.username === username && user.password === password); if (user) setUserStatus(UserStatus.LoggedIn); }, []); <Provider store={store}> <div> <Status status={userStatus} />/ <Login handleSubmit={setStatus} /> </div> </Provider>; } export default App;

There is only one general state in the store using Redux, and each component has access to it. This reduces the need to transmit the state from one component to another regularly.

You can also choose a slice from the store for a specific component, which improves the efficiency of your program. That means the part of your app re-renders only when the data relevant to that part changes.

Redux hooks

In React-Redux we use two hooks, useSelector and useDispatch hook. These two hooks need to be inside component that is wrapped by Provider component. UseSelector hook gives us option to specify which part of the store do we need, to reduce number of re-renders in the app. It can be extended with TypeScript so that we have access only to the data in the store.

type RootState = ReturnType<typeof store.getState>; const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; const status = useAppSelector((state) => state.auth.status);

UseDispatch hook gives access to dispatch function that can be also extended with TypeScript so that we can only use actions that are defined in reducer.

export function useAppDispatch() { return useDispatch<AppDispatch>(); } export type AppDispatch = typeof store.dispatch;

Redux middleware

Before they're delivered to the reducer function, Redux allows developers to intercept any actions dispatched from components. Middlewares are used to intercept the data.

We might want to sanitize the user's input before it reaches our store for further processing, based on the example Login component presented in the previous section. Redux middleware can help you achieve this.

Middlewares, in technical terms, are functions that, after processing the current action, call the next method in an argument. After every dispatch, they are called.

The following is an example of simple middleware:

function simpleMiddleware({ getState, dispatch }) { return function(next) { return function(action) { const nextAction = next(action); const state = getState(); return nextAction; } } }

This may appear daunting, but you may not need to write your own middleware in most circumstances because the large Redux community has already made a number of them available. If you believe middleware is necessary, you will appreciate it because it empowers you to execute a lot of fantastic work with the best abstraction.

Benefits of Redux in State Management

  1. With the help of the global accessing capability, developers may easily control the application's state.
  2. Any change to the state causes the child components to be disrupted, impacting performance. The Redux library, on the other hand, centralizes the application's state management. This allows the developer to leverage essential development capabilities like undo/redo, state persistence, and so on.
  3. Tracking the status of the application during the debugging process in React is really challenging. On the other hand, Redux offers a pleasant developer experience, allowing for "time-travel debugging" and even sending complete fault reports to a server.
  4. React has a complex user interface, making data flow challenging when multiple components are used to share the git pull same data. On the other hand, Redux is versatile across all UI layers and has an extensive ecosystem of add-ons to meet your needs.
  5. Because React is so intimately connected with the root component, reusing components is highly challenging. Redux simplifies application development and enables global accessibility, making it easier to test and deploy apps in various settings (client, server, and native).

Why use Redux?

States won't need to be raised up when utilizing Redux with React. This makes it simple to determine which action is responsible for any changes.

As you can see from the preceding example, the component needs not to provide any state or function for its children components to communicate data. Redux is in charge of the entire process. This drastically reduces the app's complexity and makes it easier to maintain.

This is the most important reason to utilize Redux, but it isn't the only one. Take a look at the table below for a quick overview of the benefits of using Redux for state management.

Redux Makes the State Predictable

The state of Redux is always predictable. Because reducers are pure functions, they always deliver the same result when the same state and action are supplied to them. The status is also unchangeable and unalterable. This allows for complex jobs like limitless undo and redo to be implemented.

It's also possible to use time travel, which allows you to go back and forth between past states and see the repercussions in real-time.

Redux is Maintainable

Redux has tight guidelines for how code should be arranged, making it easy for someone who knows Redux to comprehend the structure of any Redux application. This makes it easier to maintain in general. This also aids in the separation of business logic from the component hierarchy. Large-scale apps need to be more predictable and maintained.

Debugging is Easy in Redux

Debugging an application is simple with Redux. It is simple to understand code problems, network errors, and other types of issues that may arise during production by logging activities and state.

It offers excellent DevTools, including the ability to time-travel activities, persists actions on page refresh, and so on. Debugging consumes more time than developing features in medium and large-scale apps. The Redux DevTools package makes it simple to use all of Redux's features.

Performance Benefits

It's reasonable to think that keeping the app's state global will degrade performance. That isn't the case to an enormous extent.

Many performance optimizations are built into React Redux internally, ensuring that your connected component only renders when required.

Ease of Testing

Because functions are used to update the state of pure functions, testing Redux programs is simple.

State Persistence

Some of the app's state can be saved to local storage and restored after a refresh. This has the potential to be really useful.

Server-Side Rendering

For server-side rendering, Redux can be utilized. You can use it to manage the app's initial render by delivering the app's state and its response to the server request to the server. Following that, the necessary components are rendered in HTML and provided to the clients.

Implementing Redux in Your App?

Debugging React applications can be challenging, especially when there is a complicated state. Therefore, using LogRocket to track Redux state and actions is a good idea. Try LogRocket if you want to monitor and track Redux state in production for all of your users.