Rendering in React: What Makes a Component Tick?

One of the most essential theoretical topics that can make working with React smoother is understanding the rendering process. How does React determine it’s time to update the DOM? How does re-rendering impact performance, and how can we optimize it? What happens under the hood when we decide to display a component on the page, and what role do hooks play in this? To answer these questions, we must grasp concepts such as rendering, the component lifecycle, reconciliation, side effects, and more.

Article Outline:

  1. Rendering in React
  2. Component Lifecycle
    • 2.1 Mounting a Component
    • 2.2 Updating a Component
    • 2.3 Unmounting a Component
  3. Self-Check Questions

1. Rendering in React

What does the term “rendering” mean to you? Typically, it refers to the visual representation of a computed model (rendering a scene, a 3D model, a video, etc.). Essentially, rendering is the visual result of calculations — no computations, no rendering. This seems obvious.

But in React, rendering has its own meaning. Rendering in React refers to the process of calculating what needs to be displayed. When React renders components, it doesn’t actually display them right away; instead, it calculates which elements make up the component by analyzing the code it returns.

Take, for example, this functional component:

const SomeComponent = () => {  
  return (
    <div>      
      <h1>Text of SomeComponent</h1>   
    </div>
  )  
}

Let’s assume React begins rendering this component (for now, we’ll skip the reasons that triggered this render). What happens behind the scenes?

Since SomeComponent is a function, React calls it. The function returns JSX, which React analyzes. It figures out what actual DOM elements this component contains. Understanding this, React converts the JSX into a call to React.createElement(), which returns a virtual DOM object representing the component.

So, the JSX above will be transpiled to the following function call:

const SomeComponent = React.createElement(
  'div',  // element type  
  null,   // props, which are null in this case  
  React.createElement(    
    'h1', // element type    
    null, // props, which are null in this case    
    'Text of SomeComponent' // child of the h1 element
  )
);

Thus, React generates an object-based representation of the component, which helps build the virtual DOM. This process is what we call rendering.

Rendering, however, is only half the job. What happens next? React enters the commit phase, where it inserts the component’s virtual DOM object into the real DOM. This is managed by the react-dom module. To do this efficiently, React initiates a process known as reconciliation, where it compares (or “diffs”) the current virtual DOM with the previous one, updating only the necessary parts of the real DOM.

In summary, each render triggers a chain of processes:

render → reconciliation (diffing) → commit

This is why React is so efficient in updating the DOM, but why it can also become costly if not handled properly.

2. Component Lifecycle

A component’s lifecycle is the sequence of stages it passes through from creation to removal from the DOM. The lifecycle consists of three main stages:

  • Mounting
  • Updating
  • Unmounting
2.1 Mounting a Component

Mounting is the phase where the component is first added to the DOM.

Here’s an example component:

const SomeComponent = () => {  
  const [inputValue, setInputValue] = useState(() => getInitialValue());  
  const ref = useRef(null);  

  useEffect(() => {
    // side effects (e.g., API requests, event subscriptions)
    return () => { 
      // cleanup function
    };  
  }, [inputValue]);

  const heavyComputation = useMemo(() => doHeavyStuff(inputValue), [inputValue]);

  return (
    <div>
      <label htmlFor="textInput">Label for input</label>
      <input
        id="textInput"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="Type something to update"
        ref={ref}
        autoFocus
      />
      <ChildComponent heavy={heavyComputation} />
    </div>
  );
};

Here’s a breakdown of what happens during the mounting phase:

  1. Rendering: React calls the component to analyze its JSX. It calculates the local values declared inside (like id). State hooks (useState(), useReducer()) and refs (useRef()) are initialized.
  2. Memoization: If expensive computations exist (like with useMemo()), React will memoize the result to improve performance.
  3. Commit: React inserts the newly created DOM nodes into the real DOM.
  4. Ref Assignment: After the commit, the refs are assigned their DOM nodes.
  5. Layout Effects: Any layout effects (useLayoutEffect()) are triggered after the commit, but before painting to the screen.
  6. Painting: The browser paints the updated DOM.
  7. Side Effects: Finally, React triggers any side effects (useEffect()) after a brief delay.
2.2 Updating a Component

Updating happens whenever a component’s state or props change.

  • Rerendering: React re-renders the component, recalculates state and props, and compares dependencies in hooks. Changed dependencies will trigger hook re-execution.
  • Commit: React commits changes to the DOM, updating only the affected elements.
  • Side Effects: Cleanup functions from the previous render execute, followed by the side effects of the current render.

2.3 Unmounting a Component

Unmounting occurs when the component is removed from the DOM. During this phase, React runs any necessary cleanup functions to prevent memory leaks (e.g., removing event listeners or canceling network requests).


3. Self-Check Questions

  1. What is the difference between rendering in general and rendering in React?
  2. How does React optimize performance during the rendering process?
  3. What triggers the rerendering of a React component?
  4. What are the key phases of a component’s lifecycle?
  5. How do hooks like useEffect() and useMemo() impact component rendering?