Options for optimizing caching in React

6 min read 1716

Options Caching React

As one of the most popular frontend libraries for creating performant web applications, React follows a component-based approach where each component has its own state and logic.

The biggest challenge regarding React is avoiding unnecessary renders, which can cause major performance issues, especially in larger applications. In this article, we’ll go over a few different ways to optimize the performance of React applications through different methods for caching.

React Hooks for memoization

Memoization is a feature provided by React itself. As we know, React creates new references each time it re-renders. If your component has an extensive calculation, it will be calculated on each re-render, even if the output does not change.

To keep the CPU loads minimal by avoiding unnecessary loads, React provides two Hooks that help in memoization. The Hooks follow a process in which the results are cached in memory and returned without re-computation when we get the same input. In the case of different inputs, the cache gets invalidated.

useMemo()

useMemo() is a Hook provided by React for memoization that helps in keeping the cached values for the same values provided to it. It tracks the input and returns the previously executed result.

Let’s see an example. Suppose that we have to add two huge numbers in a component with the following function:

const addTwoHugeNumbers=(a,b)=>{
return a+b
}

The written function above is heavy on the CPU, and therefore should only be computed when the values of a and b are changed. However, by default, it will run on every re-render.

With useMemo(), we can store the result for the specific values, meaning the function will not compute and we’ll get the previously calculated result directly:

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

The value is stored in memoizedValue. We have passed the dependency array to useMemo, which tells it when to run again. In our case, it will run when either of the values change.

UseCallback()

With useCallback(), we also get the ability to memoize, but it works in a different way. useCallback() doesn’t memoize the value, but instead it memoizes the callback function provided to it. Let’s see a small example:

We made a custom demo for .
No really. Click here to check it out.

const increment = (() => {
  setCount(count + 1);
});

With useCallback(), the function above looks like the code below:

const increment = useCallback(() => {
  setCount(count + 1);
}, [count]);

useCallback() will memoize the increment function, running only when the dependency given changes. It does not track input or the value returned by the function.

Lazy loading React components

Lazy loading in React renders the necessary components upfront and delays loading the unimportant components until after.

Especially in larger applications, this approach is highly recommended to improve performance. In React, we have built-in options to lazy load components.

We’ve created a component named </Artists> and we want it to lazy load, we can do it as follows:

import { lazy } from 'react';

First, we import lazy from react and use it like below:

const Artists = React.lazy(() => import('./Artists'));

function App() {
  return (
    <div>
      <Artists />
    </div>
  );
}

useRef()

We know that whenever we use useState() in a component, it causes a re-render in the component when the state changes. To track the state without causing re-render, React has introduced the useRef() Hook.

There are some scenarios where useState() might not be the right solution for your application. useRef() is ideal for situations where we need a state that does not cause a re-render and has no contribution in the visible information rendered by a component. For example, you can use it to count renders:

function App() {
    const [foo, setFoo] = React.useState(false)
    const counter = React.useRef(0)
    console.log(counter.current++)
    return (
      <button onClick={() => setFoo(f => !f)} > Click </button>
     )
}

ReactDOM.render(<React.StrictMode><App /></React.StrictMode>, document.getElementById('mydiv'))

In the code above, we have a simple toggler that re-renders a component. counter is a mutable ref that persists its value. We can do the same thing with useState(), but it will cause two renders for each toggle.

Redux cached selectors

Selectors are simply functions that are used to select data from a larger pool of data. In React, selectors are widely used to get values from a Redux store. Selectors are extremely useful and powerful, but they do come with their own disadvantages.

In React Redux, we have the useSelector() Hook that is used to get the state from the store. The issue with useSelector() is that it runs every time a component renders. useSelector() might be ideal for some cases, but most of the times, the data returned by the selectors does not change which makes the computation unnecessary.

Let us see an example of this:

import React, {useEffect,useState} from 'react'
import {useSelector, useDispatch} from 'react-redux'
import {getPosts} from './postActions'

export const List=()=>{
  Const [toggle, setToggle]=useState(false)
  const myPosts=useSelector(state=>state.posts)
  const dispatch=useDispatch()


  return(
    <div>
    {myPosts.map(post=><p>{posts}<p/>)}
    <button type="button" onClick={()=>{setToggle(!toggle)}} >Click Me!</button>
    <div/>
  )
}

In the above code, we are changing the toggle state and the component will render every time we do it. The useSelector() Hook will also run, even though the posts are not changing in our Redux store.

To solve this problem, we’ll cache the results of a selector function. Even though there are no built-in React solutions for that, we have many third-party libraries that allow us to create cached selectors. Let’s use Reselect, which is famous solution for caching selectors.

Reselect

Reselect is a popular library for creating memoized selectors. You can install it in your project with the following command:

yarn add reselect

We can use Reselect as follows:

import { createSelector } from 'reselect' 
import React, {useEffect,useState} from 'react'
import {useSelector, useDispatch} from 'react-redux'
import {getPosts} from './postActions'

export const List=()=>{
  Const [toggle, setToggle]=useState(false)
  const myPosts = createSelector(state=>state.posts)
  const dispatch=useDispatch()


  return(
  <div>
  {myPosts.map(post=><p>{posts}<p/>)}
  <button type="button" onClick={()=>{setToggle(!toggle)}} >Click Me!</button>
  <div/>
  )
}

In the code above, we imported createSelector from Reselect, which takes a selector and returns a memoized version of it. With the memoized version, the component will not compute the value of selectors even after thousands of re-renders, unless the value of postReducer changes. Reselect’s createSelector proves to be an excellent solution for solving performance issues in larger applications.

Optimizing API calls with React Query

React handles async operations in its own way, which is sometimes an issue for developers. The usual pattern for async operations is fetching the server data in the useEffect Hook, which runs on each render and fetches new data every time, even if there is no new data on the server.

On the other hand, React Query caches the data and returns it first before making a call, but if the new data returned by the server is the same as the previous data, React Query will not re-render the component. We can use React Query as follows:

import React from 'react'
import {useQuery} from 'react-query'
import axios from 'axios'

async function fetchPosts(){
    const {data} = await axios.get('https://jsonplaceholder.typicode.com/posts')    
    return data
}

function Posts(){
    const {data, error, isError, isLoading } = useQuery('posts', fetchPosts) 
    // first argument is a string to cache and track the query result
    if(isLoading){
        return <div>Loading...</div>
    }
    if(isError){
        return <div>Error! {error.message}</div>
    }

    return(
        <div className='container'>
        <h1>Posts</h1>
        {
            data.map((post, index) => {
                return <li key={index}>{post.title}</li>
            })
        }

        </div>
    )
}

export default Posts

React fragments

If you’re a React developer, you’ve probably encountered an error that says to wrap the component with a parent div. If the extra div isn’t needed in your component, it doesn’t make sense to add it. For example, if you have a thousand components in your React application, you will have a thousand extra divs, which can be heavy on the DOM. To avoid this, React gives you the option to use fragments:

const Message = () => {
  return (
    <React.Fragment>
      <p>Hello<p/>
      <p>I have message for you<p/>
    </React.Fragment>
  );
};

The code snippet below is exactly the same as the code above, using <> as a shortcut for React.Fragment:

const Message = () => {
  return (
    <>
      <p>Hello<p/>
      <p>I have message for you<p/>
    </>
  );
};

With either approach, you avoid adding an extra <div>, resulting in less DOM markup, increasing render performance, and reducing memory overhead.

React virtual lists

Frequently, we need to render large lists on the browser; doing so is extensive for the browser because it has to create new nodes and paint them all on the screen.

To make the process efficient in React, we have the option to use virtual lists. A virtual list renders only a handful of items as required, simply replacing them as a user dynamically scrolls through the items.

Renders are faster than changing the DOM, so you can render thousands of list items with fast performance using a virtual list. React-virtualized is an excellent library that has components for rendering virtual lists.

Functional components

React started from class base components, however, it is now recommended to use functional components due to their lightweight nature. Functional components are basically functions that are much faster to create, and they are easier to minify, decreasing the bundle size.

Conclusion

In this tutorial, we covered several different solutions for optimizing cache management in React applications, like memorization, cached selectors, lazy loading, React fragments, virtual lists, and functional components. Each of these methods can improve your application by reducing the number of unnecessary component renders, reducing overhead and increasing speed.

The right solution will depend on the needs of your individual project, but hopefully, this article helped introduce you to the options available.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

One Reply to “Options for optimizing caching in React”

Leave a Reply