People usually start with the basics of JSX and components, but before that, I want to explain why React is important.
To answer this question, I want you to understand how React works behind the scenes: the virtual DOM, shadow DOM, diffing algorithm, reconciliation, and everything else.
So lets begin…
The DOM and DOM Tree
What is the DOM?
The DOM is a programming interface for web documents. It represents the structure of a web page as a tree of objects, where each node corresponds to an element, attribute, or text in the document.
When a browser loads a web page, it parses the HTML and constructs the DOM, which allows developers to:
Access and modify content
Change the structure of the document
Add interactivity using JavaScript
The DOM Tree
The DOM Tree is a hierarchical representation of an HTML document. It starts with the document
object as the root and expands into branches representing elements, attributes, and text nodes.
Here’s an example of a simple HTML document and its corresponding DOM Tree:
<!DOCTYPE html>
<html>
<head>
<title>Sample Page</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>This is a paragraph.</p>
</body>
</html>
DOM Tree Representation:
Document
html
head
- title: "Sample Page"
body
h1: "Hello, World!"
p: "This is a paragraph."
Diagram: The DOM Tree can be visualized like this:
Document
|
|-- html
|-- head
| |-- title: "Sample Page"
|
|-- body
|-- h1: "Hello, World!"
|-- p: "This is a paragraph."
Why is the DOM Slow?
While the DOM is powerful, certain operations can be slow due to the following reasons:
1. Large DOM Trees
For pages with a high number of elements, the DOM Tree becomes larger and more complex. Traversing or modifying such a tree takes more time and resources.
2. Reflows and Repaints
Manipulating the DOM can trigger layout recalculations (reflows) and visual updates (repaints) in the browser. These processes are computationally expensive, especially if they occur frequently.
3. Synchronous Updates
DOM manipulations are often synchronous. This means the browser has to block other tasks to process updates, potentially leading to performance bottlenecks.
4. DOM Traversal
Accessing or modifying deeply nested elements requires traversing the tree, which can slow down performance for complex structures.
5. Inefficient JavaScript
Poorly written JavaScript, such as frequent DOM queries or unnecessary updates, can degrade performance. For example, repeatedly querying the DOM inside a loop is less efficient than caching the result.
So now you can see the problem too, right? Even with a basic to-do website, adding or deleting a single task causes the whole tree to update and re-render. This is where our savior, the VIRTUAL DOM, comes in.
The Virtual DOM
React uses the concept of the virtual DOM, reconciliation, and diff algorithms together to optimize the website.
What is the Virtual DOM?
The Virtual DOM is a JavaScript object that mirrors the structure of the real DOM. It allows developers to make changes to this virtual representation instead of directly interacting with the real DOM.
Here’s how it works:
Changes are made to the Virtual DOM.
The Virtual DOM calculates the difference (diff) between its previous and current states.
The minimal set of changes is applied to the real DOM.
This process is called reconciliation and ensures that updates are efficient and fast.
How the Virtual DOM Works
Step-by-Step Process:
Rendering: The UI is initially rendered using a virtual representation of the DOM.
State Updates: When a state change occurs, a new Virtual DOM tree is created.
Diffing: The framework calculates the differences between the old and new Virtual DOM trees.
Patching: The minimal set of changes is applied to the real DOM.
function App() {
const [count, setCount] = React.useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
When the button is clicked, React updates the Virtual DOM.
It compares the old and new Virtual DOM trees.
Only the
<p>
element’s text content is updated in the real DOM
Reconciliation and Render Process
Reconciliation is the process by which React updates the DOM to match the desired state of the application. When the state or props of a component change, React needs to determine what has changed in the virtual DOM and update only the necessary parts of the real DOM.
This process is powered by React’s Virtual DOM, a lightweight copy of the actual DOM. React uses the Virtual DOM to calculate the minimal set of changes required to update the UI, ensuring efficiency and performance.
The Key Phases of Reconciliation
Render Phase:
React creates a new Virtual DOM tree by calling the render methods of components.
This phase is pure and does not involve any mutations.
Commit Phase:
React compares the new Virtual DOM with the previous one using a diffing algorithm.
Necessary updates are applied to the real DOM during this phase.
Types of Reconciliation
Reconciliation in React can be classified into three major types:
1. Component Reconciliation
This involves determining whether a component needs to be updated, replaced, or removed based on changes in props or state.
Key Concepts:
Key Props: React uses keys to identify which components have changed, are added, or are removed.
Pure Components and
React.memo
: These optimize reconciliation by skipping updates if the props haven’t changed.
Example:
function Parent() {
const [count, setCount] = React.useState(0);
return (
<div>
<Child key={count} />
<button onClick={() => setCount(count + 1)}>Update Key</button>
</div>
);
}
function Child() {
console.log("Child rendered");
return <p>Child Component</p>;
}
- When the key changes, React will unmount the old component and mount a new one.
2. Element Reconciliation
React compares elements within the same level of the Virtual DOM tree to detect differences.
Key Concepts:
Elements of different types (e.g.,
<div>
vs.<span>
) result in React destroying the old node and creating a new one.Attributes and content changes within the same element type are updated directly.
Example:
function App({ isVisible }) {
return isVisible ? <h1>Hello, World!</h1> : <p>Goodbye, World!</p>;
}
- When
isVisible
changes, React will replace<h1>
with<p>
rather than updating attributes.
3. List Reconciliation
Lists present a unique challenge due to dynamic changes like additions, deletions, and rearrangements.
Key Concepts:
React’s diffing algorithm uses the
key
prop to identify which list items have changed.Without keys, React may re-render all items unnecessarily, causing performance issues.
Example:
function List({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
Incorrect Key Example:
{items.map((item, index) => (
<li key={index}>{item.name}</li>
))}
- Using
index
as a key can lead to incorrect UI updates when the order of items changes.
Diagram: Reconciliation Process
Below is a simplified representation of the reconciliation process:
Old Virtual DOM New Virtual DOM Diffing Algorithm Real DOM
<div> <div> |
<p>A</p> <p>B</p> | Changes Detected -> Apply to Real DOM
</div> </div>
There might be many things you didn't understand, like useState
, but don't worry, I will cover this later.
We learned about the DOM and Virtual DOM, and now it's time for the Shadow DOM.
Shadow DOM
The Shadow DOM is a sub-tree of DOM elements that is isolated from the main DOM tree. It provides encapsulation by allowing a component to maintain its own set of styles and structure, which are not affected by the global styles or DOM structure of the application.
Think of the Shadow DOM as a private world for a web component. It ensures that the internal implementation of the component does not accidentally interfere with the rest of the document.
How Shadow DOM Works
Creating a Shadow Root
A shadow root is the starting point of the Shadow DOM. To create a shadow root, use the attachShadow()
method on a host element.
<div id="my-component"></div>
const host = document.getElementById('my-component');
const shadowRoot = host.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
p {
color: blue;
}
</style>
<p>Hello from Shadow DOM!</p>
`;
In this example:
A shadow root is attached to the
<div>
withid="my-component"
.The Shadow DOM has its own styles and content, which are encapsulated.
Modes of Shadow DOM
The attachShadow
method accepts an options object with a mode
property:
open
: The shadow root is accessible viaelement.shadowRoot
.closed
: The shadow root is hidden and cannot be accessed directly.
const shadowRoot = host.attachShadow({ mode: 'closed' });
console.log(host.shadowRoot); // null
Diagram: Shadow DOM Encapsulation
Here’s a visual representation of how the Shadow DOM works:
Main DOM Tree:
<html>
<body>
<div id="my-component"> <-- Shadow Host
#shadow-root (open) <-- Shadow Root
<style>
p { color: blue; }
</style>
<p>Hello from Shadow DOM!</p>
</div>
</body>
</html>
Key Benefits of Shadow DOM
Encapsulation: Styles and scripts in the Shadow DOM do not leak out to the main DOM, and vice versa.
Modularity: Developers can create self-contained components with predictable behavior.
Reusability: Components can be reused across projects without worrying about style conflicts.
Simplified CSS Management: No need for complicated class naming conventions to avoid conflicts.
Summary Table: DOM, Virtual DOM, and Shadow DOM
Feature | DOM (Document Object Model) | Virtual DOM | Shadow DOM |
Definition | Represents the entire structure of a web document as a tree. | A lightweight copy of the real DOM used for efficient updates. | A web standard to encapsulate elements and their styles, creating isolated DOM trees. |
Purpose | Used to manipulate the structure, content, and style of web pages. | Improves performance by minimizing direct DOM updates. | Provides style and behavior encapsulation for web components. |
Properties | - Reflects the actual structure of the document. | ||
- Changes directly affect the UI. | |||
- Relatively slower due to frequent reflows and repaints. | - Does not directly update the DOM. | ||
- Uses a diffing algorithm to determine changes. | |||
- Efficient updates. | - Encapsulation: Styles and DOM changes do not leak outside. | ||
- Avoids style conflicts. | |||
- Requires support for web components. | |||
When to Use | Use for basic web pages or when direct DOM manipulation suffices. | Use in frameworks like React to optimize rendering performance. | Use for creating reusable, self-contained components with isolated styles and behavior. |
Advantages | - Full control over the document. | ||
- Browser-native. | |||
- Simple to use for small-scale applications. | - Faster updates as it calculates the minimal changes required. | ||
- Avoids unnecessary reflows/repaints. | |||
- Improves performance in dynamic applications. | - Ensures style encapsulation. | ||
- Reduces the risk of CSS conflicts. | |||
- Enhances component reusability. | |||
- Improves maintainability in large projects. | |||
Disadvantages | - Slower for frequent updates in dynamic UIs. | ||
- Can lead to performance issues with large documents. | - Added complexity due to abstraction. | ||
- Requires a library/framework like React. | |||
- Debugging can be harder. | - Browser compatibility can be an issue in older versions. | ||
- Slightly increased learning curve. | |||
- Requires extra work for polyfills or support in some cases. | |||
Use Case Examples | - Manipulating HTML directly using JavaScript. | ||
- Creating simple static websites. | - React, Vue, or other frameworks where frequent updates are required. | ||
- Applications with high interactivity like single-page applications (SPAs). | - Web components like <my-custom-button> . | ||
- Widgets or custom elements needing encapsulated styles. | |||
- Frameworks like Lit or Stencil. | |||
Performance | - Slower as every change triggers reflows/repaints. | - Highly optimized for rendering changes. | |
- Uses a reconciliation algorithm. | - Neutral performance impact but ensures style/DOM encapsulation. | ||
Abstraction Level | Low – Directly represents the document's structure. | High – Abstracts away direct DOM manipulations. | Medium – Encapsulates part of the DOM and its styles. |
Support | Supported in all browsers natively. | Requires a library or framework like React or Vue. | Native support in modern browsers (partial support in older ones). |
Why It's Used | - Direct manipulation of web pages. | ||
- Works for simple or static pages. | - Optimized rendering for dynamic UIs. | ||
- Minimizes performance bottlenecks. | - To create self-contained, reusable components. | ||
- Avoid style leaks or conflicts. |
Summary:
DOM is suitable for basic applications or when you need direct control of the document.
Virtual DOM is ideal for performance-intensive, dynamic applications, mainly with frameworks like React.
Shadow DOM is best for encapsulating components, especially in modern web development with reusable elements.
React Components: Functional vs Class Components
React is a powerful JavaScript library used to build interactive user interfaces (UIs) by breaking down the UI into smaller, reusable pieces. These pieces are called components. React components form the fundamental building blocks of any React application.
What are React Components?
A React component is a self-contained unit of code that represents a part of the user interface. Each component has its own state and can accept inputs (called props) to render dynamic content. Components can be as simple as displaying text or as complex as managing form data, handling user input, or interacting with APIs.
Types of React Components
There are two primary types of React components:
Functional Components
Class Components
Each type has its own way of handling rendering, managing state, and reacting to events. Let’s break down the differences between them.
1. Functional Components
What are Functional Components?
A Functional Component is a JavaScript function that returns JSX (JavaScript XML), which is a syntax extension that looks similar to HTML. This function accepts props
as arguments and can render the UI based on them.
Creating a Functional Component
Here’s a simple example of a functional component:
import React from 'react';
function Greeting(props) {
return <h1>Hello, {props.name}!</h1>;
}
export default Greeting;
In this example, Greeting
is a functional component that takes props
as an argument and renders a personalized greeting message.
Rendering Functional Components
Functional components are rendered based on the data passed to them through props. React will call the functional component every time it needs to update the UI.
For example, if the Greeting
component is used like this:
<Greeting name="Vaishnavi" />
React will invoke the Greeting
function and render the JSX with the name
prop, outputting:
Hello, Vaishnavi!
Managing State in Functional Components
Initially, functional components were stateless, meaning they couldn’t hold or manage internal state. However, with the introduction of React Hooks (such as useState
), functional components can now manage state just like class components.
Here’s an example of a functional component with state:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
export default Counter;
In this example, the useState
hook manages the count
state, which is updated whenever the button is clicked.
2. Class Components
What are Class Components?
A Class Component is a JavaScript class that extends React.Component
. Class components can hold internal state and have lifecycle methods, allowing them to manage side effects and interact with the component lifecycle.
Creating a Class Component
Here’s an example of a class component:
import React, { Component } from 'react';
class Greeting extends Component {
render() {
return <h1>Hello, {this.props.name}!</h1>;
}
}
export default Greeting;
In this case, the Greeting
component is created using a class that extends React.Component
. It renders the name
prop in a <h1>
tag just like a functional component.
Rendering Class Components
Class components render based on the props
they receive. The rendering process is managed inside the render()
method, which returns the JSX. Whenever React needs to re-render the component, it calls the render()
method again.
Class components also receive props like functional components. If the Greeting
component is used like this:
<Greeting name="Vaishnavi" />
React will invoke the render()
method and output:
Hello, Vaishnavi!
Managing State in Class Components
Class components have a special state
object where they can store data. The state is initialized in the constructor and updated using the setState
method.
Here’s an example of how to manage state in a class component:
import React, { Component } from 'react';
class Counter extends Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
export default Counter;
In this example, the state is initialized in the constructor and updated by calling setState
. When the state is updated, React re-renders the component.
Comparing Functional and Class Components
Feature | Functional Components | Class Components |
Syntax | Simple function-based | Class-based with render method |
State Management | Managed with hooks (useState ) | Managed with this.state and setState |
Lifecycle Methods | No lifecycle methods (until React 16.8) | Full access to lifecycle methods (e.g., componentDidMount ) |
Hooks | Can use hooks (useEffect , useState , etc.) | Cannot use hooks directly (but can use HOCs) |
Performance | Slightly better in some cases due to simpler syntax | Slightly slower due to the overhead of class methods |
Why Use Functional Components?
Simplicity: Functional components are easier to read and maintain due to their simple syntax.
Hooks: With the introduction of hooks, functional components now have access to the same features (state, lifecycle methods, etc.) as class components.
Performance: Functional components are often more performant because they have less overhead compared to class components.
Why Use Class Components?
Legacy Code: Older React codebases use class components extensively, so maintaining them may be necessary.
Lifecycle Methods: Before hooks, class components were the only way to access lifecycle methods like
componentDidMount
.
How React Components are Rendered and Updated
Rendering
The process of rendering a React component involves React invoking the component function or class, executing its code, and converting the JSX into real HTML elements. React does this in response to changes in the component's state or props.
When a component’s state or props change, React triggers a re-render of that component. This is how React ensures the UI stays in sync with the underlying data.
Updating
Components update when:
Their state changes (using
setState
in class components oruseState
in functional components).Their props change (i.e., when parent components pass new values to child components).
When a component’s state or props change, React compares the new virtual DOM with the previous one using a process called reconciliation. It then updates the real DOM with only the changes (i.e., the diff).
Understanding JSX (JavaScript XML) in React
JSX, or JavaScript XML, is a syntax extension for JavaScript commonly used with React. It allows developers to write HTML-like code directly within JavaScript, which makes the creation of React components more intuitive and declarative.
What is JSX?
JSX is a syntax extension that looks similar to HTML but is written in JavaScript. It enables you to write components in a way that visually resembles HTML, while still leveraging the power and flexibility of JavaScript. JSX lets you structure UI elements, set attributes, and embed dynamic data within the markup in a concise and expressive way.
Basic JSX Syntax
Here's an example of JSX:
const element = <h1>Hello, World!</h1>;
This looks very similar to HTML, but it’s actually a JavaScript expression. In this case, element
is a JSX expression representing an h1
tag with the text "Hello, World!".
Embedding Expressions in JSX
JSX allows you to embed JavaScript expressions inside curly braces {}
. You can include variables, function calls, or any other valid JavaScript expression within these braces.
const name = 'Vaishnavi';
const element = <h1>Hello, {name}!</h1>;
In this example, {name}
will be replaced with the value of the name
variable, so the rendered output will be:
Hello, Vaishnavi!
JSX in React Components
JSX is often used to define the structure of React components. Here’s an example of how JSX is used in a functional component:
function Greeting(props) {
return <h1>Hello, {props.name}!</h1>;
}
This component takes props
as an argument and renders a personalized greeting.
JSX Elements are JavaScript Objects
Under the hood, JSX elements are not HTML elements but JavaScript objects. When you write JSX, you're creating React elements, which are lightweight representations of DOM elements. React then uses these elements to update the real DOM efficiently.
For example, the JSX code:
const element = <h1>Hello, World!</h1>;
Gets compiled into a JavaScript object like this:
const element = React.createElement('h1', null, 'Hello, World!');
This React.createElement
method returns a React element that React can use to render the actual DOM.
How JSX is Compiled into JavaScript
JSX is not valid JavaScript by itself, so it needs to be compiled (transformed) into regular JavaScript code. This compilation process is usually handled by a tool like Babel, which is a JavaScript compiler. Babel converts JSX code into React.createElement
calls, which React can then process.
Babel Compilation Example
Consider the following JSX:
const element = <h1>Hello, World!</h1>;
Babel compiles this into:
const element = React.createElement('h1', null, 'Hello, World!');
Babel transforms the JSX into a React.createElement
function call, which returns a plain JavaScript object that React can use to manage the virtual DOM.
Why is Compilation Necessary?
JSX allows us to write code in a more human-readable form, resembling HTML, but browsers cannot natively understand it. JavaScript engines do not recognize JSX syntax as valid JavaScript. Therefore, the code needs to be compiled into standard JavaScript that the browser can execute, which is where tools like Babel come in. This also helps with features like compatibility across different browsers and JavaScript versions.
Benefits of JSX
1. Readability and Declarative Syntax
JSX makes React code more readable and declarative by allowing developers to write UI structure directly in JavaScript. The HTML-like syntax makes it easy to visualize what the UI will look like while still allowing the flexibility of JavaScript to control dynamic behavior.
For example, instead of writing:
const element = React.createElement('h1', null, 'Hello, World!');
With JSX, we can write:
const element = <h1>Hello, World!</h1>;
The latter is simpler and more readable, especially for developers familiar with HTML.
2. Declarative Nature
JSX helps in creating declarative UI. Instead of manually manipulating the DOM, JSX allows developers to describe the desired UI based on state and props, and React will take care of rendering and updating the UI. The declarative approach is less error-prone and leads to cleaner, easier-to-maintain code.
For example, consider this JSX:
function Welcome(props) {
return <h1>Hello, {props.name}!</h1>;
}
In this function, we're declaratively describing what the UI should look like based on the props.name
. React will update the DOM whenever props.name
changes, without the developer having to manually intervene.
3. Integration with JavaScript
JSX allows JavaScript to be integrated seamlessly with the UI. You can embed expressions, call functions, use variables, and even handle events directly within the JSX code.
For example:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Current count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
In this functional component, JSX is used to render the current value of count
and a button that updates it when clicked. The event handler is embedded directly in the JSX, making the code easy to follow and understand.
Common JSX Rules and Best Practices
One Parent Element: Every JSX expression must return a single parent element. If you need to return multiple elements, wrap them in a
div
or a fragment (<></>
).return ( <div> <h1>Hello</h1> <p>Welcome to React</p> </div> );
Or using a fragment:
return ( <> <h1>Hello</h1> <p>Welcome to React</p> </> );
CamelCase for Attributes: JSX uses camelCase for attribute names, unlike HTML, where attributes are typically lowercase. For example,
class
in HTML becomesclassName
in JSX, andfor
becomeshtmlFor
.<input type="text" className="input-field" />
JS Expressions in Curly Braces: Any JavaScript expression can be embedded in JSX using curly braces
{}
. However, statements likeif
orfor
loops cannot be directly used in JSX.const name = 'Vaishnavi'; return <h1>Hello, {name}!</h1>;
This article explains why React is great by exploring the DOM, Virtual DOM, and Shadow DOM, and how they improve speed and organization. It covers how React uses the Virtual DOM for faster rendering through reconciliation. You'll learn about functional vs. class components, focusing on syntax, state, and rendering. It also discusses JSX—its syntax, integration with JavaScript, and benefits for creating clear UIs. Overall, it provides a concise overview of React's key features and their importance in modern web development.