AdSense Leaderboard

Google AdSense Placeholder

HEADER SLOT

What Triggers a Re-Render in React 19: Internals & Edge Cases

Last updated:
What Triggers a Re-Render in React 19: Internals & Edge Cases

Understand React re-render mechanics in depth. Learn how state updates, prop changes, and parent renders trigger component updates, with fiber architecture insights.

# What Triggers a Re-Render in React 19: Internals & Edge Cases

If you've debugged a React application, you've probably wondered: "Why did this component render again?" Understanding what actually triggers a re-render is crucial for writing performant applications. The answer isn't always obvious—React's rendering system has nuances that catch even experienced developers off-guard.

Let's dig into how React decides to re-render components and what this means for your architecture.

# Table of Contents

  1. The Core Rule: State Changes Drive Re-Renders
  2. Parent Re-Renders Cascade to Children
  3. Prop Changes Trigger Child Re-Renders
  4. Context Changes and Re-Renders
  5. What Doesn't Trigger Re-Renders
  6. The Fiber Architecture Behind Re-Renders
  7. Edge Cases and Gotchas
  8. Practical Optimization Patterns
  9. FAQ

# The Core Rule: State Changes Drive Re-Renders {#the-core-rule}

At React's heart is one fundamental principle: when state changes, the component function re-executes.

That's it. That's the rule.

When you call setState (or a state setter from useState), React doesn't just update the state value internally—it marks that component as needing a re-render. React then re-runs the component function, which causes useState to be called again, returning the updated state value.

# TypeScript Version

typescript
import { useState } from 'react';

interface CounterProps {
  initialCount?: number;
}

export function Counter({ initialCount = 0 }: CounterProps) {
  const [count, setCount] = useState(initialCount);

  const handleIncrement = () => {
    // Calling setCount triggers a re-render of Counter
    // The component function will re-execute with the new count value
    setCount(count + 1);
  };

  console.log('Counter rendered with count:', count);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
    </div>
  );
}

# JavaScript Version

javascript
import { useState } from 'react';

export function Counter({ initialCount = 0 }) {
  const [count, setCount] = useState(initialCount);

  const handleIncrement = () => {
    // Calling setCount triggers a re-render
    setCount(count + 1);
  };

  console.log('Counter rendered with count:', count);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
    </div>
  );
}

Each time you click the button, setCount is called, which triggers a re-render. The console.log statement executes again with the updated count value. This is by design—React needs to re-execute your component function to determine what the new UI should look like.

# Parent Re-Renders Cascade to Children {#parent-re-renders-cascade}

Here's where things get interesting. When a parent component re-renders, React automatically re-renders all child components—even if the child's props haven't changed.

This is a common source of performance problems. A parent state update causes the parent to re-render, which causes its children to re-render, regardless of whether their props changed.

# TypeScript Version

typescript
import { useState, ReactNode } from 'react';

interface ParentProps {
  children?: ReactNode;
}

interface ChildProps {
  value: number;
}

export function Parent() {
  const [parentCount, setParentCount] = useState(0);

  const handleIncrement = () => {
    setParentCount(parentCount + 1);
  };

  return (
    <div>
      <p>Parent count: {parentCount}</p>
      <button onClick={handleIncrement}>Parent Increment</button>
      
      {/* Both children re-render when parentCount changes */}
      <Child1 value={parentCount} />
      <Child2 />
    </div>
  );
}

export function Child1({ value }: ChildProps) {
  console.log('Child1 rendered with value:', value);
  return <div>Child1: {value}</div>;
}

export function Child2() {
  console.log('Child2 rendered (no props received)');
  // This component re-renders even though it receives no props!
  // Its props didn't change, but the parent re-render triggered it anyway
  return <div>Child2: Static content</div>;
}

# JavaScript Version

javascript
import { useState } from 'react';

export function Parent() {
  const [parentCount, setParentCount] = useState(0);

  const handleIncrement = () => {
    setParentCount(parentCount + 1);
  };

  return (
    <div>
      <p>Parent count: {parentCount}</p>
      <button onClick={handleIncrement}>Parent Increment</button>
      
      <Child1 value={parentCount} />
      <Child2 />
    </div>
  );
}

export function Child1({ value }) {
  console.log('Child1 rendered with value:', value);
  return <div>Child1: {value}</div>;
}

export function Child2() {
  console.log('Child2 rendered (no props received)');
  return <div>Child2: Static content</div>;
}

In this example, both Child1 and Child2 re-render when the parent's state changes. Child1 has a reason—its value prop changed. But Child2 receives no props and its content never changes. It still re-renders because its parent re-rendered.

In a large application with deeply nested components, this cascade can cause real performance issues. This is exactly why React.memo and useCallback exist.

# Prop Changes Trigger Child Re-Renders {#prop-changes}

If a child component receives new props (via a parent update), the child will re-render. React compares the old props to the new props using shallow equality.

# TypeScript Version

typescript
import { useState } from 'react';

interface ItemProps {
  id: number;
  label: string;
  onClick: () => void;
}

export function ItemList() {
  const [selectedId, setSelectedId] = useState<number | null>(null);
  const [items, setItems] = useState([
    { id: 1, label: 'Item 1' },
    { id: 2, label: 'Item 2' },
    { id: 3, label: 'Item 3' },
  ]);

  const handleSelectItem = (id: number) => {
    setSelectedId(id);
  };

  return (
    <div>
      {items.map(item => (
        <Item
          key={item.id}
          id={item.id}
          label={item.label}
          onClick={() => handleSelectItem(item.id)}
          isSelected={selectedId === item.id}
        />
      ))}
    </div>
  );
}

interface ExtendedItemProps extends ItemProps {
  isSelected: boolean;
}

export function Item({ id, label, onClick, isSelected }: ExtendedItemProps) {
  console.log(`Item ${id} rendered`);
  // This component re-renders when:
  // - label changes
  // - onClick reference changes (new function created on every parent render)
  // - isSelected changes
  
  return (
    <button
      onClick={onClick}
      style={{ fontWeight: isSelected ? 'bold' : 'normal' }}
    >
      {label}
    </button>
  );
}

# JavaScript Version

javascript
import { useState } from 'react';

export function ItemList() {
  const [selectedId, setSelectedId] = useState(null);
  const [items, setItems] = useState([
    { id: 1, label: 'Item 1' },
    { id: 2, label: 'Item 2' },
    { id: 3, label: 'Item 3' },
  ]);

  const handleSelectItem = (id) => {
    setSelectedId(id);
  };

  return (
    <div>
      {items.map(item => (
        <Item
          key={item.id}
          id={item.id}
          label={item.label}
          onClick={() => handleSelectItem(item.id)}
          isSelected={selectedId === item.id}
        />
      ))}
    </div>
  );
}

export function Item({ id, label, onClick, isSelected }) {
  console.log(`Item ${id} rendered`);
  
  return (
    <button
      onClick={onClick}
      style={{ fontWeight: isSelected ? 'bold' : 'normal' }}
    >
      {label}
    </button>
  );
}

Critical insight: The onClick handler is created as a new function on every parent render (inside the .map() call). This means every Item receives a new onClick reference on every parent render, causing all items to re-render even if their other props haven't changed. This is a classic performance problem that useCallback solves.

# Context Changes and Re-Renders {#context-changes}

Context is a special case. When a context value changes, every component that consumes that context will re-render—regardless of whether the specific value they use actually changed.

# TypeScript Version

typescript
import { createContext, useContext, useState, ReactNode } from 'react';

interface ThemeContextType {
  isDark: boolean;
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

interface ThemeProviderProps {
  children: ReactNode;
}

export function ThemeProvider({ children }: ThemeProviderProps) {
  const [isDark, setIsDark] = useState(false);

  const toggleTheme = () => {
    setIsDark(!isDark);
  };

  // ⚠️ Creating a new object on every render
  // This causes all consumers to re-render even if they only use isDark
  const value = { isDark, toggleTheme };

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

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

export function ThemeToggle() {
  const { isDark, toggleTheme } = useTheme();
  console.log('ThemeToggle rendered');

  return (
    <button onClick={toggleTheme}>
      Current theme: {isDark ? 'dark' : 'light'}
    </button>
  );
}

export function ThemeInfo() {
  const { isDark } = useTheme();
  console.log('ThemeInfo rendered');
  // Even though this component only uses isDark,
  // it will re-render whenever toggleTheme reference changes!
  // (which is every parent render in this example)

  return <div>Theme is {isDark ? 'dark' : 'light'}</div>;
}

# JavaScript Version

javascript
import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext(undefined);

export function ThemeProvider({ children }) {
  const [isDark, setIsDark] = useState(false);

  const toggleTheme = () => {
    setIsDark(!isDark);
  };

  // Creating a new object on every render
  const value = { isDark, toggleTheme };

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

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

export function ThemeToggle() {
  const { isDark, toggleTheme } = useTheme();
  console.log('ThemeToggle rendered');

  return (
    <button onClick={toggleTheme}>
      Current theme: {isDark ? 'dark' : 'light'}
    </button>
  );
}

export function ThemeInfo() {
  const { isDark } = useTheme();
  console.log('ThemeInfo rendered');

  return <div>Theme is {isDark ? 'dark' : 'light'}</div>;
}

The issue here is that even though ThemeInfo only uses the isDark value, it re-renders when the context value object changes (which happens on every provider render in this example). A common solution is to use useMemo to memoize the context value.

# What Doesn't Trigger Re-Renders {#what-doesnt-trigger}

This is just as important to understand. Several things do not trigger re-renders:

  1. Regular JavaScript variables - Mutating a plain object or array doesn't trigger a re-render. React doesn't track these.

  2. DOM mutations outside React - If you directly manipulate the DOM with document.getElementById(), React won't know about it.

  3. External API calls or timers - Async operations don't trigger re-renders unless they update state.

  4. Props from non-component objects - If you pass a configuration object that's created outside your component, changes to that object won't trigger a re-render unless you update state.

# TypeScript Version

typescript
import { useState, useRef } from 'react';

interface CounterWithBugProps {
  onClick?: () => void;
}

export function CounterWithBug() {
  // ❌ This variable doesn't trigger a re-render when changed
  let localCount = 0;

  // ✅ This state value does trigger a re-render when changed
  const [stateCount, setStateCount] = useState(0);

  // This reference won't change and won't trigger re-renders
  const configRef = useRef({ maxCount: 10 });

  const handleClickBug = () => {
    localCount++;
    // The UI won't update because localCount is just a variable
    console.log('Local count:', localCount);
  };

  const handleClickCorrect = () => {
    setStateCount(stateCount + 1);
    // The UI will update because state changed
  };

  return (
    <div>
      <p>
        Local count (won't update): {localCount}
        {/* This will always show 0, even after clicks */}
      </p>
      <p>State count (will update): {stateCount}</p>
      <button onClick={handleClickBug}>Bug: Click me</button>
      <button onClick={handleClickCorrect}>Correct: Click me</button>
    </div>
  );
}

# JavaScript Version

javascript
import { useState, useRef } from 'react';

export function CounterWithBug() {
  // ❌ This variable doesn't trigger a re-render when changed
  let localCount = 0;

  // ✅ This state value does trigger a re-render when changed
  const [stateCount, setStateCount] = useState(0);

  const configRef = useRef({ maxCount: 10 });

  const handleClickBug = () => {
    localCount++;
    console.log('Local count:', localCount);
  };

  const handleClickCorrect = () => {
    setStateCount(stateCount + 1);
  };

  return (
    <div>
      <p>Local count (won't update): {localCount}</p>
      <p>State count (will update): {stateCount}</p>
      <button onClick={handleClickBug}>Bug: Click me</button>
      <button onClick={handleClickCorrect}>Correct: Click me</button>
    </div>
  );
}

This is a frequent source of bugs. Developers sometimes try to manage component state with plain variables and wonder why the UI doesn't update when they change the variable.

# The Fiber Architecture Behind Re-Renders {#fiber-architecture}

To understand why React makes these re-rendering decisions, you need to know about React's fiber architecture.

React doesn't immediately update the DOM when state changes. Instead, it:

  1. Schedules work - React marks components that need updating
  2. Builds a fiber tree - React creates a new tree of fiber objects representing the component structure
  3. Reconciles - React compares the new fiber tree with the old one using a diffing algorithm
  4. Commits - React applies the minimal set of DOM changes needed

This is why a parent re-render cascades to children—React needs to re-run the parent component function to know what children it should render. It can't just update the parent's DOM; it needs to check if the component tree structure itself has changed.

# How Shallow Equality Works

When React compares props between renders, it uses shallow equality. This means:

  • Primitive values (number, string, boolean) are compared by value
  • Objects and arrays are compared by reference

# TypeScript Version

typescript
import { useState, useMemo, useCallback } from 'react';

interface ConfigObject {
  theme: string;
  fontSize: number;
}

interface ChildProps {
  config: ConfigObject;
}

export function ParentWithOptimization() {
  const [theme, setTheme] = useState('light');
  const [fontSize, setFontSize] = useState(14);

  // ❌ This creates a new object on every render
  // const config = { theme, fontSize };

  // ✅ This memoizes the object, only recreating when theme/fontSize change
  const config = useMemo(() => ({ theme, fontSize }), [theme, fontSize]);

  // ✅ Callbacks are also memoized to maintain stable references
  const handleThemeToggle = useCallback(() => {
    setTheme(t => t === 'light' ? 'dark' : 'light');
  }, []);

  return (
    <div>
      <button onClick={handleThemeToggle}>Toggle theme</button>
      <Child config={config} />
    </div>
  );
}

export function Child({ config }: ChildProps) {
  console.log('Child rendered with config:', config);
  return <div>Theme: {config.theme}, Size: {config.fontSize}</div>;
}

# JavaScript Version

javascript
import { useState, useMemo, useCallback } from 'react';

export function ParentWithOptimization() {
  const [theme, setTheme] = useState('light');
  const [fontSize, setFontSize] = useState(14);

  // This memoizes the object
  const config = useMemo(() => ({ theme, fontSize }), [theme, fontSize]);

  const handleThemeToggle = useCallback(() => {
    setTheme(t => t === 'light' ? 'dark' : 'light');
  }, []);

  return (
    <div>
      <button onClick={handleThemeToggle}>Toggle theme</button>
      <Child config={config} />
    </div>
  );
}

export function Child({ config }) {
  console.log('Child rendered with config:', config);
  return <div>Theme: {config.theme}, Size: {config.fontSize}</div>;
}

# Edge Cases and Gotchas {#edge-cases}

# Re-Render Batching

React batches multiple state updates within the same event handler to avoid unnecessary re-renders.

typescript
export function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
    // Only ONE re-render happens here, not three!
    // All three updates are batched together
  };

  return <button onClick={handleClick}>Count: {count}</button>;
}

# Async Operations Don't Batch

If you perform async operations and then update state, those updates may not be batched:

typescript
export function AsyncCounter() {
  const [count, setCount] = useState(0);

  const handleClickAsync = async () => {
    await new Promise(resolve => setTimeout(resolve, 0));
    
    setCount(count + 1);
    setCount(count + 1);
    // These might trigger TWO re-renders (not batched)
    // because they happen after an async operation
  };

  return <button onClick={handleClickAsync}>Async Count: {count}</button>;
}

# Key Prop and Re-Renders

The key prop influences how React handles re-renders in lists:

typescript
interface ItemProps {
  id: number;
  value: string;
}

export function ItemList({ items }: { items: ItemProps[] }) {
  return (
    <ul>
      {items.map(item => (
        // ✅ Using unique id as key
        <li key={item.id}>
          {item.value}
        </li>
        // ❌ Don't use array index as key—it causes issues with list reordering
        // <li key={items.indexOf(item)}>{item.value}</li>
      ))}
    </ul>
  );
}

# Practical Optimization Patterns {#optimization-patterns}

# Pattern 1: Memoize Child Components

When a child component receives stable props, use React.memo to prevent unnecessary re-renders:

typescript
import { memo, useState } from 'react';

interface ExpensiveChildProps {
  name: string;
}

const ExpensiveChild = memo(function ExpensiveChild({ name }: ExpensiveChildProps) {
  console.log(`Expensive component rendered with name: ${name}`);
  // Expensive computation here
  return <div>Hello, {name}!</div>;
});

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

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment ({count})</button>
      {/* This component won't re-render unless the name prop changes */}
      <ExpensiveChild name="Alice" />
    </div>
  );
}

# Pattern 2: Move State Down

Instead of keeping state in a parent component, move it to a child:

typescript
// ❌ Anti-pattern: state in parent causes child re-renders
export function BadExample() {
  const [inputValue, setInputValue] = useState('');

  return (
    <div>
      <input
        value={inputValue}
        onChange={e => setInputValue(e.target.value)}
      />
      <ExpensiveComponent />
    </div>
  );
}

// ✅ Better: uncontrolled input or state in child
export function GoodExample() {
  return (
    <div>
      <InputField />
      <ExpensiveComponent />
    </div>
  );
}

function InputField() {
  const [inputValue, setInputValue] = useState('');
  return (
    <input
      value={inputValue}
      onChange={e => setInputValue(e.target.value)}
    />
  );
}

function ExpensiveComponent() {
  // This no longer re-renders when InputField's state changes
  return <div>Expensive content</div>;
}

# Pattern 3: Split Contexts by Update Frequency

Separate frequently-changing values from stable ones in context:

typescript
import { createContext, useContext, useState, useMemo, ReactNode } from 'react';

// Separate contexts for different update frequencies
const ThemeContext = createContext<{ isDark: boolean } | undefined>(undefined);
const ThemeActionsContext = createContext<{ toggleTheme: () => void } | undefined>(undefined);

interface ThemeProviderProps {
  children: ReactNode;
}

export function ThemeProvider({ children }: ThemeProviderProps) {
  const [isDark, setIsDark] = useState(false);

  const toggleTheme = () => {
    setIsDark(!isDark);
  };

  // Memoize to prevent unnecessary re-renders
  const themeValue = useMemo(() => ({ isDark }), [isDark]);
  const actionsValue = useMemo(() => ({ toggleTheme }), []);

  return (
    <ThemeContext.Provider value={themeValue}>
      <ThemeActionsContext.Provider value={actionsValue}>
        {children}
      </ThemeActionsContext.Provider>
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) throw new Error('useTheme must be used within ThemeProvider');
  return context;
}

export function useThemeActions() {
  const context = useContext(ThemeActionsContext);
  if (!context) throw new Error('useThemeActions must be used within ThemeProvider');
  return context;
}

# Practical Application: Building a performant form component

Let's build a real-world example: a form component that doesn't trigger unnecessary re-renders of expensive child components.

# TypeScript Version

typescript
import { useState, useCallback, memo, useMemo, ReactNode } from 'react';

interface FormData {
  username: string;
  email: string;
  message: string;
}

interface FormProps {
  onSubmit: (data: FormData) => void;
}

// This component is memoized because it's expensive to render
const PreviewPane = memo(function PreviewPane({ data }: { data: FormData }) {
  console.log('PreviewPane rendered');
  // Simulate expensive rendering
  return (
    <div>
      <h3>Preview</h3>
      <p>Username: {data.username}</p>
      <p>Email: {data.email}</p>
      <p>Message: {data.message}</p>
    </div>
  );
});

export function OptimizedForm({ onSubmit }: FormProps) {
  const [formData, setFormData] = useState<FormData>({
    username: '',
    email: '',
    message: '',
  });

  // Memoize the handler to avoid recreating it on every render
  const handleChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
      const { name, value } = e.target;
      setFormData(prev => ({
        ...prev,
        [name]: value,
      }));
    },
    []
  );

  const handleSubmit = useCallback(
    (e: React.FormEvent) => {
      e.preventDefault();
      onSubmit(formData);
    },
    [formData, onSubmit]
  );

  // Memoize formData to prevent PreviewPane from re-rendering
  // unnecessarily (though memo already handles shallow prop comparison)
  const memoizedFormData = useMemo(() => formData, [formData]);

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="username"
        value={formData.username}
        onChange={handleChange}
        placeholder="Username"
      />
      <input
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <textarea
        name="message"
        value={formData.message}
        onChange={handleChange}
        placeholder="Message"
      />
      <button type="submit">Submit</button>
      
      {/* PreviewPane only re-renders when memoizedFormData actually changes */}
      <PreviewPane data={memoizedFormData} />
    </form>
  );
}

# JavaScript Version

javascript
import { useState, useCallback, memo, useMemo } from 'react';

// This component is memoized because it's expensive to render
const PreviewPane = memo(function PreviewPane({ data }) {
  console.log('PreviewPane rendered');
  return (
    <div>
      <h3>Preview</h3>
      <p>Username: {data.username}</p>
      <p>Email: {data.email}</p>
      <p>Message: {data.message}</p>
    </div>
  );
});

export function OptimizedForm({ onSubmit }) {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    message: '',
  });

  const handleChange = useCallback((e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value,
    }));
  }, []);

  const handleSubmit = useCallback(
    (e) => {
      e.preventDefault();
      onSubmit(formData);
    },
    [formData, onSubmit]
  );

  const memoizedFormData = useMemo(() => formData, [formData]);

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="username"
        value={formData.username}
        onChange={handleChange}
        placeholder="Username"
      />
      <input
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <textarea
        name="message"
        value={formData.message}
        onChange={handleChange}
        placeholder="Message"
      />
      <button type="submit">Submit</button>
      
      <PreviewPane data={memoizedFormData} />
    </form>
  );
}

Performance note: In this example, the handleChange callback is stable across renders, so the form inputs themselves don't re-render unnecessarily. The PreviewPane is memoized and only re-renders when the form data changes. This pattern prevents the common issue of expensive child components re-rendering on every keystroke.

# FAQ

# Q: Does using React.memo always improve performance?

A: No. React.memo adds overhead—it needs to compare props on every render. Only use it when:

  • The component is actually expensive to render
  • Props change infrequently
  • The component receives objects/arrays that need memoization anyway

Blindly memoizing every component can actually hurt performance.

# Q: Why do all my components re-render when I toggle a boolean in context?

A: Because you're likely creating a new context value object on every render. Always memoize context values:

typescript
const value = useMemo(() => ({ isDark, toggleTheme }), [isDark, toggleTheme]);

Or split contexts by update frequency so consumers only subscribe to what they actually use.

# Q: Can I prevent a child component from re-rendering when its parent re-renders?

A: Yes, with three approaches:

  1. Use React.memo to memoize the child
  2. Use useCallback to keep prop references stable
  3. Move the child component definition into a parent component so it's not a child of the re-rendering component

# Q: Does updating state in an event handler always cause a re-render?

A: In React 18+, multiple state updates in the same event handler are batched into one re-render. However, if you update state after an async operation (like inside a promise), those updates may not be batched and could cause multiple re-renders.

# Q: What's the difference between a re-render and a re-mount?

A: A re-render means the component function runs again and React checks if the DOM needs updating. A re-mount means the component is completely unmounted (hooks cleaned up) and then mounted again fresh. This happens when a component's key changes or when the component is added/removed from the tree. Re-mounts are more expensive than re-renders.


Related Articles:


Still have questions? Share your thoughts in the comments below. What re-rendering patterns have caught you off-guard in your React applications?

Sponsored Content

Google AdSense Placeholder

CONTENT SLOT

Sponsored

Google AdSense Placeholder

FOOTER SLOT