After working professionally with React for some time now, I thought it might be helpful to share some of my experience with React, especially the Re-rendering process, in the form of a Definitive Guide.

In this article, I’d want to dive deeper into this topic and provide a more straightforward way of understanding how the Re-rendering process works in React using and how it can be optimized for better performance.

Glossary

  • Why do components re-render?
  • What is Reconciliation in React?
  • How do state changes or props changes affect the re-rendering process?
  • How do hooks work, and what’s their impact on the re-rendering process?
  • Best Practices for performance enhancements

What is re-rendering?

React Re-Rendering Diagram

A second or subsequent render to update a component is called re-rendering.

Why do components re-render?

State Updates

  1. Using the useState hook – It’s the most common way to manage component state in React; it returns an array with two elements – the current value and a function to update the state.
  2. Using the useReducer hook: It’s similar to useState in the sense that it’s used to manage the component’s state, but it’s more useful when handling a complex state.
  3. Using the Context API (Accessed via the useContext hook), which is used to manage state shared across components using a context provider that can be created via createContext.
  4. Using Redux, a popular library that can manage React applications’ global state.

Note:

We have not mentioned hooks, such as useEffect, and that’s because useEffect‘s primary task is to handle side-effects such as data fetching, subscriptions, or manually changing the DOM after a component re-render.

We have an article where we go into more depth about useEffect – I’d recommend that you can check that out here before proceeding with the article, as it will give you a better understanding of side-effects and dependencies.

Props Update

  • When a component’s props change, React will compare the new and previous props and re-render the component in case there’s a mismatch.
  • Update in the parent component: When there’s a change in the parent component state, all its children will re-render as well.

Where do Re-renders occur?

Re-rendering can occur either on the server side or the client side.

Server-Side

Re-renders solely occur when a new page is requested; this will result in the server generating a new HTML document that will be sent to the browser.

Client-Side

Re-renders can occur through the use of client-side routing and they allow us to only update specific a page partially when navigating between different sections of the application.

What are necessary and unnecessary re-renders?

Necessary re-renders

Let’s say, for example, that a user types in an input field. In this scneario the component that manages the input’s value state shall update itself on every key entered in the input field.

Unnecessary re-renders

Let’s now suppose that a user types in the input field, and the entire page will re-render on every key entered in the input field. In this case, the process will affect the app’s performance and cause the user’s machine’s battery loss as a side-effect.

What is Reconciliation?

When a component’s props or state change, React will check for differences within the Virtual DOM and the actual DOm, and if there are any, it will update the component’s DOM and display the new state.

If you are not yet familiar with the Reconciliation process and how it works, we have written an article explicitly tackling that – you can check it out here.

Triggers for Re-renders

Props

In the following example, we have the main App component, which renders the CounterOutside component, which, in turn, renders the CounterInside component.

We are passing props from CounterOutside component to CounterInside component. We are incrementing the counter with a button click using the setter provided by the useState hook.

import { useState, useEffect, useCallback } from 'react';

const CounterInside = ({ parentStyle, parentCount, count }) => {
  useEffect(() => {
    setTimeout(() => {
      setStyle(parentStyle);
    }, 100);
  }, []);

  return (
    <div className="container">
      <div>
        <b className="text">
          CounterInsideComponent
        </b>
        <p className="text">
          ChildComponent Re-render - {parentCount}
        </p>
        <p className="text">
          {count}
        </p>
      </div>
    </div>
  );
};

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

  const incrementHandler = useCallback(() => {
    setCount((prevState) => prevState + 1);
  }, []);

  return (
    <div className="container">
      <div>
        <b className="text">
          CounterOutsideComponent
        </b>
        <p className="text">
          ParentComponent Re-render - {count}
        </p>
        <button
          className="btn"
          onClick={incrementHandler}
        >
          Increment
        </button>
        <CounterInside
          parentCount={count}
          parentStyle={temporaryStyle}
        />
      </div>
    </div>
  );
};

const App = () => {
  return (
    <div className="AppMain">
      <h1>Hey!</h1>
      <CounterOutside />
    </div>
  );
}

export default App;

From the example above, we get to know that on state change of CounterOutside component, the parent App component does not re-render, but its child CounterInside does.

Re-render is a snapshot, like a photo taken from the camera; it checks the difference between an older snapshot and a newer snapshot which is generated after the state change.

The important point to note is that the entire app and any parent components do not re-render whenever a state variable changes. It’s only when a component re-renders that it also re-renders all its children.

Parent State Changes

The code below is similar to what we’ve seen above.

In this example, we have the App component, which renders CounterOutside component, which, in turn, renders two components:

  • CounterInside1 with props
  • CounterInside2 component without props

We are incrementing the counter on click of the button using the useState hook’s setter.

import { useState, useEffect } from 'react';

const CounterInside1 = ({ parentStyle, count, parentCount }) => {
  useEffect(() => {
    setTimeout(() => {
      setStyle(parentStyle);
    }, 100);
  }, []);

  return (
    <div className="container">
      <div>
        <b className="text">
          CounterInside1Component
        </b>
        <p className="text">
          ChildComponent Re-render - {parentCount}
        </p>
        <p className="text">
          {count}
        </p>
      </div>
    </div>
  );
};

const CounterInside2 = ({ parentStyle }) => {
  useEffect(() => {
    setTimeout(() => {
      setStyle(parentStyle);
    }, 100);
  }, []);

  return (
    <div className="container">
      <div>
        <b className="text">
          CounterInside2Component
        </b>
      </div>
    </div>
  );
};

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

  return (
    <div className="container">
      <div>
        <b className="text">CounterOutsideComponent</b>
        <p className="text">ParentComponent Re-render- {count}</p>
        <button
          className="btn"
          onClick={incrementHandler}
        >
          Increment
        </button>
        {/* passing props to child component */}
        <CounterInside1
          parentCount={count}
          parentStyle={temporaryStyle}
        />
        {/* not passing props to child component */}
        <CounterInside2 />
      </div>
    </div>
  );
};

const App = () => {
  return (
    <div className="AppMain">
      <h1>Hi, again!</h1>
      <CounterOutside />
    </div>
  );
}

export default App;

From the prior example, we get to know that once the state of the CounterOutside component changes, its children CounterInsideComponent1 and CounterInsideComponent2 will be re-rendered.

It’s important to point out that Child components re-render on state change of parent component irrespective of whether the props are being passed.

It is difficult for React to know whether CounterInsideComponent2 does directly or indirectly depend on the parent’s state for count or not.

Re-rendering inside the useContext hook

Let’s see how re-rendering works in case of context.

In this example, SenderComponent and ReceiverComponent are wrapped inside the ContextProvider component.

import { useState, createContext, useContext } from "react";

const initialState = {};
const CounterContext = createContext(initialState);

const ContextProvider = (props) => {
  const [countContext, setCountContext] = useState(0);
  const [styleContext, setStyleContext] = useState({});

  return (
    <CounterContext.Provider
      value={{
        countContext,
        setCountContext,
        styleContext,
        setStyleContext,
      }}
    >
      {props.children}
    </CounterContext.Provider>
  );
};

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

  const sender = useContext(CounterContext);

  const incrementHandler = () => {
    setCount((prevState) => prevState + 1));
    sender.setCountContext(count);
  };

  return (
    <div className="container">
      <div>
        <b className="text">
          SenderComponent
        </b>
        <button
          className="btn"
          onClick={incrementHandler}
        >
          Increment
        </button>
        <div className="colContainer">
          <CounterInside1 />
        </div>
      </div>
    </div>
  );
};

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

  const sender = useContext(CounterContext);
  const receiver = useContext(CounterContext);

  return (
    <div>
      <div>
        <b className="text">
          ReceiverComponent
        </b>
        <p className="text">
          ParentComponent Re-render - {receiver.countContext}
        </p>
        <div className="colContainer">
          <CounterInside3
            parentCount={count}
            parentStyle={temporaryStyle}
          />
        </div>
      </div>
    </div>
  );
};

const App = () => {
  return (
    <ContextProvider>
      <div className="AppMain">
        <b>ContextProvider</b>
        <div className="flexCol">
          <CounterOutside1 />
          <CounterOutside2 />
        </div>
      </div>
    </ContextProvider>
  );
}

export default App;

Looking at the example above, we get to know that the SenderComponent components forwards data using context and ReceiverComponent, as well as its child(ren) will be re-rendered.

The important point to note is that a Component consuming the context and its children will be re-render after any context updates.

In the above scenario, the following components will re-render:

  • The Receiver component: This component will be re-rendered since it’s consuming the context and will receive the updated context value from the provider.
  • The Receiver component’s children: As we see in the above examples, the state change of the parent component results in the re-rendering of its children.

In the above scenario, the following components will not re-render:

  • The Sender component: This component is updating the state of the context provider component, but is not consuming the context, thus, it won’t re-render.
  • The Parent component of both Sender and Receiver components: They’re not consuming the state so they won’t be affected by the any context update.
  • The App component and its children: Despite being wrapped by the context provider, they are not consuming the context.

When the context provider component updates its state, it updates the context value, and that causes all the components that are consuming the context to re-render.

Re-rendering in other hooks?

  • useRef: It allows us to add a reference to a DOM node or value that persists across re-render. It doesn’t trigger a re-render.
  • useReducer: It works similarly to useState hook; it’s used to manage complex state logic in applications using a reducer function and once its value changes, the component will re-render.

Performance Optimization Techniques

One of the key ways to improve the performance of a React application is to minimize any unnecessary re-renders. There can be several techniques that can be used to achieve this, such as the usage of the useMemo, useCallback, and useEffect hooks, as well as the memo HoC.

Optimization using memo

To stop some of the re-renders and increase the speed of the application, we make use of some optimization techniques apart from hooks is memo.

In this example, we have the App component, which renders the CounterOutside component, which, in turn, renders two components. But now both the child components are wrapped within memo.

import { useState, useEffect, memo } from 'react';

const CounterInside1 = ({ parentStyle, count, parentCount }) => {
  useEffect(() => {
    setTimeout(() => {
      setStyle(parentStyle);
    }, 100);
  }, []);

  return (
    <div className="container">
      <div>
        <b className="text">
          CounterInside1Component
        </b>
        <p className="text">
          ChildComponent Re-render - {parentCount}
        </p>
        <p className="text">
          {props.count}
        </p>
      </div>
    </div>
  );
};

const MemoizedCounterInside1 = memo(CounterInside1);

const CounterInside2 = ({ parentStyle }) => {
  useEffect(() => {
    setTimeout(() => {
      setStyle(parentStyle);
    }, 100);
  }, []);

  return (
    <div className="container">
      <div>
        <b className="text">
          CounterInside2Component
        </b>
      </div>
    </div>
  );
};

const MemoizedCounterInside2 = memo(CounterInside2);

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

  return (
    <div className="container">
      <div>
        <b className="text">
          CounterOutsideComponent
        </b>
        <p className="text">
          ParentComponent Re-render - {count}
        </p>
        <button
          className="btn"
          onClick={incrementHandler}
        >
          Increment
        </button>
        {/* passing props to child component */}
        <CounterInside1
          parentCount={count}
          parentStyle={temporaryStyle}
        />
        {/* not passing props to child component */}
        <CounterInside2 />
      </div>
    </div>
  );
};

const App = () => {
  return (
    <div className="AppMain">
      <h1>Here we are again</h1>
      <CounterOutside />
    </div>
  );
}

export default App;

From the above example, we get to know that on state change of parent CounterOutside component, child CounterInsideComponent1 is re-rendered but CounterInsideComponent2 is not get re-rendered.

The critical point to note is that wrapping components inside memo tells React to re-render the respective components if their props change.

React uses the older snapshot, and if none of the component’s props change, it again uses the older snapshot.

In the above example, you might argue: “Why not always use memo it to stop unnecessary re-rendering?”

Let’s suppose that a component has a large number of props coming from its parent component – In that case, it’s slower to check whether there is any change in props of the component compared to an older snapshot than a quick re-render.

The memo HoC is not the best solution when used for every single component; it should only be used when it makes a noticeable difference in performance (Hundreds of miliseconds).

Optimization using useMemo and useCallback

While those hooks might be a nice syntax sugar for people coming from Vue, they are mainly used for optimizing the performance of React applications.

Both useMemo and useCallback hooks receive a function as their first argument and a dependencies array as the second argument. The hook will return a new value only when one of the dependency’s value change.

The useMemo hook is used to memoize the value, which is expensive to compute; the value will only be recomputed when one of its dependencies changes.

The useCallback hook is used to memoize the function, which is costly to compute; the will function only be recreated when one of its dependencies changes.

The useMemo the hook will call the function in its first argument and return the function’s result or value, while the useCallback hook will return the function without calling it.

Let’s take a look at a simple Search Bar example where we can apply the concepts we’ve learned about useMemo and useCallback, while also making use of API calls.

const SearchBar = () => {
  const [apiResponse, setApiResponse] = useState();
  const [inputData, setInputData] = useState("");
  const [visible, setVisible] = useState(true);

  const items = useMemo(() => {
    if (!inputData) {
      return apiResponse?.data;
    }

    return apiResponse
      ? apiResponse.data.filter((val) =>
          val.title.toLowerCase().includes(inputData.toLowerCase())
        )
      : "";
  }, [inputData]);

  const inputDataHandler = useCallback((e) => {
    setInputData(e.target.value);
  }, []);

  useEffect(() => {
    axios
      .get("https://fakestoreapi.com/products")
      .then((res) => {
        setApiResponse(res);
        console.log(res);
      })
      .catch((err) => {
        console.log(err);
      });
  }, []);

  return (
    <div className="main">
      <input
        onClick={() => {
          setVisible(true);
        }}
        placeholder="Search product"
        className="inp"
        value={inputData}
        onChange={inputDataHandler}
      />
        <div
          onClick={() => {
            setVisible(true);
          }}
          className="list"
        >
          {items?.map((value) => (
            <p>{value.title}</p>
          )}
        </div>
    </div>
  );
};

export default SearchBar;

In case of the useMemo hook, the value of items is computed after doing expensive operation code, hence here, we use the useMemo hook – we put all the costly operation codes inside useMemo’s first argument function and return the value of the expensive operation code.

This return value is stored in items variable. This value is recomputed only when changing its dependency.

In case of the useCallback hook, inputDataHandler is an expensive function. We put inputDataHandler code inside useCallback hook’s first argument’s function. This function is recomputed only when changing its dependency.

Optimizing with useEffect

The useEffect hook is used to handle side-effects such as fetching data, updating the DOM, or subscribing to a service. It might also be used to optimize the performance of an application.

  • We can pass the dependency array; the callback will run only when there is a change in its dependencies; if an empty array is passed, the callback will be called only once – when the component is mounted.
  • We can use the cleanup function right before the component is unmounted; this function is usually used to unsubscribe from a service, remove or stop a timer, or deallocate the usage of other resources.

Conclusion

In conclusion, Re-rendering is an essential part of React – understanding how it works and how we can go about optimizing it can help improve our React applications’ performance.

I hope you’ve enjoyed the read! Feel free to leave any feedback in regards to this article below!

Cheers!

👋 Hey, I'm Vlad Mihet
I'm Vlad Mihet, a blogger & Full-Stack Engineer who loves teaching others and helping small businesses develop and improve their technical solutions & digital presence.

💬 Leave a comment

Your email address will not be published. Required fields are marked *

We will never share your email with anyone else.