Our Blog:

Optimizing React Applications with Hooks


Middle
Andrej Vajagic

Andrej Vajagic

28.07.2023, 15:48

Read time: undefined mins

Optimizing React Applications with Hooks

React has become one of the most popular JavaScript libraries for building user interfaces, and with the introduction of Hooks in React 16.8, developers now have more power to write clean and easy-to-understand functional components. In this blog post, we'll explore how to use React Hooks to optimize your application and improve performance.

Before we go into hooks, lets cover two important concepts first!

  1. The Virtual DOM (VDOM)

    The Virtual DOM (VDOM) is a core concept in React and some other modern JavaScript libraries and frameworks. It's a programming concept where an ideal, or "virtual", representation of a user interface is kept in memory and synced with the "real" DOM by a library such as React DOM.

    The Virtual DOM works as follows:

    • UI Representation in Memory: When a React component's state changes, a new Virtual DOM representation of the user interface is created. This representation is a tree of React elements (plain JavaScript objects) which corresponds to the structure of the actual DOM.
    • Diffing Algorithm: React then compares this newly created Virtual DOM with the previous one using a diffing algorithm. The goal of this comparison is to find out what has changed in the Virtual DOM.
    • Reconciliation: Once React knows what has changed, it updates only those parts of the real DOM to match the new Virtual DOM. This process is known as reconciliation.
  2. When React component re-renders

    • State Changes: Every time the state of a component changes, the component is re-rendered to reflect the new state.
    • Props Changes: If the props passed to a component change, the component will re-render.
    • Context Changes: When a React component is subscribed to a context and the value of that context changes, the component re-renders.
    • Parent Renders: If a parent component re-renders for any reason, it will cause its child components to re-render as well.
    • Force Rerender: If the forceUpdate method is called, it will cause the component to re-render.
    • Hooks Changes: For functional components, certain hooks (useState, useReducer, useEffect, useContext) can trigger a re-render when their values or dependencies change.

    It's important to note that re-rendering a component doesn't always lead to a DOM update. React uses a reconciliation process to diff the new virtual DOM with the old one and only makes updates to the real DOM where necessary. This is a key part of React's efficiency.

What are React Hooks?

Before we dive into optimization, let's briefly talk about what React Hooks are.

React Hooks are a set of APIs introduced in React 16.8 that allow you to use state and other React features without writing a class. There are a variety of hooks available, including:

  • useState: This is a Hook that lets you add React state to functional components.
  • useEffect: This Hook lets you perform side effects in your components.
  • useContext: React's Context API can be consumed using this Hook.
  • useReducer: An alternative to useState that accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method.
  • useMemo and useCallback: These Hooks return a memoized version of the function that only changes if one of the dependencies has changed. These are just a few examples. There are many other built-in Hooks, and you can even create your own custom Hooks.

Optimize with React Hooks

React is fast, but sometimes you need to optimize your application for better performance. React Hooks can help with this in several ways.

1. Prevent Unnecessary Renders with useMemo and useCallback and React.memo

Let's dive deeper into useMemo, useCallback, and React.memo to understand how they work and why they are useful.

useMemo

useMemo is a Hook that returns a memoized value. This is particularly useful when you have computationally expensive calculations. Instead of performing the calculation every time the component re-renders, useMemo allows you to perform the calculation only when one of its dependencies changes.

Here's the basic syntax of useMemo:

code)

The first argument to useMemo is a function that computes the value you want to memoize. The second argument is an array of dependencies that will trigger a re-computation when they change. In the above example, computeExpensiveValue(a, b) will only be executed when either a or b changes.

When to use useMemo

You should consider using useMemo when you need to optimize expensive calculations. If you have a function that processes large arrays or objects and this function is being called on every render, it can lead to performance issues.

useMemo can help by ensuring that this function is only called when the dependencies change, not on every render.

Here is an example: code)

In this example, the sortedList is only re-computed when list changes, not on every render. Without useMemo, the sort function would be called on every render, even if list didn't change, leading to unnecessary computations.

useCallback

useCallback is similar to useMemo, but instead of memoizing a value, it memoizes a function. This can be useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders.

The useCallback Hook looks like this:

code)

The first argument is the function you want to memoize, and the second argument is an array of dependencies. The memoizedCallback will remain the same across renders unless a or b change.

When to use useCallback:

useCallback is particularly useful when passing callback functions to child components that are wrapped in React.memo. React.memo will do a shallow comparison of the props, and if you're passing a function that is being re-created on every render, it will cause the memoized child component to re-render unnecessarily.

useCallback ensures the function identity is maintained between re-renders unless one of its dependencies changes.

Here's an example:

code)

In this example, increment is only re-created when one of its dependencies changes (in this case, there are no dependencies, so it's only created once). If we didn't use useCallback, increment would be a new function on each render, causing ChildComponent to re-render unnecessarily.

React.memo

React.memo is a higher-order component that you can use to wrap your functional components to prevent unnecessary renders. If your component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.

Here's a basic example: code)

In this example, MyComponent will only re-render if its props have changed. If the props are the same, React will skip rendering the component and use the previous render.

It's important to note that React.memo only checks for prop changes. If your component's behavior is tied to the current state or context, React.memo probably isn't necessary.

When to use React.memo:

You should consider using React.memo when you have a functional component that:

  • Renders often with the same props.

  • The render operation is quite heavy.

  • Doesn't depend on global state or context.

    Here's an example:

code)

In this example, typing into the input field causes App to re-render, but the List component is not re-rendered unless the items prop changes. If we didn't wrap List with React.memo, it would re-render every time App re-renders, even though its props haven't changed.

Note: It's important to be aware that while React.memo can optimize certain scenarios, it also introduces an overhead as React needs to perform a shallow comparison of the props. Therefore, it's recommended to use it judiciously, and when there is a demonstrated performance issue.

2. Manage Complex State with useReducer

For complex state logic, useReducer is a great Hook. It's an alternative to useState and is usually preferable when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.

Here's the basic structure of a reducer function:

code)

The useReducer hook is used like this:

code)

Let's see it in action with our form example:

code)

In this example, state holds the state of the entire form and dispatch is a function to update the state. handleChange dispatches an action with the fields name and value, and the reducer function updates the appropriate field in the state.

This is a fairly simple example, but useReducer can be used to manage much more complex state interactions, including handling asynchronous actions. It's especially useful when different actions can affect multiple pieces of state, as it allows you to group these updates together to ensure consistency.

3. Avoid Prop Drilling with useContext

useContext is a Hook that lets you tap into the context API, which can be particularly useful when you have a lot of nested components and you want to avoid prop drilling.

code)

useContext and useReducer can work together

useContext and useReducer can work together to solve this issue by providing a centralized store for state and actions, eliminating the need to pass data and callback props through every level of the component tree.

Here's an example that demonstrates this:

  1. Create the context:

code)

  1. Define the reducer:

code)

  1. Create the Provider:

code)

  1. Create components:

code)

  1. Use the Provider to wrap your app or component tree:

code)

In the above example, neither CounterDisplay nor CounterControls receive props from their parent. They get the state and dispatch function directly from the context. This way, you can deeply nest these components, move them around, or introduce new components in between without worrying about passing the state and dispatch function down through props.

Share:

Accelerating Digital Success. Experience the future of web development – faster, smarter, better. Lets innovate together.

©2024 Dreit Technologies | All rights reserved

Contact

  • +38 64 577 3034
  • Serbia
  • Marka Pericina Kamenjara 11A, Ruma
  • Contact us