DEV Community

Cover image for WHY YOUR REACT APP RE-RENDERS TOO MUCH (AND WHAT ACTUALLY FIXES IT)
qodors
qodors

Posted on Originally published at linkedin.com

WHY YOUR REACT APP RE-RENDERS TOO MUCH (AND WHAT ACTUALLY FIXES IT)

A React page feels slow, so someone opens the code and starts adding memo, useMemo, and useCallback everywhere.

Usually that makes the code harder to read before it makes the page faster.

React rendering a component again is normal. React does that when state, props, or context change so it can work out what the screen should look like now. It can run a component function again without changing anything in the browser.

It becomes worth looking into when a small update makes expensive parts of the page run again for no useful reason. A user opens a help panel, but a large table, charts, and filters also run. That is where the page starts to feel slow.

Most of the time, the cause is simple: state is sitting too high in the component tree.

WHAT A RE-RENDER ACTUALLY MEANS

Developers often open React DevTools, see a component render several times, and assume something is broken. Often, it is not.

A render means React ran the component function again. It does not automatically mean the browser rebuilt every element on the page. React still compares the new result with what was already on screen and only updates the DOM where it needs to.

What matters is whether that render is doing expensive work.

A button rendering again is rarely worth worrying about. A table with thousands of rows, a large chart, or a costly filter running again after an unrelated click is worth checking.

STATE SITTING TOO HIGH IN THE PAGE

Here is a common setup:

function ProductPage({ products }) {
  const [isHelpOpen, setIsHelpOpen] = useState(false);
  return (
 <>
   <button onClick={() => setIsHelpOpen(true)}>
     Need help?
   </button>
     {isHelpOpen && (
        <HelpPanel onClose={() => setIsHelpOpen(false)} />
      )}
      <ProductList products={products} />
      </>
 );
}
Enter fullscreen mode Exit fullscreen mode

Opening the help panel changes state in ProductPage. React renders ProductPage again, and ProductList runs again too, even though the products did not change.

That may be fine for a small list. It becomes a problem when ProductList is large or does work that takes time.

Move the state down to the component that uses it:

function HelpButton() {
 const [isHelpOpen, setIsHelpOpen] = useState(false);
 return (
  <>
     <button onClick={() => setIsHelpOpen(true)}>
      Need help?
     </button>
        {isHelpOpen && (
        <HelpPanel onClose={() => setIsHelpOpen(false)} />
        )}
      </>
   );
} 
function ProductPage({ products }) {
  return (
   <>
      <HelpButton />
      <ProductList products={products} />
     </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now opening help updates HelpButton and HelpPanel. ProductPage does not need to update, so ProductList does not run again.

In many React screens, moving state down the tree fixes the issue without any memoization.

MEMO ONLY HELPS WHEN PROPS STAY THE SAME

React.memo can skip a render when a component receives the same props as last time.

But it cannot do much if you pass new objects and functions into that component every time its parent renders.

const ProductList = memo(function ProductList({ products, options }) {
  // Expensive list rendering
});
Then a parent does this:

<ProductList
   products={products}
   options={{ showStock: true }}
/>
Enter fullscreen mode Exit fullscreen mode

The object looks the same, but it is a new object on every render. memo sees a different options reference and runs ProductList again.

The simplest fix is often to pass the actual value instead of wrapping it in an object:

<ProductList
  products={products}
  showStock={true}
/>
Enter fullscreen mode Exit fullscreen mode

If the component really needs an object and its values do not change often, keep that object stable:

const options = useMemo(() => ({
 showStock
}), [showStock]);
<ProductList
  products={products}
  options={options}
/>
Enter fullscreen mode Exit fullscreen mode

Do this when the component is expensive and you have checked that it helps. Do not add useMemo around every object in the app. For cheap components, the extra code is usually not worth it.

BE CAREFUL WITH LARGE CONTEXT OBJECTS

Context is useful. It is also easy to put too much into one place.

const AppContext = createContext(null);
function AppProvider({ children }) {
 const [user, setUser] = useState(null);
 const [theme, setTheme] = useState("dark");
 const [notifications, setNotifications] = useState([]);
   return (
   <AppContext.Provider
      value={{ user, setUser, theme, setTheme, notifications }}
   >
      {children}
   </AppContext.Provider>
   );
}
Enter fullscreen mode Exit fullscreen mode

Any component that reads this context will re-render when the provider value changes.

Add a notification, and a component that only needs the theme can still render again because both values live in the same context. The object passed to value is also new whenever AppProvider renders.

You do not need a separate context for every value. But avoid keeping every unrelated value in one large context object.

For example:

const ThemeContext = createContext(null);
const UserContext = createContext(null);
const NotificationContext = createContext(null);
Enter fullscreen mode Exit fullscreen mode

Theme, user data, and notifications often change for different reasons. Keeping them separate stops one update from affecting every consumer of one large context.

For state used by one screen or one small part of a screen, regular component state is often easier than context.

DO NOT ADD MEMOIZATION BEFORE CHECKING THE PAGE

memo, useMemo, and useCallback are useful tools. They are not a default setting for React code.

A page with memoization everywhere is harder to change. You have dependency arrays to keep right, object references to think about, and more chances to keep an old value by mistake.

Use the React DevTools Profiler first.

Record the interaction that feels slow: typing in a filter, opening a panel, switching tabs, or selecting an item. Check which components rendered and how long they took.

You may find that the real problem is a large list that needs pagination or virtualization. You may find a calculation that should be memoized. Or you may find state that only needs to move down one component.

The profiler shows which component took time during the slow interaction, so you know where to start.

OUR TAKE

At Qodors, we often see state sitting high in a page and memoization added later to deal with all the extra renders.

Moving that state closer to where it is used usually solves more of the problem.

A component rendering again is not a failure. But if opening a help drawer causes a large product table to run expensive work again, that is worth fixing. Keep state close to the part of the page that owns it, then profile the page before adding memoization.

That keeps the code easier to work with and removes the updates users can actually feel.

QUICK REFERENCE

  • A React re-render does not always mean the DOM changed

  • Look for expensive components running after unrelated state changes

  • Keep state close to the component that uses it

  • New object and function props can stop memo from helping

  • Do not keep every unrelated value in one large context object

  • Use React DevTools Profiler before adding memo, useMemo, or useCallback

  • Use memoization for expensive work you have measured

Do not try to stop every React render. Find the interaction that feels slow, check what ran during it, and remove the work that did not need to happen.

React #ReactJS #JavaScript #Frontend #WebDevelopment #ReactPerformance #ReactHooks #TypeScript #WebDev #QodorsEdge

Written by the team at Qodors — we build and improve full-stack products for a living. → https://www.qodors.com/?utm_source=devto&utm_medium=post&utm_campaign=react_rerenders

Top comments (1)

Collapse
 
morphoices profile image
MORPHOICΞS.

I would have the greatest interest in watching the comparison between fixing unnecessary renders vs fixing why they happen. ~

It’s easy to leap on memoization once you see the profiler lighting up, but that can just make the code harder to skim.

Initially, I would analyse the data flow, i.e., which actual state is required to change, and which component really needs to know this. Most often this does the bigger fix.