March 14, 2024

Redux in React With Examples

While creating a React application you would have definitely done some state management. If it is for a single component or passing data between parent and child component (not a big hierarchy of nested components) it makes sense to use useState() and props to keep it simple. But if there are lots of nested components then it becomes very complex and unwieldly to use useState() to manage state as that will require prop drilling to send data down to nested components.

One way to pass data without prop drilling is to use Context API in React, but that may also become unwieldly if you have many different context providers. As you will need to wrap those contexts around the component whose child components requires the value stored by context. So, you may end up in a scenario like this.

<Context1.Provider>
  <Context2.Provider>
    <Context3.Provider>
            <ComponentA></ComponentA>
    </Context3.Provider>
  </Context2.Provider>
</Context1.Provider>

Another option to store state is using Redux library that provides a centralized store for storing application wide state that can be accessed by any component with in the application. For example, an authentication state which may be required to change menu options in navbar. Redux gives you an option to easily manage global state which can be updated or accessed by any component. Doing that requires some work so let's see how to go with setting up Redux managed state.

How Redux works

In order to understand how Redux works you should know about three entities.
  1. Store- A single centralized store that contains the state. Components subscribe to this store, whenever there is a change in state, components are notified. Then, components can get the required data from the state object. State object may have lot of properties and a specific component may require only a part of that data that's what I mean by getting the required data from the state object.
  2. Reducer function- The saved state in the store will also require changes. Be aware that components never directly change the state. That is done by using a reducer, which is just a simple function that takes two parameters- current state and action. For example-
    const counterReducer = (state = {counter:0}, action) => {
    
    }
    

    Reducer function has the logic to update the state if necessary and it returns the new state. Please note one very important point that the reducer is not allowed to change the existing state. Change to the state must be immutable; copy the existing state and make changes to the copied values.

    Reducers are pure functions that take the previous state and an action, and return the next state.

  3. Action- So, reducer function is used to modify the state but how does reducer know what action to perform and when? That's where action comes, which is dispatched from a component. You can think of an action as a event that describes something that has happened in the application. Action is an object which, by convention, has two properties type and payload (optional). The type field should be a string that describes the action and any additional information if needed is passed in payload field. For example, a typical action object might look like this:
    const addTodoAction = {
      type: 'todos/todoAdded',
      payload: 'Write an article'
    }
    

    In the type string first part is the category that this action belongs to and the second part is the event that happened.

Redux data flow

Flow for state storage, change to the state and UI re-rendering based on the changed state can be described by following steps.

Initial setup:

  • A Redux store is created using a root reducer function. In most cases you will split the data handling logic and create multiple reducers. These multiple reducers are combined together to create a single root reducer.
  • The store calls the root reducer once, and saves the return value as its initial state.
  • When the UI is first rendered, UI components access the current state of the Redux store, and use that data to decide what to render. They also subscribe to any future store updates so they can know if the state has changed.

Updating state and re-rendering

  1. An event occurs that require a state change. For example, user clicks a button.
  2. The app code dispatches an action to the Redux store, like dispatch({type: 'counter/incremented'})
  3. The store runs the reducer function again with the previous state and the current action. Reducer function may have several conditions for different action types. Based on the passed action type it runs the appropriate logic and returns the new state which is stored in the store as new state.
  4. The store notifies all parts of the UI that are subscribed that the store has been updated
  5. Each UI component that needs data from the store checks to see if the parts of the state they need have changed.
  6. Each component that sees its data has changed forces a re-render with the new data, so it can update what's shown on the screen.
React-Redux example

Redux with React example

In this simple example we'll use a Redux store to store counter and dispatch increment and decrement actions to change the state.

First thing you need is to install redux and react-redux. Here redux is the core library and react-redux is the React binding library for Redux. Go to the app location where you need these libraries and run the following command.

npm install redux react-redux

Creating store

import { createStore} from 'redux'
import counterReducer from '../reducers/counterReducer';
export const appStore = createStore(counterReducer);

Here note that in this example createStore is used which is deprecated now and Redux suggests to use configureStore() of @reduxjs/toolkit package. In my opinion using createStore() gives a better understanding of the above explained steps so just go through it to understand working of Redux better but in real project use configureStore().

Refer this post- Redux Toolkit in React With Examples to know more about using @reduxjs/toolkit.

Reducer function

In the createStore(), reducer is passed as an argument so that is the function we'll write now.

counterReducer.js

const counterReducer = (state = {counter:0}, action) => {
    switch(action.type){
        case "INCREMENT":
            return {
                counter: state.counter + 1
            }
        case "DECREMENT":
            return {
                counter: state.counter - 1
            }
        default:
            return state;
    }   
}

export default counterReducer;

In the reducer function you should take note of the following points-

  1. As the parameters to the function current state (initialized with default state) and action is passed.
  2. Switch case is used to go through the action types and change the counter state accordingly and return the new state.
  3. There is also a default returning the existing state.

Provider

React Redux includes a <Provider /> component, which makes the Redux store available to the rest of your app. We'll wrap <App /> component with the Provider which guarantees that the store provided with this Provider is available to App component and all its child components. That change can be done in index.js file.

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

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

Provider has a store prop that's where we pass our store so that react-redux know which store to provide.

Component

Note that Component uses Bootstrap for styling

Components/Counter.js

import { useDispatch, useSelector } from "react-redux";

const Counter = () => {
    const counter = useSelector((state) => state.counter);
    const dispatch = useDispatch();
    const incrementHandler = () => {
        dispatch({type: 'INCREMENT'});
    }
    const decrementHandler =  () => {
        dispatch({type: 'DECREMENT'});
    }
    return (
        <div>
            <h2>Counter</h2>
            <div>
                <button className="btn btn-primary mx-2" onClick={incrementHandler}>+</button>            
                <span>{counter}</span>            
                <button className="btn btn-primary mx-2" onClick={decrementHandler}>-</button>
            </div>
        </div>
    );
}

export default Counter;

Some important points to note here-

  1. Two hooks useSelector() and useDispatch() are used here which are imported from react-redux.
  2. useSelector() hooks allows you to extract data from the Redux store state for use in this component. The selector will be called with the entire Redux store state as its only argument. The selector may return any value as a result, including directly returning a value that was nested inside state, or deriving new values.
  3. useSelector() hook also does the job of subscribing this component to the Redux store
  4. Selector runs whenever an action is dispatched. Component is re-rendered if the returned value of the selector is different from the previous selector value.
  5. useDispatch() hook returns a reference to the dispatch function from the Redux store. You may use it to dispatch actions.
  6. Different actions are dispatched based on whether increment or decrement button is clicked.
  7. In the Reducer appropriate case is executed based on the passed action type and the new state is returned.
  8. This new state is saved in the store and the subscribed components are notified of the change.
  9. useSelector() gets the state.counter value from the state since that value is different from the previous value so the component is re-rendered.
Redux with React

Redux with React example with action payload

In the previous example in the action object only 'type' field is used, in this example we'll add payload field too. For this we'll modify the previous example itself and provide the facility to add a passed value to the counter.

Redux counter example

counterReducer.js

A new case for increasing the counter by given amount is added.

const counterReducer = (state = {counter:0}, action) => {
    switch(action.type){
        case "INCREMENT":
            return {
                counter: state.counter + 1
            }
        case "DECREMENT":
            return {
                counter: state.counter - 1
            }
        case "INCREMENT_BY_VALUE":
            return {
                counter: state.counter + Number(action.payload)
            }
        default:
            return state;
    }   
}

export default counterReducer;

Component

In the Counter component a new action is dispatched with type as 'INCREMENT_BY_VALUE' and payload as the amount entered by user.

import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";

const Counter = () => {
    const counter = useSelector((state) => state.counter);
    const dispatch = useDispatch();
    const [incrementByValue, setIncrementByValue] = useState(0);
    const incrementHandler = () => {
        dispatch({type: 'INCREMENT'});
    }
    const decrementHandler =  () => {
        dispatch({type: 'DECREMENT'});
    }
    const incrementByValueHandler = () => {
        dispatch({type: 'INCREMENT_BY_VALUE',
                payload: incrementByValue});
    }
    return (
        <div>
            <h2>Counter</h2>
            <div>
                <button className="btn btn-primary mx-2" onClick={incrementHandler}>+</button>            
                <span>{counter}</span>            
                <button className="btn btn-primary mx-2" onClick={decrementHandler}>-</button>
                <div className="mt-2">
                    <input value={incrementByValue} onChange={e => setIncrementByValue(e.target.value)}></input>
                    <button className="btn btn-primary mx-2" onClick={incrementByValueHandler}>Add Amount</button>
                </div>
            </div>
        </div>
    );
}

export default Counter;

Source: https://redux.js.org/tutorials/fundamentals

That's all for the topic Redux in React With Examples. If something is missing or you have something to share about the topic please write a comment.


You may also like

No comments:

Post a Comment