DEV Community

Ganesh Shetty
Ganesh Shetty

Posted on

An Overview of Redux and its Middleware for React Applications

In this post we will be looking at how redux works and its core concepts, principles, and patterns for using Redux.This will be mainly focusing on understanding concept in depth rather than exploring with example.

What is Redux?

Redux is a pattern and library for managing and updating application state, using events called "actions". It serves as a centralised store for state that needs to be used across your entire application, with rules ensuring that the state can only be updated in a predictable fashion.

When Should I Use Redux?
Redux is more useful when:

  • You have large amounts of application state that are needed in many places in the app
  • The app state is updated frequently over time
  • The logic to update that state may be complex
  • The app has a medium or large-sized codebase, and might be worked on by many people

To understand all these in depth let us take a small example

function Counter() {
  // State: a counter value
  const [counter, setCounter] = useState(0)

  // Action: code that causes an update to the state when something happens
  const increment = () => {
    setCounter(prevCounter => prevCounter + 1)
  }

  // View: the UI definition
  return (
    <div>
      Value: {counter} <button onClick={increment}>Increment</button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

This is small example of one way data flow

  • State describes the condition of the app at a specific point in time
  • The UI is rendered based on that state
  • When something happens (such as a user clicking a button), the state is updated based on what occurred
  • The UI re-renders based on the new state

Image description

As the application grows maintaining this one way data flow pattern gets very complicated especially if those components are located in different parts of the application.
One way to solve this problem is by passing props from parent to child components, but that doesn't always help.

So the best to solve this problem is to have centralised store so whichever components is subscribed to the store will get notified when the state is updated,

Thats awesome rite, No need to pass props down from parent to all of its children,

This is the basic idea behind Redux: a single centralized place to contain the global state in your application, and specific patterns to follow when updating that state to make the code predictable.

Redux Terminology:-
Actions:
An action is a plain JavaScript object that has a type field. You can think of an action as an event that describes something that happened in the application.

Reducers:
A reducer is a function that receives the current state and an action object , it calculates the new state value based on the state and action arguments

You can think of a reducer as an event listener which handles events based on the received action (event) type.

Store:
Entire Redux application state lives in an object called store.

store can be created by passing in a reducer,It has a method store.getState() which returns current state value

Dispatch:

The Redux store has a method called dispatch. The only way to update the state is to call store.dispatch() and pass in an action object. The store will run its reducer function and save the new state value inside, and we can call getState() to retrieve the updated value

Core Concepts and Principles:-

Single Source of Truth
The global state of your application is stored as an object inside a single store. Any given piece of data should only exist in one location, rather than being duplicated in many places.

State is Read-Only
The only way to change the state is to dispatch an action, an object that describes what happened.

Redux Application Data Flow:-

Earlier, we talked about "one-way data flow", which describes this sequence of steps to update the app:

Redux uses a "one-way data flow" app structure

  • State describes the condition of the app at a specific point in time
  • The UI is rendered based on that state
  • When something happens (such as a user clicking a button), the state is updated based on what occurred
  • The UI re-renders based on the new state

For Redux specifically, we can break these steps into more detail:

Initial setup

  • A Redux store is created using a root reducer function
  • 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.

Updates

  • Something happens in the app, such as a user clicking a button
  • The app code dispatches an action to the Redux store, like dispatch({type: 'counter/incremented'})
  • The store runs the reducer function again with the previous state and the current action, and saves the return value as the new state
  • The store notifies all parts of the UI that are subscribed that the store has been updated
  • Each UI component that needs data from the store checks to see if the parts of the state they need have changed.
  • 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

Here's what that data flow looks like visually:

Image description

Reading State from the Store with useSelector
We somehow should be able to get store data it in our component, useSelector hook, which lets your React components read data from the Redux store.

useSelector accepts a single function, which we call a selector function. A selector is a function that takes the entire Redux store state as its argument, reads some value from the state, and returns that result.

But, what happens if we dispatch an action and The Redux state will be updated by the reducer, but our component needs to know that something has changed so that it can re-render with the new list of data.

Fortunately, useSelector automatically subscribes to the Redux store for us! That way, any time an action is dispatched, it will call its selector function again right away.

const todos = useSelector(state => state.todos)

Above line shows how to read data from redux store in react component using useSelector hooks.

There is one problem, the component will re-render any time the selector result is a new reference! so we should use shallowEqual as a second arguments to useSelector hooks which checks if the state has actually changed.

Dispatching Actions with useDispatch

We now know how to read data from the Redux store into our components. But, how can we dispatch actions to the store from a component? We know that outside of React, we can call store.dispatch(action). Since we don't have access to the store in a component file, we need some way to get access to the dispatch function by itself inside our components.

The React-Redux useDispatch hook gives us the store's dispatch method as its result.

So, we can call const dispatch = useDispatch() in any component that needs to dispatch actions, and then call dispatch(someAction) as needed.

What is Middleware and Why we need it in Redux?

As we have seen, Redux store does not know asycn logic,It only knows how to synchronously dispatch an action,

And Redux should never contain side effects,A "side effect" is any change to state or behaviour that can be seen outside of returning a value from a function. Some common kinds of side effects are things like:

  • Logging a value to the console
  • Saving a file
  • Setting an async timer
  • Making an AJAX HTTP request

Any real app will need to do these kinds of things somewhere. So, if we can't put side effects in reducers, where can we put them?

Some people say we can easily do this in the component itself,Thats right we can, what if we have to use the same logic in other components as well, We will have to duplicate logic in that component as well.

And one more reason we should not write async logic/any complex logic in component is we should try to keep it small
so that it's much easier to change and maintain.

So if we go with keeping our async logic outside of our JSX component, with the existing redux pattern its not possible
As we can not wait till the async operation completed,We can write our own middleware logic to handle async operator.

But Why to write custom logic when we already have readymade middleware to enable writing side effects logic.

Redux middleware were designed to enable writing logic that has side effects.
"Side effects" are code that changes state/behaviour outside a function, like AJAX calls, modifying function arguments, or generating random values.

Middleware add an extra step to the standard Redux data flow,
We know the app code dispatches an action to the Redux store,
With middleware, After dispatching an action, it will first go through the middleware, then into the reducer.

Redux Async Data Flow:-
Just like with a normal action, we first need to handle a user event in the application, such as a click on a button. Then, we call dispatch(), and pass in something, whether it be a plain action object, a function, or some other value that a middleware can look for.

Once that dispatched value reaches a middleware, it can make an async call, and then dispatch a real action object when the async call completes.

Image description

Action Creators:-

  • An action creator is a function that creates and returns an action object. We typically use these so we don't have to write the action object by hand every time.

  • Action creator functions encapsulate preparing action objects and thunks.

  • Action creators can accept arguments and contain setup logic, and return the final action object or thunk function.

Source:(https://redux.js.org/)

Discussion (1)

Collapse
rajeshroyal profile image
Rajesh Royal

Nice Article @ganeshshetty195

One addition - if the project is using Redux and have api calls, Its better to use Redux toolkit. We will have Global state management and Query management both.