DEV Community

VinaySehwag14
VinaySehwag14

Posted on

Optimizing React Re-renders: A Developer's Complete Guide

React is known for its ability to efficiently update and render UI components. However, as your application grows, unnecessary re-renders can creep in, affecting performance and user experience. In this guide, we'll dive deep into React optimization strategies and provide practical examples to ensure your app runs smoothly.


Why Optimize React Re-renders?

React’s rendering process is efficient, but unnecessary re-renders can:

  • Increase CPU usage, slowing down apps.
  • Cause visible lags for users.
  • Lead to hard-to-debug performance issues.

The good news is that React provides tools and patterns to optimize rendering.


1. State Colocation

What It Is:
State colocation means keeping state close to where it’s needed. This minimizes the impact of state changes on unrelated components.

Example:

Bad Example:

function Parent() {
  const [input, setInput] = useState("");

  return (
    <div>
      <ChildA input={input} setInput={setInput} />
      <ChildB />
    </div>
  );
}

function ChildA({ input, setInput }) {
  return (
    <input
      value={input}
      onChange={(e) => setInput(e.target.value)}
      placeholder="Type something"
    />
  );
}

function ChildB() {
  console.log("ChildB re-rendered unnecessarily!");
  return <div>I’m unrelated to the input field.</div>;
}
Enter fullscreen mode Exit fullscreen mode

Problem:
ChildB re-renders every time the input state changes, even though it doesn’t use input.

Optimized Example:

function Parent() {
  return (
    <div>
      <ChildA />
      <ChildB />
    </div>
  );
}

function ChildA() {
  const [input, setInput] = useState("");

  return (
    <input
      value={input}
      onChange={(e) => setInput(e.target.value)}
      placeholder="Type something"
    />
  );
}

function ChildB() {
  console.log("ChildB rendered only once!");
  return <div>I’m unrelated to the input field.</div>;
}
Enter fullscreen mode Exit fullscreen mode

Why It's Better:
By colocating input state inside ChildA, ChildB doesn’t re-render unnecessarily.


2. Derived State

What It Is:
Derived state avoids duplicating data by calculating values dynamically.

Example:

Bad Example:

function Cart({ items }) {
  const [total, setTotal] = useState(0);

  useEffect(() => {
    setTotal(items.reduce((sum, item) => sum + item.price, 0));
  }, [items]);

  return <div>Total: {total}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Problem:
The total state duplicates data already present in items and risks getting out of sync.

Optimized Example:

function Cart({ items }) {
  const total = items.reduce((sum, item) => sum + item.price, 0);
  return <div>Total: {total}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Why It's Better:
total is derived directly from items, ensuring consistency and reducing unnecessary logic.


3. Component Composition

What It Is:
Breaking large components into smaller, focused ones limits the impact of changes.

Example:

Bad Example:

function Dashboard({ user }) {
  return (
    <div>
      <div>Welcome, {user.name}!</div>
      <Notifications user={user} />
      <Settings user={user} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Problem:
Any change in user forces the entire Dashboard to re-render.

Optimized Example:

function Dashboard({ user }) {
  return (
    <div>
      <UserProfile name={user.name} />
      <Notifications user={user} />
      <Settings user={user} />
    </div>
  );
}

function UserProfile({ name }) {
  return <div>Welcome, {name}!</div>;
}
Enter fullscreen mode Exit fullscreen mode

Why It's Better:
By splitting responsibilities, re-renders affect only updated parts.


4. Context Optimization

What It Is:
Using multiple smaller contexts instead of one large context avoids widespread re-renders.

Example:

Bad Example:

const AppContext = React.createContext();

function AppProvider({ children }) {
  const value = { user: { name: "Vinay" }, theme: "dark" };
  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

function Profile() {
  const { user } = useContext(AppContext);
  return <div>{user.name}</div>;
}

function ThemeSwitcher() {
  const { theme } = useContext(AppContext);
  return <button>{theme}</button>;
}
Enter fullscreen mode Exit fullscreen mode

Problem:
Changing theme causes Profile to re-render unnecessarily.

Optimized Example:

const UserContext = React.createContext();
const ThemeContext = React.createContext();

function AppProvider({ children }) {
  return (
    <UserContext.Provider value={{ name: "Vinay" }}>
      <ThemeContext.Provider value="dark">
        {children}
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

function Profile() {
  const user = useContext(UserContext);
  return <div>{user.name}</div>;
}

function ThemeSwitcher() {
  const theme = useContext(ThemeContext);
  return <button>{theme}</button>;
}
Enter fullscreen mode Exit fullscreen mode

Why It's Better:
Changes in theme no longer trigger re-renders in Profile.


5. Memoization

What It Is:
Use useMemo and useCallback to cache values and functions, avoiding unnecessary recomputation.

Example:

Without Memoization:

function ExpensiveComponent({ data }) {
  const result = data.reduce((sum, item) => sum + item.value, 0);
  console.log("Expensive computation!");
  return <div>{result}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Optimized Example:

function ExpensiveComponent({ data }) {
  const result = useMemo(() => {
    console.log("Expensive computation!");
    return data.reduce((sum, item) => sum + item.value, 0);
  }, [data]);

  return <div>{result}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Why It’s Better:
The computation is cached and recalculated only when data changes.


6. Lift Expensive Components

What It Is:
Reuse expensive components across renders by lifting them higher in the component tree.

Example:

Without Optimization:

function Parent() {
  return (
    <div>
      <ExpensiveComponent />
      <Child />
    </div>
  );
}

function ExpensiveComponent() {
  console.log("Expensive component rendered!");
  return <div>I’m expensive!</div>;
}
Enter fullscreen mode Exit fullscreen mode

Optimized Example:

const StaticExpensiveComponent = <ExpensiveComponent />;

function Parent() {
  return (
    <div>
      {StaticExpensiveComponent}
      <Child />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why It’s Better:
ExpensiveComponent is rendered only once and reused across renders.


Conclusion

Optimizing React re-renders is a vital skill for creating performant and scalable applications. By following these strategies—state colocation, derived state, component composition, context optimization, memoization, and lifting expensive components—you can ensure your app remains efficient as it grows.

Do you have other optimization tips? Let’s discuss in the comments!

Top comments (0)