Some concepts that I don’t deeply understand that harm my React App performance

Some concepts that I don’t deeply understand that harm my React App performance

Hi, it’s such so shame that even though I’ve been working with ReactJS for years but still don’t deeply understand the basic concepts of ReactJS. It leads to some misunderstanding as well as immature optimizations (which I thought would make my app run 10 times faster but actually made things worse; immature optimizations and incorrect assumptions are extremely dangerous; don't overuse anything if you don't thoroughly understand what you're doing, especially with advanced concepts). Okay, that’s done with the introduction part. Let’s dive into the content of this blog today.

The birth of custom hooks was an evolution for React developers. It introduced a new way of separating our application's logic and UI code when we develop web applications using ReactJS. When it first came to me, I was like, "Oh, finally, React makes something that I could use to get rid of a super giant React file with a lot of logic code messed up with my UI code—files that I won’t want to go back and read or refactor later.” But it comes to my first mistake (or maybe because I was too careless about the harm of overusing custom hooks).

I always try to follow the DIY (Don't Repeat Yourself) principle, so whenever I see something that could be called in more than one place, it could be a function or a piece of code to handle the logic for something or some functions related to the same domain (for example, a function to handle logic related to a tweet in a social app -- create a tweet, update a tweet, go around with some data transformation related to a tweet using data given by a global store, etc.). At first, they were so perfect, I can see all the code logic that related to the same domain (or feature) in 1 file and I can get that function from other files in just one line of code. But I missed an important key point about Custom Hooks, it is: “When the custom hook’s state changes, the component which is calling that custom hook re-renders as well

Consider a simple example below:

For example, we have a custom hook called useLocalStorage, I won’t write down the detail of the implementation of that hook here because it would make this article a little bit longer. You can find the detail here: Link

Then, we create a component, let’s call it a ‘slow component’

tsx
// SlowComponent.tsx
import React, { useEffect } from "react";

const SlowComponent = () => {
  let now = performance.now();
  while (performance.now() - now < 500) {
    // do nothing in 500ms
  }

  useEffect(() => {
    console.log("Super slow component re-rendered");
  });

  return <p>Hello, I'm a super slow component </p>;
};

export default SlowComponent;
// SlowComponent.tsx
import React, { useEffect } from "react";

const SlowComponent = () => {
  let now = performance.now();
  while (performance.now() - now < 500) {
    // do nothing in 500ms
  }

  useEffect(() => {
    console.log("Super slow component re-rendered");
  });

  return <p>Hello, I'm a super slow component </p>;
};

export default SlowComponent;

Now, go to our App.tsx file and consider what will happen if we write the code like this

tsx
import React from "react";
import SlowComponent from "./SlowComponent";
import "./styles.css";
import { useLocalStorage } from "./useLocalStorage";

export default function App() {
  const [testState, setTestState] = useLocalStorage("testState", "");

  const onChangeTestState = (e: React.ChangeEvent<HTMLInputElement>) => {
    setTestState(e.target.value);
  };

  return (
    <div className="App">
      <p>{testState}</p>
      <input onChange={onChangeTestState}></input>
      <SlowComponent />
    </div>
  );
}
import React from "react";
import SlowComponent from "./SlowComponent";
import "./styles.css";
import { useLocalStorage } from "./useLocalStorage";

export default function App() {
  const [testState, setTestState] = useLocalStorage("testState", "");

  const onChangeTestState = (e: React.ChangeEvent<HTMLInputElement>) => {
    setTestState(e.target.value);
  };

  return (
    <div className="App">
      <p>{testState}</p>
      <input onChange={onChangeTestState}></input>
      <SlowComponent />
    </div>
  );
}

The problem comes when we try to type in the input very fast, we will see the “lagging” of our app, even though it’s a very small app. Go to the console and see that each time we type into the input, the SlowComponent re-renders as well which causes the UI to lag because the SlowComponent takes time to finish its work. It’s because when we change the state of useLocalStorage hooks, it is likely that we make a state change in the App component which will trigger all children components to re-render. Of course, there is some way to get rid of this problem (one of the most common ways is using memorization, I won’t go into the solution in this article since it’s out of scope). But what I’m trying to show here is that every change you make to the state of the custom hooks will cause the current root component which is using that hooks. A specific case that I faced is when I use a server state management lib called react-query. I tried to put all the queries (which will automatically cached and only re-fetch if you reach the stale time of that query or when you force invalidate them), I called it useTweetQuery the problem comes when I used it in multiple places with different uses, in some component I get the data of tweet list, another component I get the data of specific tweet. Even though each time you call the custom hook, it is a brand new instance of that custom hook, the shared state which is managed by react-query is still the same. Therefore, each time I force invalidate any query (or any query reaches the stale time of the data), all the components using that custom hook will re-render no matter if they use that query or not.

Another built-in feature I really like in React is Context. It provides us with a global store that we can use to share states between multiple components at different places in our application. And with the born of Custom Hooks, it’s even more powerful, since one of the main reasons that make us stick with Redux is the ability to separate our logic code to UI code and make asynchronous operations out of our UI components.

Let’s consider the following example:

tsx
// app.context.tsx
import React, { useReducer, useMemo, useContext } from "react";

type TAppState = {
  globalState1: string;
  globalState2: string;
};

type TAppAction =
  | {
      type: "UPDATE_GLOBAL_STATE_1";
      payload: string;
    }
  | {
      type: "UPDATE_GLOBAL_STATE_2";
      payload: string;
    };

const initialState: TAppState = {
  globalState1: "hello from global state 1",
  globalState2: "hello from global state 2",
};

type TAppDispatch = (action: TAppAction) => void;

const AppContext = React.createContext<{
  state: TAppState;
  dispatch: TAppDispatch;
} | null>(null);

const appReducer = (state: TAppState = initialState, action: TAppAction) => {
  switch (action.type) {
    case "UPDATE_GLOBAL_STATE_1":
      return {
        ...state,
        globalState1: action.payload,
      };
    case "UPDATE_GLOBAL_STATE_2":
      return {
        ...state,
        globalState1: action.payload,
      };
    default:
      return state;
  }
};

const AppProvider = ({ children }: { children: React.ReactNode }) => {
  const [state, dispatch] = useReducer(appReducer, initialState);

  const value = useMemo(() => ({ state, dispatch }), [state, dispatch]);

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};

export const useAppContext = () => {
  const context = useContext(AppContext);
  if (context === null) {
    throw new Error("AppContext is not available");
  }
  return context;
};

export default AppProvider;
// app.context.tsx
import React, { useReducer, useMemo, useContext } from "react";

type TAppState = {
  globalState1: string;
  globalState2: string;
};

type TAppAction =
  | {
      type: "UPDATE_GLOBAL_STATE_1";
      payload: string;
    }
  | {
      type: "UPDATE_GLOBAL_STATE_2";
      payload: string;
    };

const initialState: TAppState = {
  globalState1: "hello from global state 1",
  globalState2: "hello from global state 2",
};

type TAppDispatch = (action: TAppAction) => void;

const AppContext = React.createContext<{
  state: TAppState;
  dispatch: TAppDispatch;
} | null>(null);

const appReducer = (state: TAppState = initialState, action: TAppAction) => {
  switch (action.type) {
    case "UPDATE_GLOBAL_STATE_1":
      return {
        ...state,
        globalState1: action.payload,
      };
    case "UPDATE_GLOBAL_STATE_2":
      return {
        ...state,
        globalState1: action.payload,
      };
    default:
      return state;
  }
};

const AppProvider = ({ children }: { children: React.ReactNode }) => {
  const [state, dispatch] = useReducer(appReducer, initialState);

  const value = useMemo(() => ({ state, dispatch }), [state, dispatch]);

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};

export const useAppContext = () => {
  const context = useContext(AppContext);
  if (context === null) {
    throw new Error("AppContext is not available");
  }
  return context;
};

export default AppProvider;
tsx
// index.tsx
import { StrictMode } from "react";
import * as ReactDOMClient from "react-dom/client";
import AppProvider from "./app.context";

import App from "./App";

const rootElement = document.getElementById("root");
const root = ReactDOMClient.createRoot(rootElement);

root.render(
  <StrictMode>
    <AppProvider>
      <App />
    </AppProvider>
  </StrictMode>,
);
// index.tsx
import { StrictMode } from "react";
import * as ReactDOMClient from "react-dom/client";
import AppProvider from "./app.context";

import App from "./App";

const rootElement = document.getElementById("root");
const root = ReactDOMClient.createRoot(rootElement);

root.render(
  <StrictMode>
    <AppProvider>
      <App />
    </AppProvider>
  </StrictMode>,
);
tsx
// App.tsx
import { ChangeEvent, useContext, useEffect } from "react";
import { useAppContext } from "./app.context";
import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <TestComponentA />
      <TestComponentB />
    </div>
  );
}

const TestComponentA = () => {
  const {
    state: { globalState1 },
    dispatch,
  } = useAppContext();

  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    dispatch({
      type: "UPDATE_GLOBAL_STATE_1",
      payload: e.target.value,
    });
  };

  useEffect(() => {
    console.log("test component A re-rendered");
  });

  return (
    <div>
      {globalState1}
      <input onChange={onChange} />
    </div>
  );
};

const TestComponentB = () => {
  const {
    state: { globalState2 },
    dispatch,
  } = useAppContext();

  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    dispatch({
      type: "UPDATE_GLOBAL_STATE_2",
      payload: e.target.value,
    });
  };

  useEffect(() => {
    console.log("test component B re-rendered");
  });

  return (
    <div>
      {globalState2}
      <input onChange={onChange} />
    </div>
  );
};
// App.tsx
import { ChangeEvent, useContext, useEffect } from "react";
import { useAppContext } from "./app.context";
import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <TestComponentA />
      <TestComponentB />
    </div>
  );
}

const TestComponentA = () => {
  const {
    state: { globalState1 },
    dispatch,
  } = useAppContext();

  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    dispatch({
      type: "UPDATE_GLOBAL_STATE_1",
      payload: e.target.value,
    });
  };

  useEffect(() => {
    console.log("test component A re-rendered");
  });

  return (
    <div>
      {globalState1}
      <input onChange={onChange} />
    </div>
  );
};

const TestComponentB = () => {
  const {
    state: { globalState2 },
    dispatch,
  } = useAppContext();

  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    dispatch({
      type: "UPDATE_GLOBAL_STATE_2",
      payload: e.target.value,
    });
  };

  useEffect(() => {
    console.log("test component B re-rendered");
  });

  return (
    <div>
      {globalState2}
      <input onChange={onChange} />
    </div>
  );
};

You can see the full source code at Link

When you type into any of the 2 inputs, you always see that componentA and componentB always re-render even though they don’t care about the changed state in the global context.

So can you see the problem now? Every time a slice of data in Context is changed, all the components which are consuming that Context Provider also re-render as well, even though they might not use the recently changed slice of data. You can solve this issue by splitting the appContext into smaller contexts. But that solution comes with the cost of refactoring and writing a lot of code in order to archive that, moreover, if you use an extension like React extension when you go to dev tools and see the component tree, you will see something called context hell like this.

2. React Context: NtMw8

If your app is small or you just have a few global states, using Context and Custom Hooks is perfect. But when it comes to a super large app with complex global states, using Redux (or some other state management libraries) might be a better choice. I’ve only used Redux and Recoil, both of them don’t run into the problem like Context, as long as you specify the slice of data you want to get from the global store (⚠️ If you just simply get a giant object from the global store when using Redux/Recoil, you’ll run into the same problem with Context)

One of the best ways to improve our React app is to avoid unnecessary re-render, when it comes to that point, you’ll find a bunch of articles on the internet about that topic, and sooner or later, you will see they mention about useCallback, useMemo. Yeah, they’re awesome hooks that could help us prevent wasting re-rendering only if they’re used correctly

We know that a component re-renders if its states or props are changed (1).

Let’s consider the following code

tsx
import { useCallback, useEffect, useMemo, useState } from "react";

const ChildComponent = ({
  childText,
  onChangeChild,
}: {
  childText: string;
  onChangeChild: () => void;
}) => {
  useEffect(() => {
    console.log("child component re-rendered.");
  });

  return (
    <div>
      <p>{childText}</p>
      <button onClick={onChangeChild}>click on change child </button>
    </div>
  );
};

export default function App() {
  const [number, setNumber] = useState(0);

  const childText = useMemo(() => {
    return "Hello this is child text";
  }, []);

  const onChangeChild = useCallback(() => {
    console.log("On change child was called");
  }, []);

  return (
    <div className="App">
      <h1>{number}</h1>
      <button onClick={() => setNumber((v) => v + 1)}> Increase number </button>
      <ChildComponent childText={childText} onChangeChild={onChangeChild} />
    </div>
  );
}
import { useCallback, useEffect, useMemo, useState } from "react";

const ChildComponent = ({
  childText,
  onChangeChild,
}: {
  childText: string;
  onChangeChild: () => void;
}) => {
  useEffect(() => {
    console.log("child component re-rendered.");
  });

  return (
    <div>
      <p>{childText}</p>
      <button onClick={onChangeChild}>click on change child </button>
    </div>
  );
};

export default function App() {
  const [number, setNumber] = useState(0);

  const childText = useMemo(() => {
    return "Hello this is child text";
  }, []);

  const onChangeChild = useCallback(() => {
    console.log("On change child was called");
  }, []);

  return (
    <div className="App">
      <h1>{number}</h1>
      <button onClick={() => setNumber((v) => v + 1)}> Increase number </button>
      <ChildComponent childText={childText} onChangeChild={onChangeChild} />
    </div>
  );
}

Here, we use useMemo and useCallback to memorize the props we passed into the ChildComponent. Some might think that our ChildComponent will only render 1 time after its initialization. But let’s test it out. When you click “Increase number”, go to console log and see what happens.

You can test it out Here

We know that a component re-renders if its states changed or props change (1).

The above statement is not false, but it’s not fully cover all the cases when the component is re-rendered. The component also re-renders when the parent component changes. If we want to prevent this issue, we need to wrap the ChildComponent with React.memo

tsx
const MemorizedChildComponent = React.memo(ChildComponent);
const MemorizedChildComponent = React.memo(ChildComponent);

Now, try replacing the ChildComponent with MemorizedChildComponent, we will see that the child component only renders 1 (or 2 if you’re in Strict Mode)

Therefore, if in your React app, whenever you use useCallback or useMemo to prevent unnecessary re-render, just remember to use them with React.memo, otherwise, you’re wasting your app memory for nothing.

If you render a list of items without specifying the “key” props, React will warn us. But for a long time, I misunderstood what is the purpose of using key props there. When you go to the home page of React or some other pages talking about key props, they do have some statements, and some examples to show you about it. I don’t know do they make sense to you, but to me, they’re not really clear so I will try to explain it in my way.

You need the key props to identify the item on the list. The main purpose of it is that React will know if that item is new or old. If React sees a key that was not there before, it will think: “Oh, that’s a new one item, let’s create a new item”, but when it sees the existing key again, it won’t re-create it but re-render it if needed. So the main difference here is between “re-creating” and “re-rendering”. Okay, so I understand that part, so why it’s not recommended to use the index as key props? To explain it to you, let’s consider the following small app

tsx
import { useEffect, useMemo, useState } from "react";
import "./styles.css";
import { shuffle } from "./utils";

const ListItem = ({
  item,
}: {
  item: {
    title: string;
    body: string;
  };
}) => {
  const [isActive, setIsActive] = useState<boolean>(false);

  useEffect(() => {
    console.log("List item was created");
  }, []);

  useEffect(() => {
    console.log("List item re-rendered");
  });

  const onClickItem = () => {
    setIsActive((v) => !v);
  };

  return (
    <article
      onClick={onClickItem}
      style={{
        display: "flex",
        gap: "1rem",
        alignItems: "center",
        border: "1px solid #000",
        background: `${isActive ? "red" : "none"}`,
      }}
    >
      <h3>{item.title}</h3>
      <p>{item.body}</p>
    </article>
  );
};

const initialData = new Array(5).fill(0).map((_, idx: number) => ({
  title: idx + "",
  body: idx + "",
}));

export default function App() {
  const [data, setData] = useState(initialData);
  const [flag, setFlag] = useState<number>(0);

  const onClickShuffle = () => {
    const newData = shuffle(data);
    setData(newData);
  };

  const toggleFlag = () => {
    setFlag((v) => 1 - v);
  };

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      {data?.map((e, idx: number) => (
        <ListItem item={e} key={idx} />
      ))}
      Flag {flag}
      <button onClick={onClickShuffle}>Shuffle List</button>
      <button onClick={toggleFlag}> Toggle flag </button>
    </div>
  );
}
import { useEffect, useMemo, useState } from "react";
import "./styles.css";
import { shuffle } from "./utils";

const ListItem = ({
  item,
}: {
  item: {
    title: string;
    body: string;
  };
}) => {
  const [isActive, setIsActive] = useState<boolean>(false);

  useEffect(() => {
    console.log("List item was created");
  }, []);

  useEffect(() => {
    console.log("List item re-rendered");
  });

  const onClickItem = () => {
    setIsActive((v) => !v);
  };

  return (
    <article
      onClick={onClickItem}
      style={{
        display: "flex",
        gap: "1rem",
        alignItems: "center",
        border: "1px solid #000",
        background: `${isActive ? "red" : "none"}`,
      }}
    >
      <h3>{item.title}</h3>
      <p>{item.body}</p>
    </article>
  );
};

const initialData = new Array(5).fill(0).map((_, idx: number) => ({
  title: idx + "",
  body: idx + "",
}));

export default function App() {
  const [data, setData] = useState(initialData);
  const [flag, setFlag] = useState<number>(0);

  const onClickShuffle = () => {
    const newData = shuffle(data);
    setData(newData);
  };

  const toggleFlag = () => {
    setFlag((v) => 1 - v);
  };

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      {data?.map((e, idx: number) => (
        <ListItem item={e} key={idx} />
      ))}
      Flag {flag}
      <button onClick={onClickShuffle}>Shuffle List</button>
      <button onClick={toggleFlag}> Toggle flag </button>
    </div>
  );
}

Every time we click on the toggle flag button, the List Item is re-rendered, even though we don’t change the data of the list. This comes as the first misunderstanding that I run into in the past. I thought that when I specify a key here, the list item won’t re-render if the data does not change. But it’s not true. Oh, so now we have a problem with re-rendering, let’s try to memorize the list item and see if it could solve the issue or not.

tsx
const MemorizedListItem = React.memo(ListItem);
const MemorizedListItem = React.memo(ListItem);

Then replace the ListItem with MemorizedListItem.

Okay, perfect, now when we click on Toggle Flag, the list item does not re-render anymore.

But now we have another problem. Let’s try clicking an item in the list and then try clicking on the Shuffle List button.

There are 2 problems here:

  • For example, if we click the item with the title 0, it’s the first item on the list, and the background of that item turns red, yes, right, that’s what we expect it to be. Now when you click the Shuffle List Button, the item with the title 0 is moved to another place, not the first item of the list, but now, the first item of the list still has the background of red, while the background of the item has the title is 0 now turns back to white. Note that in some scenarios, that’s what your expected result is. But most of the time, when we click on an item in a list, we want to remain in the state associated with that item when the list changes (and after changes, that item is still on the list, of course).
  • The second thing is, even though we memorized the component, it still re-render

So why do they happen? Please note that in here, we’re using the index as the key prop of the list item. As I mentioned before, when it sees the existing key again, it won’t re-create it but re-render it if needed.

Let’s go deep into the “needed” case. When rendering the list of items, React will compare that list with the previous one and find the existing item using the key props. From React’s perspective, 2 components with the same type and same key are the same. That will answer 2 above problems. Since we are using the index as key props here. So every time React re-renders the component, it will see that all the components are the same as before, just the item props change (because we shuffled the list). Because the item props change → the list item will re-render even though we memorized it. And because React sees all the components as before, it will keep the state associated with the list item. When we click the first item and update the isActive state to be true, React will understand that the item with key 0 has the isActive state as true, therefore when we shuffle the list, the first item still remains red background.

That’s the main reason why using the index as the key props is not recommended if you have a dynamic list. The solution for those 2 problems is simple, change the key props to something unique between items so that React can identify them better. Let’s add an id key to the data

tsx
const initialData = new Array(5).fill(0).map((_, idx: number) => ({
  title: idx + "",
  body: idx + "",
  id: idx,
}));
const initialData = new Array(5).fill(0).map((_, idx: number) => ({
  title: idx + "",
  body: idx + "",
  id: idx,
}));

and update the key

tsx
{
  data?.map((e, idx: number) => {
    return <MemorizedListItem item={e} key={e.id} />;
  });
}
{
  data?.map((e, idx: number) => {
    return <MemorizedListItem item={e} key={e.id} />;
  });
}

Now, when we click on Shuffle button or click on an item, the isActive state will stick to the item we’ve just clicked. and if we click the shuffle button, the list item won’t re-render anymore.

Find the final code here

We all know that useCallback and useMemo are used for improving the performance of our React app by reducing the re-rendering process. But everything comes with a cost, and so do use useCallback and useMemo. useCallback memorizes the function while useMemo memorizes the result of function use passed into it. Memorization requires memory to store them, so the more useCallback and useMemo → more memory your app need when it runs.

I think that a very common mistake that newbie encounters when using useCallback and useMemo is that they sometimes try to use them as much as possible and think that it will help with improving the performance of their applications. But keep in mind that, the point of using them is to prevent re-rendering. Some might argue that useMemo is used for memorizing the result of heavy functions, yeah, it’s true, but how many times have you been in that situation? and what’s the definition of “heavy function”? A very well-known example in some articles about useMemo is a function that calculates the n-th prime, seriously? Personally, I don’t think that you will rarely have to use useMemo for that kind of purpose. So let’s get back to the story, remember this: “Only use useCallback for functions and useMemo for values when you have a child component in your component that has them as props. But if you have any other props of that child component that can’t be memorized and changed after each render, don’t use useCallback or useMemo, otherwise, you just waste the user’s memory for nothing. Because at the end of the day, if any props changes, the component will be re-rendered.

I hope that you can get my point here.

So that’s all for today. Remember that, no matter if it’s basic or advanced concepts, please make sure that you understand them in deep before using them in your project.

Tagged:#React#Web Development
0