The declarativeness of React

React, a JavaScript library developed by Facebook around 2013 in response to the increasing complexity and scale of Facebook’s own product, the Facebook Ads app, has gained widespread adoption due to its innovative approach to managing code complexity and state.

At its core, React offers a powerful tool for developers: the ability to reuse code through components and a robust system for managing state. These features, coupled with React’s high level of abstraction, simplify the development process by encouraging a declarative way of building user interfaces.


React, a declarative library

What is declarative programming?

Declarative programming is a non-imperative style of programming in which programs describe their desired results without explicitly listing commands or steps that must be performed.

In the realm of React, a declarative library, this paradigm comes to life. React’s declarative nature signifies that developers need not intricately guide it through step-by-step instructions. Instead, they articulate the desired end state, allowing React to seamlessly handle the underlying processes.

So instead of saying:

When this button is clicked:

  1. Create a new div called newElement.

  2. Set the textContent of the div to “New Content”

  3. Get the parent element.

  4. Insert the div (newElement) before the first child of the parent element.

// Imperative: Step-by-step instructions
function addButtonClick() {
const newElement = document.createElement('div');
newElement.textContent = 'New Content';

const parentElement = document.getElementById('parent');
parentElement.insertBefore(newElement, parentElement.firstChild);
}

We describe (declare) the desired end result:

When the button is clicked:

The page should display with a div after the button element whose content is set to “New Content”.

// Declarative: Declare the end result
import React, { useState } from 'react';

function App() {
  const [content, setContent] = useState('');

  const handleButtonClick = () => {
    setContent('New Content');
  };

  return (
    <div>
      <button onClick={handleButtonClick}>Click me</button>
      <div>{content}</div>
    </div>
  );
}

// Render the App component
ReactDOM.render(<App />, document.getElementById('root'));

You don’t care to know how it gets the div there or how it sets the content.

This showcases the declarative nature of React, where we focus on what we want rather than the detailed steps of how to achieve it.


This suggests that React has a method for achieving this; otherwise, it would seem like magic. It’s crucial to understand that manipulating a computer inherently involves imperative actions (step-by-step instructions). However, React cleverly abstracts this imperativeness to another level.

Understanding how React accomplishes this abstraction is vital. It provides an intuitive way of thinking about React, enabling you to easily identify the reasons behind certain behaviours and debug effectively.

The process of React displaying content on the screen involves four key steps:

  1. Triggering a Render: This occurs when there is a modification in your app’s state or conditions.

  2. Rendering Components: React then determines the minimal changes needed to be applied to the DOM.

  3. Committing Changes: The identified changes are then served to the DOM.

  4. Painting the UI: Finally, the browser’s painter comes in and repaints the DOM using the updated information.

Triggering a render

This could occur due to :

  • The component’s initial render

  • A state change in the component or one of its ancestors.

  • A prop change.

Let me remind you of some terms before we go on…

The virtual DOM

The Virtual DOM (Document Object Model) is a concept in web development, especially associated with libraries like React. It’s a lightweight, in-memory representation of the actual DOM elements on a webpage. Instead of directly manipulating the real webpage elements, developers make changes to this Virtual DOM. Through a process called reconciliation, the changes are made reflect on the real DOM.

Reconciliation

During the initial render, React creates a virtual DOM representation of your entire app. On subsequent renders of your components, the following process occurs:

  1. React generates a new virtual DOM (Vn+1).

  2. React compares this new virtual DOM to the existing one (Vn), noting the minimal set of changes required to make them equal — this process is known as “Diffing.”

  3. React instructs the renderer to apply these changes to the real DOM. As a result, the real DOM, initially at the level of Vn, gets updated to Vn+1.

An illustration show reconcilation and diffing in React

Diffing Algorithm

Manipulating the real DOM is computationally expensive therefore you want to make as little changes as possible. The need for this birthed the diffing algorithm.

Diffing is the process of comparing the two virtual DOMs and figuring out the minimal set of changes that will make the former virtual DOM equal to the new virtual DOM.

Without the diffing algorithm, comparing the two virtual DOMs node for node is a lot of work. A thousand node will take a billion turns. To curb this, the algorithm makes 2 assumptions:

  • If the element is different, the tree will therefore be different. No need to compare the nodes downward, just rebuild them from scratch.

  • The developer can hint at which child elements may be stable across different renders with a *key* prop. In that case, don’t rebuild the child element.

These assumptions increases the efficiency, cutting down the computation form order of O(n3) to O(n). A thousand nodes will now take a thousand turns to compare instead of a billion turns.

Alright, let’s get back to it…

Rendering the components

Initial render ?

On the first render of your application, the following happens:

  1. Creating the Root Node: React establishes the root node and then recursively calls all the components present in the application.

  2. Constructing the Initial Virtual DOM Tree: React builds a virtual DOM tree that represents the entire application. Each node in this tree corresponds to a component, with the JSX from each component serving as the node’s content. Child components and elements become subnodes, and so on.

  3. Diffing: As this is the initial render, there is no previous tree to compare against. Therefore, React assumes that everything needs to be updated, as there’s no previous state to compare against.

  4. Commit: React instructs the renderer (e.g., ReactDOM for web) to apply these changes to the real DOM. Since this is the initial render, the entire tree representation is committed.

  5. Painting : after the rendering is completed, the browser paints the screen with the current state of the DOM.

Re-renders ?

  1. React calls the functional component whose state update triggered the render.

  2. Creating a virtual DOM tree: React creates a new virtual DOM representation for that specific component and its descendants.

  3. Diffing algorithm: The diffing algorithm then comes into play, engaging in a reconciliation process by comparing this new virtual DOM with the corresponding section in the existing virtual DOM. The goal is to identify the minimal set of changes needed to update the real DOM to reflect the new virtual DOM. This minimizes the performance impact.

  4. Commit: React instructs the renderer (e.g., ReactDOM for web) to apply these changes to the real DOM. In the case of the initial render, the entire tree representation is committed.

  5. Painting : After the rendering is completed, the browser paints the screen with the current state of the DOM.

  6. Voila, the UI is as you want it.

Conclusion

In React, developers embrace a declarative approach, where they express their intent by describing the desired end result rather than prescribing a sequence of step-by-step instructions. When triggering a render in React, whether due to a state change or an initial render, developers are essentially declaring a change they want to see in the UI. React takes this declaration and handles the imperative aspects behind the scenes, cooking up the right components during the rendering phase based on the declared changes.

As the process continues, commit and the subsequent painting of the DOM may sound imperative, involving the application of specific modifications. However, it’s crucial to recognize that React’s declarative nature remains intact throughout. Developers stay focused on declaring what they want the UI to look like, and React efficiently translates these declarations into imperative actions during the commit and painting phases.

This seamless transition from declarative expressions to imperative actions showcases the power of React’s abstraction. While it may involve steps that sound imperative, developers are shielded from the intricacies, allowing for an intuitive development experience where the focus remains on expressing what the UI should be rather than the detailed steps of how to achieve it.

1
Subscribe to my newsletter

Read articles from Opabode Abdulmujeeb directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Opabode Abdulmujeeb
Opabode Abdulmujeeb

I'm Mujeeb, a full-stack developer with a strong affinity for frontend development.