app-generator / docs

App Generator - The Official Documentation | AppSeed
https://docs.appseed.us
1 stars 1 forks source link

[React] Redux #114

Open mahfujul-helios opened 1 month ago

mahfujul-helios commented 1 month ago

Redux

what is redux ?

Redux is an open-source JavaScript library commonly used with React for managing the state of web applications. It provides a centralized store to manage the entire application's state, making state management predictable and efficient, especially in large-scale applications with complex data flow.

When to use Redux

Not long after its release, Redux became one of the hottest topics of debate in the frontend world.

Redux allows you to manage your app’s state in a single place and keep changes in your app more predictable and traceable, making it easier to understand the changes happening in your app. But all of these benefits come with a set of challenges. Some developers argue that Redux introduces unnecessary boilerplate, potentially complicating what are otherwise simple tasks. However, this depends on the architectural decisions of the project.

So, when should you use Redux? One simple answer to this question is that you will organically realize for yourself when you need Redux. If you’re unsure about whether you need it, you probably don’t. This usually happens when your app grows to a scale where managing app state becomes a hassle and you start looking for ways to make it simplify it.

Using Redux with React

As we mentioned earlier, Redux is a standalone library that can be used with different JavaScript frameworks including Angular, Inferno, Vue, Preact, React, etc. However, Redux is most frequently used with React.

This is because React only allows for a uni-directional flow of data. That means data cannot be sent from a child to a parent; it has to flow downward from the parent to the child. This thought model works very well with Redux where we cannot directly modify the state. Instead, we dispatch actions that intend to change the state, and then separately, we observe the resulting state changes.

State management with Redux

State management is essentially a way to facilitate the communication and sharing of data across components. It creates a tangible data structure to represent the state of your app that you can read from and write to. That way, you can see otherwise invisible states while you’re working with them.

Most libraries, such as React and Angular, are built with a way for components to internally manage their state without the need for an external library or tool. This works well for applications with few components, but as an application grows larger, managing states shared across components becomes a hassle.

In an app where data is shared among components, it might be confusing to actually know where a state should live. Ideally, the data in a component should live in just one component, so sharing data among sibling components becomes difficult.

For example, to share data among siblings in React, a state has to live in the parent component. A method for updating this state is provided by the parent component and passed as props to these sibling components. Here’s a simple example of a login component in React. The input of the login component affects what is displayed by its sibling component, the status component:

import React, { useState } from 'react';

const App = () => {
  const [count, setCount] = useState(0);

  const handleIncrement = (incrementBy) => {
    const numberToIncrement = incrementBy || 1;
    setCount(count + numberToIncrement)
  }

  return (
    <div>
      <BigCountDisplay count={count} />
      <CounterButton onIcrement={handleIncrement} />
    </div>
  );
}

export default App;

Remember, this data is not needed by the parent component, but because its children need to share data, it has to provide a state.

Now imagine what happens when a state has to be shared between components that are far apart in the component tree. Basically, the state will have to be lifted up to the nearest parent component and upwards until it gets to the closest common ancestor of both components that need the state, and then it is passed down. This makes the state difficult to maintain and less predictable.

It’s clear that state management gets messy as the app gets more complex. This is why you need a state management tool like Redux to more easily maintain these states. Now, let’s take a look at Redux concepts before considering its benefits.

How Redux works

The way Redux works is simple. There is a central store that holds the entire state of the application. Each component can access the stored state without having to send down props from one component to another.

There are three core components in Redux — actions, store, and reducers. Let’s briefly discuss what each of them does. This is important because they help you understand the benefits of Redux and how it can be used.

We’ll be implementing a similar example to the login component above but this time in Redux.

💡 store refers to the object that holds the application data shared between components.

Redux actions

Redux actions are events. They are the only way you can send data from your application to your Redux store. The data can be from user interactions, API calls, or even form submissions.

Actions are plain JavaScript objects that must have:

A type property to indicate the type of action to be carried out A payload object that contains the information that should be used to change the state Actions are created via an action creator, which is a function that returns an action. Actions are executed using the dispatch() method, which sends the action to the store:

Here’s an example of an action:

{ 
  type: "INCREMENT",
  payload: {
    incrementBy: 5,
  }
}
And here is an example of an action creator. It is just a helper function that returns the action:

const getIncrementAction = (numberToIncrement) => {
  return {
    type: "INCREMENT",
    payload: {
      incrementBy: numberToIncrement,
    }
  }
}

Redux reducers

Reducers are pure functions that take the current state of an application, perform an action, and return a new state. The reducer handles how the state (application data) will change in response to an action:

redux

💡 A pure function is a function that will always return the same value if given the same parameters, i.e., the function depends on only the parameters and no external data.

Reducers are based on the reduce function in JavaScript, where a single value is calculated from multiple values after a callback function has been carried out.

Here is an example of how reducers work in Redux:

const CounterReducer = (state = initialState, action) => {
    switch (action.type) {
      // This reducer handles any action with type "LOGIN"
      case "INCREMENT":
          return state + action.incrementBy ? action.incrementBy : 1
       default:
          return state;
      } 
};

Hence, if the initial state was 12, after the action to increment it by five gets dispatched and processed, we get the new value of the state, i.e., 17.

💡 Reducers take the previous state of the app and return a new state based on the action passed to it. As pure functions, they do not change the data in the object passed to them or perform any side effect in the application. Given the same object, they should always produce the same result.

Redux store

The store is a “container” (really, a JavaScript object) that holds the application state, and the only way the state can change is through actions dispatched to the store. Redux allows individual components to connect to the store.

It is highly recommended to keep only one store in any Redux application. You can access the stored state, update the state, and register or unregister listeners via helper methods. Basically, the components get to update the store via actions and then subscribe to the changes to the store so they know when to re-render:

redux1

Redux toolkit

Redux is a great utility for state management in React. But, as we mentioned before, it can introduce a lot of boilerplate into your application due to the verbosity of its API. Because of this, it is recommended to use the Redux Toolkit while using Redux with React. Let’s look at a few benefits that this provides.

Setting up a store with Redux toolkit While setting up a store with pure Redux can be quite cumbersome, with Redux Toolkit, it is a single call to the configureStore function. This is how we can set up a store that has a counterReducer slice in it:

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterReducer'

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

export default store

Creating an action using Redux Toolkit

Redux Toolkit also provides us with utilities to generate actions. In Redux, actions are just normal objects with a type and a payload field. The createAction utility from Redux Toolkit returns us a function. We can call that function with any object and it will get dispatched as the payload for that particular action:

const addTodo = createAction('INCREMENT')
addTodo({ val: 5 })
// {type : "INCREMENT", payload : {val : 5}})

Creating the reducer with Redux Toolkit

The reducer in Redux is a normal, pure function that takes care of the various possible values of state using the switch case syntax. But that means several things need to be taken care of — most importantly, keeping the state immutable.

Redux Toolkit provides the createReducer utility, which internally uses immer. This allows us to freely mutate the state, and it will internally be converted to an immutable version. This is how a reducer defined with Redux Toolkit would look like:

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

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

const initialState = { value: 0 }

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

Notice how easy that makes the task of playing with the state. The difference is more evident when the state is more complex with several nested objects. In the scenario without Redux Toolkit, we have to be careful to keep all the operations immutable but the equivalent code with Toolkit is much more simplified:

// a case without toolkit, notice the .map to create a new state
case 'TOGGLE_TODO': {
  const { index } = action.payload
  return state.map((todo, i) => {
    if (i !== index) return todo

    return {
      ...todo,
      completed: !todo.completed,
    }
  })
}

// a case with toolkit, notice the mutation which is taken care internally
.addCase('TOGGLE_TODO', (state, action) => {
  const todo = state[action.payload.index]
  // "mutate" the object by overwriting a field
  todo.completed = !todo.completed
})

With that in place, let’s now move to learning about what Redux middleware are and how they can further simplify the overall experience.

Redux middleware

Redux allows developers to intercept all actions dispatched from components before they are passed to the reducer function. This interception is done via middleware, which are functions that call the next method received in an argument after processing the current action.

Here’s what a simple middleware looks like:

function simpleMiddleware({ getState, dispatch }) {
  return function(next){
    return function(action){
      // processing
      const nextAction = next(action);
      // read the next state
      const state = getState();
      // return the next action or you can dispatch any other action
      return nextAction;  
    }
  }
}

This might look overwhelming, but in most cases, you won’t need to create your own middleware because the Redux community has already made many of them available. If you feel middleware is required, you will appreciate its capacity to enable great work with the best abstraction.

conclusion

Redux is a powerful JavaScript library commonly used with React for managing the state of web applications. It provides a centralized store to manage the entire application's state, following a predictable and efficient pattern. With concepts like actions, reducers, and a unidirectional data flow, Redux simplifies state management by enforcing a clear separation of concerns and providing a single source of truth for the application's state. Redux offers benefits such as predictability, scalability, and powerful debugging capabilities through tools like Redux DevTools. While Redux may introduce some initial setup and boilerplate code, it remains a popular choice for state management in React applications, especially for projects requiring complex data flow or scalability. Overall, Redux is a valuable tool for building maintainable, scalable, and predictable React applications.