React Hooks vs Redux: Best React State Management Approach

By Saurabh Barot   |   16 November, 2022
React Hooks vs Redux: Best React State Management Approach

Quick Summary: One of the toughest problems developers face while making React apps is react state management. Commercial web programs can be quite complex, which might not be enough. Some programmers integrate it with app state management technologies like Redux, while others use React Hooks to address similar problems.

Hooks are brand-new components of React 16.8. They allow you to use more React capabilities, such as keeping your component’s state constant or even running an after-effect when certain state changes occur. Any dynamic software must have state management as an essential and unavoidable part. React provides a straightforward and extensible API to make state management in a React component easier.

What is React State Management?

React State Management

The state objects in React Components are pre-built. Between component renders, permanent assets are stored in the state, encapsulated data. A JavaScript data structure goes by the name “state.” The UI may appear entirely different if a user changes their state due to interacting with your application since the new state is used to represent it, not the previous one.

Let’s explore why state management in React is necessary now that you know its workings.

Why Do We Need State Management In React?

React apps are developed using components, and they internally handle their state. This works fine for applications with a small number of components, but as the application gets larger, it becomes challenging to manage the complexity of shared states between components. Here is a straightforward illustration of an e-commerce application where purchasing a product can alter the status of several components.

  • Add that item to your list of to-buys.
  • Adding a product to the customer history
  • calculate the number of items purchased

It can be difficult to diagnose the problem when something goes wrong if developers do not keep scalability in mind. Because of this, your application needs state management.

Let’s talk about how to use Redux and React Hooks for state management in React.

Also Read: Latest React 18 Features & Changes

What is Redux?

Redux is a step-by-step instructions container for JavaScript applications, according to the Redux documentation, which enables us to create programs that act predictably, operate in a variety of settings, and are simple to test. Prop drilling has the drawback of necessitating a substantial amount of additional coding to retrieve data from a top-level component. Redux exacerbates this drawback since creating a global state necessitates additional programming.

Redux Work flow

Most React developers turn to Redux as their go-to solution when working with a large, complex application that needs a global state. It acts as a centralized repository for the state that must be used throughout the entire program, with regulations guaranteeing that now the state can only be updated predictably.

There are three fundamental principles of Redux.

According to the redux documentation, you can describe redux in three principles.

  • Read-Only State: Emitting operations is the only way to modify the store
  • Pure functions are used to modify:  The reducer should be constructed as a pure function that updates the store.
  • Single source of truth: Your application’s global state is kept in a single store as an object tree.

Redux needs actions, reducers, and storage as its three primary building blocks to operate. Let’s examine them one by one in more depth.

Actions

Said, an action is a JavaScript object with a type field. An action is something that occurred in the application, and an event is anything that summarizes that event.

A string, such as “todolist,” or any appropriate name for this action, should be entered in the type box. The way we often write this kind of string is “domain/eventName,” in which the first part is the characteristic or class to which this action belongs, and the second section is the individual event. An action object can include fields containing extra details about what occurred. According to tradition, we place the data in a field referred to as the payload.

An example of a common action object might be as follows:

const addtodolistaction = { type: "todolist", payload: "Buy groceries" };

There is another function with the name of Action Creators

Creating and returning an action object is what an action maker does. Usually, we employ these so that we won’t need to type the action object repeatedly:

const addinlist = (text) => {
  return { type: " todolist", payload: text };
};

Reducer

Using the formula (state, action) => newState, a reducer is a function that takes two arguments: the present state and an action object. It then decides how to change the state as needed. A reducer can be compared to an event listener that responds to events according to the type of action/event sent.

Reducer must follow some specific rules that it must follow

  • They should compute the new state value only using the state and action parameters
  • They cannot change the current condition in any way. They must instead perform immutable updates by producing copies of the current state and changing the values of those copies
  • They can’t perform asynchronous logic, come up with random numbers, or have other “side effects”

The actions which each reducer should take are illustrated by the following brief example of a reducer:

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  // Check to see if the reducer cares about this action
  if (action.type === 'counter/increment') {
    // If so, make a copy of `state.`
    return {
      value.state,
      // and update the copy with the new value
      value: state.value + 1
    }
  }
  // otherwise, return the existing state unchanged
  return state
}

Any logic, including if/else, switches, loops, and so on, can be used inside reducers to determine the new state.

Store

A store object contains the state of the current Redux application. The store’s getState method, which returns the value of the current state, is called when a reducer is sent in to build it.

import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({ reducer: counterReducer });
console.log(store.getState());
// {value: 0}

Apart from this, two other things should be kept in mind.

Dispatch

A method named dispatch exists in the Redux store. Only calling the store. Dispatch () with an action object as a parameter will update the state. We can call getState() to get the most recent value once the store runs its reducer function and saves the new state value there:

store.dispatch({ type: 'counter/increment' })
console.log(store.getState())
// {value: 1}

Dispatching actions to the application might be viewed as “trigger[ing] an event.” We need to let the shop know about something that happened. Reducers provide the same function as event listeners, updating the state whenever they detect an activity that interests them. To dispatch the appropriate action, we frequently call action creators:

const increment = () => {
  return {
    type: 'counter/increment'
  }
}
store.dispatch(increment())
console.log(store.getState())
// {value: 2}

Selectors

The ability to retrieve particular pieces of data from a store state value is known as a selector. Because more and more of an application’s components must read the same data, doing so can assist prevent repetitive logic:

const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2

When to use Redux?

When to use Redux?

Redux aids in shared state management but comes with compromises, like any technology. More ideas need to be mastered, and more code must be developed. Additionally, it requires you to adhere to specific limitations and adds additional indirection to your code. A trade-off must be made between immediate and long-term production.

Redux is more beneficial when

  • There are numerous areas in the app where a lot of your application states are required.
  • Over time, there have been several updates to the app’s status.
  • It may take some time for the logic to update that state.
  • Many individuals may work on the program with a medium to the large codebase.

Now that you know what redux is, let’s move on to the react hooks.

State Management With React Hooks

Since the React library’s inception, hooks have been among the most notable features added. To functional components, hooks added “state.” Functional components can now independently construct and manage local states like class components.

React hooks, such as useState, useEffect, and useReducer, should be known to anyone already in the framework.

This section will review how good standalone state management using React Hooks can be without involving other libraries.

What are React Hooks?

What are React Hooks?

These are the functions that allow you to access React features and states. You can use React features without creating a class because hooks don’t function inside classes. Because hooks are backwards-compatible, no breaking changes are maintained. Several built-in hooks are available in React, including useState, useEffect, and useReducer. Also possible are personalized hooks.

Rules for React Hooks

  • Calling a hook at the top level merely indicates that you must do so inside a loop, nested function, or set of conditions.
  • Hooks are the only name used for React function components.

Now that you’re clear about React Hooks. Let’s see React Hooks with the example.

Basic Hooks

Following are some of the basic React Hooks are as follows :

  • useState
  • useEffect
  • useContext

useState

const [state, setState] = useState(initialState);

This returns both a stateful value and an update function are returned. During the Initial render, the value supplied as the initial argument is identical to the state (state) returned (initialState). Updates to the state are made via the setState function. The component is re-rendered and accepted when a new state value is provided.

setState(newState);

The initial value useState returns in subsequent re-renders will always represent the state that has been updated most recently.

Example: Declaring a State Variable in class and initialize count state with 0 by setting this. state to {count:0}.

class Example extends React. Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
}

You can get more information about this state in react documentation.

useEffect

useEffect(didUpdate);

There is imperative code in this function that might be useful. Within the main section of a function component, subscribers, calendars, monitoring, and other side effects are prohibited. UI inconsistencies and perplexing problems may result if they are permitted. Using the useEffect hook will help you avoid these problems. Following the commit of the render to the screen, the function supplied for useEffect will be executed. Effects might be considered a transition from the functional world of responding to the imperative world. For a better understanding, let’s examine the example.

useEffect(() => {
    // set our variable to true
    let APISubscribed = true;
    axios.get(API).then((response) => {
        if (APISubscribed) {
            // handle success
        }
    });
    return () => {
        // cancel the subscription
        APISubscribed = false;
    };
}, []);

To handle our success request, the variable APISubscribed in the code above is set to true. While unmounting our component, we changed the value ApiSubscribed to false. Get more information about useEffect in the react documentation.

useContext

const value = useContext(MyContext);

Provides a complement to the existing value for a given context after accepting a perspective object (the result of calling React.createContext). The value prop of the closest MyContext.Provider> above the caller component in the tree determines the current context value. This hook will cause a re-render with the most current context value provided to that MyContext provider whenever the closest MyContext.Provider> above the component updates. The component itself will be the starting point for a re-render, even if an ancestor utilizes React. memo or shouldComponentUpdate. Let’s use an example to grasp this better.

const newContext = React.createContext({ color: 'black' });
const value = useContext(newContext);
console.log(value); // this will return { color: 'black' }

You can get more information on react documentation.

Now that you know basic hooks let’s see some additional ones.

LookinG for React developers to hire?

Get in touch to develop highly scalable web app project.

Additional Hooks

The hooks that follow are either variations on the fundamental ones from the section before or are only required in a few edge circumstances. Never feel pressured to memorize them right away.

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

something different from useState. Takes a reducer of the form (state, action) => newState as input and returns the current state along with a dispatch function. (This process is already familiar to you if you are familiar with Redux.)

UseReducer is typically preferred over useState when you have sophisticated state logic involving several sub-values or when the following state depends on the preceding one. You can transmit dispatch down rather than callbacks using useReducer, which enables speed optimization for components that start big changes. Following is the example of the useState section. Let’s rewrite using useReducer.

const initialState = { count: 0 };
function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}
function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      {" "}
      Count: {state.count}{" "}
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>{" "}
      <button onClick={() => dispatch({ type: "increment" })}>+</button>{" "}
    </>
  );
}

You can get more information about this on react documentation.

useCallback

const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

This function yields a callback that has been memoized.

Send a callback inline and a list of dependencies. If one of the dependencies has changed, useCallback will only update the memoized version of the callback it returns. This is helpful to avoid needless renderings when delivering callback to enhanced child components than depend on referenced equality (e.g. shouldComponentUpdate).

UseMemo(() => fn, deps) and useCallback(fn, deps) are equivalents.
Let’s understand it better with an example.

import { useState } from "react";
import ReactDOM from "react-dom/client";
import Todos from "./Todos";

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

  const increment = () => {
    setCount((c) => c + 1);
  };
  const addTodo = () => {
    setTodos((t) => [...t, "New Todo"]);
  };
  return (
    <>
      <Todos todos={todos} addTodo={addTodo} />
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
      </div>
    </>
  );
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo returns a memorized value. It passes a “create” function and an array of dependencies. useMemo will only recompute the memoized value when one of the dependencies has changed. This optimization helps to avoid expensive calculations on every render.

Keep in mind that useMemo’s function executes when rendering is taking place. Avoid taking any actions there that you wouldn’t take while rendering. In useEffect, not useMemo, side effects, for instance, belong. If an array is not provided, every render will produce a new value.

import { useState, useMemo } from "react";
import ReactDOM from "react-dom/client";

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);
  const calculation = useMemo(() => expensiveCalculation(count), [count]);

  const increment = () => {
    setCount((c) => c + 1);
  };
  const addTodo = () => {
    setTodos((t) => [...t, "New Todo"]);
  };

  return (
    <div>
      <div>
        <h2>My Todos</h2>
        {todos.map((todo, index) => {
          return <p key={index}>{todo}</p>;
        })}
        <button onClick={addTodo}>Add Todo</button>
      </div>
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
        <h2>Expensive Calculation</h2>
        {calculation}
      </div>
    </div>
  );
};

const expensiveCalculation = (num) => {
  console.log("Calculating...");
  for (let i = 0; i < 1000000000; i++) {
    num += 1;
  }
  return num;
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

useRef

const refContainer = useRef(initialValue);

The result of using a reference is a modified ref object with the supplied argument initialized to its current attribute (initialValue). During the whole component lifetime, the returned object will remain persistent. To understand it better, let’s take an example:

function TextInputWithFocusButton() {
  const input = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

UseRef can store a dynamic value in its. current property, effectively making it like a “box.” The main use of refs may be as a DOM access method. React will set its. current property to the relevant DOM node each time that node changes if you pass it a ref object with the command.

UseRef(), however, is not only beneficial for the ref attribute. Similar to how you’d utilize instance fields in classes, keeping any mutable value close at hand is advantageous.

UseRef() produces a specific JavaScript object, which is why this works. UseRef() gives users the very same reference object on every render, which is the only distinction between it and manually constructing a “current:…” object.

Remember that useRef doesn’t alert you when the content changes. A re-render is not triggered when the. current property is changed. Use a callback ref instead if you want to run some function when React connects or dissociates a ref to a DOM node.

useImpretativeHandle

useImperativeHandle(ref, createHandle, [deps])

When utilizing ref, useImperativeHandle modifies the instance value exposed to parent components. Always avoid writing imperative code that uses refs. For a better understanding, let’s take the example. ForwardRef should be combined with useImperativeHandle:

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

Here, a parent component that produces FancyInput ref=inputRef /> would be able to call inputRef.current.focus ().

useLayoutEffect

Although it fires synchronously following all DOM updates, the signature is the same as useEffect. Use this to synchronously re-render while reading the layout from the DOM. Before the browser gets a chance to paint, updates planned within useLayoutEffect will be pushed synchronously. When possible, choose the standard useEffect to prevent obstructing visual updates.

useDebugValue

useDebugValue(value)

For customized hooks in React DevTools, useDebugValue can display a label. Taking the custom hook useFriendStatus from “Building Your Hooks” as an illustration:

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
  // ...
  // Show a label in DevTools next to this hook
  // e.g. "FriendStatus: Online"
  useDebugValue(isOnline ? "Online" : "Offline");
  return isOnline;
}

useTransition

const [isPending, startTransition] = useTransition();

this react hook gives back a domain-specific value for the transition’s pending state and a function to initiate it. The specified callback’s updates can be marked as transitions using startTransition:

startTransition(() => {
setCount(count + 1);
})

In the following example, the isPending function indicates when a transition is active to show a pending state.

­ function App() {
  const [isPending, startTransition] = useTransition();
  const [count, setCount] = useState(0);
  function handleClick() {
    startTransition(() => {
      setCount((c) => c + 1);
    });
  }
  return (
    <div>
      {isPending && <Spinner />}
      <button onClick={handleClick}>{count}</button>
    </div>
  );
}

useId

const id = useId();

To prevent hydration mismatches, useId is a hook for creating unique IDs consistent between the server and the client. Please note that userId is not generating keys in a list. Keys should be generated from your data. Let’s take an example of a single Id and multiple Ids

SingleID

function Checkbox() {
  const id = useId();
  return (
    <>
      
      
    </>
  );
}

In this example, the Id is passed directly to the elements which need it. Now let’s move on to the multiple ID.

function NameFields() {
  const id = useId();
  return (
    <div>
      <label htmlFor={id + "-firstName"}>First Name</label>
      <div>
        <input id={id + "-firstName"} type="text" />
      </div>
      <label htmlFor={id + "-lastName"}>Last Name</label>
      <div>
        <input id={id + "-lastName"} type="text" />
      </div>
    </div>
  );
}

In this example, For multiple Ids, a suffix is used with the same Id

Note: A string containing the token is produced by useId. In CSS selectors or APIs like querySelectorAll, this helps to verify that the token is unique, but they do not allow it. To avoid conflicts in multi-root apps, useId allows an identifier prefix. See the ReactDOMServer and hydrateRoot options for configuration.

Library Hooks

Since libraries are rarely utilized in application code, the following hook is offered to library authors to help them fully integrate their works into the React model.

useSyncExternalStore

const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);

It is advised to use the hook useSyncExternalStore to read and subscribe from external data sources in a way appropriate for concurrent rendering capabilities like temporal slicing and selective hydration. This function takes three inputs and returns the value of the store.

Whenever the store changes, a callback you register using the function “subscribe” is called.

  • The function getSnapshot returns the store’s most recent value.
  • Get the server rendering snapshot with the getServerSnapshot method.
  • The simplest straightforward illustration merely subscribes to the entire store:

The  most basic example subscribes to the entire store

const state = useSyncExternalStore(store.subscribe, store.getSnapshot); However, you can also subscribe to a specific field:

const selectedField = useSyncExternalStore(
  store.subscribe,
  () => store.getSnapshot().selectedField
);

It would be best if you gave useSyncExternalStore with the serialized server store value while doing server rendering. To avoid server conflicts, React will utilize this snapshot during hydration:

const selectedField = useSyncExternalStore(
  store.subscribe,
  () => store.getSnapshot().selectedField,
  () => INITIAL_SERVER_SNAPSHOT.selectedField
);

For more information on this library, hook refer to the official react documentation.

useInsertationEffect

useInsertionEffect(didUpdate);
It fires synchronously before DOM modifications and has the same signature as useEffect. Before using useLayoutEffect to read the layout, use this to inject styles into the DOM. This hook has limited access and cannot schedule updates because of its restricted scope.
To get more information, refer to the official react documentation.

When to use React Hooks?

For every app or component, Redux isn’t always necessary. If you’re having trouble coming up with a solid reason to use Redux in an app that only has one view, doesn’t save or load state, and doesn’t use asynchronous I/O. Following are some of the scenarios where you can use react hook.

When your component is:

  • Not using the network.
  • Neither saves nor loads state.
  • Not shared with other non-child components in terms of state.
  • Requires a temporary local component state.

have a unique app Idea?

Hire Certified Developers To Build Robust Feature, Rich App And Websites.

React Hooks vs Redux: Or Both

When it comes to front-end development, React State Management is one of the most difficult concepts to understand. Recognizing the differences and complementarities between Redux and React Hooks is crucial. Techniques like useState, useReducer, and useContext are sufficient to handle most difficulties because react is a very robust and reliable framework. Therefore, unless the fundamentals become insufficient for some reason, it is recommended to stick to them.

Get in Touch!

    Saurabh Barot

    Saurabh Barot is co-founder and Experienced Chief Technology Officer at Aglowid IT Solutions. His deep industry experience in Angular, React, Ruby on Rails, PHP, Mean Stack and just to name few, has helped the company reach to a new level. He believes that technology is all about exploring new possibilities. Whatever a mind can think (exception can't be ruled out), technology brings it into reality.

    Related Posts