React Performance Optimization: The Art of Avoiding Useless Renders

React Performance Optimization: The Art of Avoiding Useless Renders

React is designed to be highly efficient, but unnecessary re-renders can still slow down your application. Optimizing performance by minimizing redundant renders ensures a smoother user experience. This blog explores key techniques to avoid unnecessary re-renders in React applications with detailed explanations and real-world examples.

Understanding React Re-Renders

React components re-render when:

  • State updates within the component

  • Props change from the parent component

  • The parent component re-renders

  • Context values update

Unnecessary re-renders occur when components update without actual data changes, leading to wasted computation and degraded performance.

Example of Unnecessary Re-Renders

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


  return (
    <div>
      <Child count={count} />
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

const Child = ({ count }) => {
  console.log("Child Rendered");
  return <p>Count: {count}</p>;
};

In this example, every time the Parent component re-renders, the Child component also re-renders, even if the count prop remains unchanged.

Using React.memo() for Component Memoization

React.memo() is a higher-order component (HOC) that optimizes functional components by memoizing their rendered output. If the component receives the same props on subsequent renders, React will reuse the previous render result instead of re-rendering it.

const Child = React.memo(({ count }) => {

  console.log("Child Rendered");

  return <p>Count: {count}</p>;

});

Now, the Child component will only re-render when count changes, avoiding unnecessary renders.

Using useCallback Hook

useCallback is a React hook that returns a memoized version of a function. This is useful when passing functions as props to memoized components, preventing unnecessary re-renders.

import React, { useState, useCallback } from 'react';
import Button from './Button';

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

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

  return (
    <div>
      <p>Count: {count}</p>
      <Button handleClick={increment} label="Increment" />
    </div>
  );
};

export default App;

Without useCallback, a new increment function is created on every render, causing Button to re-render even if its props haven’t changed. By using useCallback, the function reference remains stable, preventing unnecessary renders.

Optimizing State Updates

Frequent and unnecessary state updates can trigger extra re-renders. Strategies to avoid them:

  • Keep state minimal: Store only necessary data in useState.

  • Use functional updates: When updating state based on previous state, use functional updates to prevent unnecessary re-renders.

      const [user, setUser] = useState({ name: "Abhi", age: 24 });
    
      const updateAge = () => {
        setUser({ ...user, age: user.age + 1 });
      };
    

    Even if only age changes, the entire user object gets recreated, triggering a re-render.

      const [age, setAge] = useState(24);
      const updateAge = () => setAge(prevAge => prevAge + 1);
    

    By only tracking age, we reduce unnecessary re-renders.

Preventing Unnecessary Prop Changes

Props passed to child components should remain stable to avoid re-renders. Common solutions include:

  • Use useCallback() for event handlers: This ensures functions maintain reference equality between renders.

      const Parent = () => {
        const handleClick = () => console.log("Clicked");
        return <Child onClick={handleClick} />;
      };
    

    Here, handleClick is recreated on every render, causing Child to re-render.

      const handleClick = useCallback(() => console.log("Clicked"), []);
    

Leveraging Context API with useContext() Efficiently

Using React Context without optimization can lead to unnecessary renders across consuming components.

const ThemeContext = React.createContext();

const ThemedComponent = () => {
  const theme = useContext(ThemeContext);
  return <div style={{ background: theme }}>Hello</div>;
};

If the entire context provider updates, all consumers will re-render.

Optimized Context Usage

  • Split context providers to isolate updates.

  • Use React.memo() or selectors inside consumers.

const ThemeProvider = ({ children }) => {

  const [theme, setTheme] = useState("light");

  const value = useMemo(() => ({ theme, setTheme }), [theme]);

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

};

Avoiding Unnecessary Effects in useEffect()

useEffect() should not include dependencies that don’t impact the effect’s execution.

useEffect(() => {

  console.log("Effect running");

}, [someObject]);

If someObject is recreated on each render, the effect runs unnecessarily. Fix this by memoizing someObject using useMemo().

Optimized Example

const someObject = useMemo(() => ({ key: "value" }), []);

useEffect(() => {

  console.log("Effect running");

}, [someObject]);

Virtualizing Lists for Large Datasets

Rendering long lists can be expensive. Use virtualization libraries like react-window or react-virtualized to render only visible items.

Example with react-window

import { FixedSizeList as List } from "react-window";


const MyList = ({ items }) => (

  <List height={400} width={300} itemSize={35} itemCount={items.length}>

    {({ index, style }) => <div style={style}>{items[index]}</div>}

  </List>

);

Avoiding Inline Functions and Objects

Creating inline functions and objects inside a component’s render function can lead to unnecessary re-renders.

const MyComponent = () => {

  const obj = { key: "value" }; // Recreated on every render

  return <ChildComponent obj={obj} />;

};

Use useMemo hook to cache the object, preventing it from being recreated unnecessarily.

const obj = useMemo(() => ({ key: "value" }), []);

Conclusion

Avoiding unnecessary renders in React improves performance and user experience. By leveraging React.memo(), useCallback hook, optimizing state updates, preventing excessive reactivity, and using virtualization, you can keep your React applications fast and efficient. Implement these best practices and see a significant improvement in your app's performance!