in

Demystifying React‘s Rendering Behavior: An In-Depth Practical Guide

Understanding how React renders our applications is crucial for any React developer. Yet it‘s also one of the most perplexing topics for React newbies and even experienced developers.

In this comprehensive guide, you‘ll learn how React‘s rendering really works behind the scenes. You‘ll understand when and why re-rendering occurs, how to optimize performance, and gain key insights from real-world examples.

By the end, you‘ll have the expertise to build blazing fast React apps that render efficiently. Sound exciting? Let‘s dive in!

What is "Rendering" in React?

Before jumping into React rendering, let‘s first build a solid mental model of what "rendering" really means.

The Virtual DOM – React‘s Secret Sauce

React uses a virtual representation of the actual DOM called the virtual DOM. Instead of manipulating the actual DOM directly, React generates a virtual DOM made up of React elements.

These React elements are just plain JavaScript objects that describe what the DOM should look like.

For example, this JSX:

<div className="app">

  <p>This is some text</p>
</div>

Gets converted to:

{
  type: ‘div‘,
  props: {
    className: ‘app‘,
    children: [
      {
        type: ‘h1‘,
        props: {
          children: ‘Hello World‘
        }
      },
      {
        type: ‘p‘,
        props: {
          children: ‘This is some text‘  
        }
      }
    ]
  } 
}

So the virtual DOM is a tree of these React element objects. This allows React to do comparisons on these objects to determine what needs to change in the real DOM.

This innovation is what enables React‘s performance since the virtual DOM can update efficiently without touching the real DOM.

The virtual DOM is like a virtual representation of the UI that React can update quickly without modifying the actual DOM.

What Does "Rendering" Refer to?

In React, rendering refers to the process of generating the virtual DOM for a component tree.

More specifically, rendering in React involves:

  • Whenever a component gets initialized, React will "render" it – meaning it generates the virtual DOM React elements representing its UI
  • This rendering process is recursive, so React will render the component‘s children also
  • Once the virtual DOM has been created, React can now diff to determine necessary DOM updates

So in summary:

  • Rendering ≠ updating the real DOM. It refers to building up the virtual DOM representation.
  • Components get re-rendered whenever their state changes.
  • Re-rendering generates a new virtual DOM tree.
  • After re-rendering, React compares the new virtual DOM with the previous one.

Rendering creates the virtual DOM representation of your React components.

Now that we understand what rendering is, let‘s go over when re-rendering happens.

When Does Re-rendering Occur in React?

Re-rendering is a normal part of React. But excessive re-rendering can hurt performance, so it‘s vital to understand what causes it.

Let‘s go over the common cases where components re-render unnecessarily.

1. Parent Components Re-rendering

By default, whenever a parent component re-renders, React will re-render all its child components recursively.

function Parent() {
  // re-renders on some state change
}

function Child() {
  // re-renders even though no changes 
}

// Parent renders
// Child renders even though unchanged
<Parent>
  <Child /> 
</Parent>

This leads to a lot of unnecessary re-rendering. A small state change in Parent forces Child to re-render even if its props are unchanged.

In fact, amajor performance problem in large React apps is unnecessary re-rendering due to parent updates.

Parent re-rendering causes child components to re-render unnecessarily.

2. Changes in Context Values

When using React Context, components re-render whenever the context value changes:

const ThemeContext = React.createContext(‘dark‘)

function App() {
  return (
    <ThemeContext.Provider value={‘light‘}>
      <Page />
    </ThemeContext.Provider>
  )
}

function Page() {
  // re-renders even though it doesn‘t read ThemeContext
}

Here, Page will re-render even if it doesn‘t actually use the theme context value.

Context changes can force deep re-rendering even if the component doesn‘t consume that context.

Context value changes lead to re-renders of consumer components.

3. Hooks Like useEffect

Hooks like useEffect cause a component to re-render after executing:

function MyComponent() {
  useEffect(() => {
    // do something
  })

  return <div>Hi</div> 
}

Here, MyComponent will re-render after the effect runs because effects are cleaned up and re-run after re-rendering.

While sometimes necessary, re-rendering due to effects can also happen unnecessarily.

The useEffect hook causes components to re-render after execution.

There are also other cases like useState and useReducer re-rendering components when updater functions run. But in general, the main sources of excessive re-rendering boil down to parents, contexts, and hooks.

Real-World Examples of Wasteful Re-rendering

To make this more concrete, let‘s go through some real-world examples of unnecessary re-rendering.

Scenario 1: Re-rendering Long Lists

Here is a component that renders a long list of items:

function ItemList() {
  // fetch list of items
  const [items] = useState(fetchItems()) 

  return (
    <ul>
      {items.map(item => <Item key={item.id} item={item} />)}  
    </ul>
  )
}

This seems fine. But now let‘s say somewhere up the component tree, there‘s a context update:

function MainLayout() {

  const [theme, setTheme] = useState(‘dark‘)

  return (
    <ThemeContext.Provider value={theme}>
       <ItemList />
    </ThemeContext.Provider>
  )

}

Although the ItemList doesn‘t read the ThemeContext, this context update will still cause ItemList and all its Item children to re-render!

Even though the list is unchanged, React will re-render potentially hundreds of items. This can severely affect performance with long lists.

Context updates can re-render huge component subtrees even if context isn‘t consumed.

Scenario 2: Re-rendering Expensive Calculations

Another common example is re-computing expensive values needlessly:

function MyComponent({ data }) {

  const [expensiveValue, setExpensiveValue] = useState(() => {
    return heavyCalculation(data)
  })

  // ...

}

Here, every time MyComponent renders, we re-compute expensiveValue via an expensive calculation.

This is wasteful if data hasn‘t changed. But React will still call heavyCalculation on each render by default.

Expensive calculations can get re-run unnecessarily on every render.

These are just a couple common real-world examples of how excessive re-rendering affects performance. The key takeaway is that re-rendering can happen frequently without us realizing it.

But there are patterns we can use to optimize this.

Optimizing Rendering Performance in React Apps

Let‘s now go over actionable strategies to optimize rendering performance.

1. React.memo for Component Memoization

React.memo allows us to easily memoize function components:

const MyComponent = React.memo(function MyComponent(props) {
  // only rerenders if props change
})

React.memo does a shallow comparison on props to determine if a re-render is needed.

This can prevent unnecessary re-renders of UI components like ListItems whose rendering depends solely on props.

React.memo memoizes components to prevent unnecessary re-renders.

However, React.memo has a couple caveats:

  • It only does a shallow comparison on props. For deep comparisons, use useMemo
  • It still renders when parent components render. To fix this, also use useMemo.

2. useMemo for Expensive Calculations

useMemo allows us to memoize expensive calculations:

const value = useMemo(() => heavyComputation(a, b), [a, b])

This ensures heavyComputation is only re-run if a or b change.

useMemo takes a function and an array of inputs. The computation is memoized and only re-runs when the inputs change.

useMemo prevents expensive computations from re-running unnecessarily.

3. useCallback to Memoize Callback Functions

Similarly, useCallback memoizes callback functions between renders:

const memoizedHandleClick = useCallback(
  () => {
    // handle click
  },
  []  
)

Now memoizedHandleClick will not change reference between re-renders.

This avoids passing callbacks down to optimized child components like React.memo unnecessarily.

useCallback memoizes callback functions to prevent passing new function references unnecessarily.

4. Virtualize Long Lists with react-window

As we saw earlier, large lists can force components to re-render unnecessarily.

Virtualization libraries like react-window solve this by only rendering visible items:

function VirtualizedList() {
  // only renders visible rows
  return (
    <List
      height={600}
      itemCount={10000}
      itemSize={50}
    >
      {Item}
    </List>
  )
}

This ensures long lists only render what‘s visible.

Virtualization renders only visible data to optimize long lists.

5. useReducer for State Composition

useReducer can help optimize re-rendering by managing state in reducers:

function reducer(state, action) {
  switch(action.type) {
    // update counters
    case ‘increment‘: 
      return {
        ...state,
        count: state.count + 1
      }
    // don‘t update state  
    case ‘setTheme‘:
      return state
    default:  
      return state
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, initialState)  

  const { count, theme } = state

  // increment will only update count  
  const increment = () => dispatch({ type: ‘increment‘ }) 

  // set theme only updates theme
  const setTheme = (theme) => dispatch({ type: ‘setTheme‘, theme })

  return /* ... */
}

Here, components that depend on count won‘t re-render if theme changes since state is separated.

useReducer allows optimizing state updates with reducers.

6. useIsomorphicLayoutEffect to Avoid Re-rendering Effects

The useLayoutEffect hook used for DOM mutations unfortunately always causes a re-render.

But the less known useIsomorphicLayoutEffect hook avoids this:

function MyComponent() {
  useIsomorphicLayoutEffect(() => {
    // mutates DOM
  })

  return <div>Hi</div>
} 

useIsomorphicLayoutEffect acts like useLayoutEffect but doesn‘t force a render after running.

This prevents unnecessary re-rendering when using effects for DOM mutations.

useIsomorphicLayoutEffect prevents re-rendering after DOM mutations.

These are some of the powerful techniques for optimizing rendering performance.

Putting it All Together: A Case Study

Let‘s now see how we can apply these patterns to optimize a real-world app.

Imagine we have a music playlist dashboard that displays:

  • A list of playlists
  • The songs in the current playlist
  • The currently playing song

When the play button is clicked, the current song changes.

We want to avoid unnecessary re-rendering as interactions occur.

First, we can React.memo our SongList and SongItem components since they are reusable:

const SongList = ({ songs }) => {
  return (
    <ul>
      {songs.map(song => <SongItem song={song} />)} 
    </ul>
  )
}

export const SongItem = React.memo(function SongItem({ song }) {
  return <li>{song.title}</li>
})

Next, we can useMemo the current song since it can be derived from state:

function Playlist() {

  const [songs] = useState(fetchSongs())
  const [currentSongIndex, setCurrentSongIndex] = useState(0)

  const currentSong = useMemo(() => {
    return songs[currentSongIndex]  
  }, [currentSongIndex, songs])  

  // ...

}

This prevents re-computing currentSong unnecessarily.

We can also extract the dispatch logic into a useReducer reducer:

function playlistReducer(state, action) {
  switch(action.type) {
    case ‘PLAY_NEXT‘: {
      return {
        ...state,
        currentSongIndex: state.currentSongIndex + 1
      }
    }
    default:
      return state
  }
}

function Playlist() {

  const [state, dispatch] = useReducer(playlistReducer, {  
    songs: [],
    currentSongIndex: 0
  })

  const {currentSongIndex, songs} = state

  const handlePlay = () => {
    dispatch({ type: ‘PLAY_NEXT‘ })
  }

  // ...

}

This ensures only currentSongIndex updates when the play button is clicked, avoiding re-rendering the rest of the UI.

Finally, we can wrap our Playlist in React.memo as well:

export const Playlist = React.memo(Playlist)  

Now only necessary components re-render as interactions occur.

By following patterns like:

  1. Memoization with React.memo and hooks like useMemo
  2. Optimizing state with useReducer
  3. Virtualization of long lists

We can optimize rendering performance.

Key Takeaways on Optimizing Rendering

Here are the big ideas:

  • Rendering refers to React generating the virtual DOM representation of components
  • Excessive re-rendering is caused by parent updates, context changes, hooks, and more
  • Techniques like React.memo, useMemo, useCallback memoize components and data
  • useReducer and state composition optimize which state changes cause re-renders
  • react-window virtualizes long lists to prevent re-rendering offscreen items
  • Tools like React Profiler provide low-level insights into component rendering

While it takes time to master, understanding React rendering will level up your ability to build high-performance React applications.

Wrapping Up

We covered a lot of ground here! Here are the key takeaways:

  • Rendering refers to React generating the virtual DOM representation of your components. It does not mean updating the real DOM.

  • Excessive re-rendering is often caused by parent component updates, context value changes, hooks scheduling updates, and more.

  • Techniques like React.memo, useMemo, and useCallback can optimize performance by memoizing data to prevent unnecessary re-renders.

  • Virtualizing long lists prevents offscreen items from rendering.

  • The React Profiler provides low-level insights into component rendering performance.

  • Applying patterns like memoization, reducer composition, and virtualization can optimize real-world apps.

I hope this guide gave you a deep understanding into React rendering behavior and patterns for optimization. Mastering these concepts takes time but will give you the skills to build high-performance React apps that render efficiently.

Let me know if you have any other questions! I‘m always happy to chat more about React performance.

AlexisKestler

Written by Alexis Kestler

A female web designer and programmer - Now is a 36-year IT professional with over 15 years of experience living in NorCal. I enjoy keeping my feet wet in the world of technology through reading, working, and researching topics that pique my interest.