Component & UI

React vs Web Components

Compare React and Web Components across component models, style scoping, state management, and lifecycle. Understand strengths and trade-offs with side-by-side code examples.

Comparison Overview

Aspect React Web Components
Component Definition Function returning JSX Class extending HTMLElement
Style Scoping CSS Modules / CSS-in-JS Shadow DOM (browser-native)
State Management useState / useReducer Attributes + Properties + manual update
Rendering Virtual DOM (diffing) Native DOM (direct manipulation)
Lifecycle useEffect (sync model) connectedCallback (callback model)
Ecosystem React-specific libraries Framework-agnostic
Naming Rule Starts with uppercase (MyButton) Hyphen required (my-button)

Runtime & Bundle Size

React
~100 KB
react + react-dom (gzipped, v19)

Runtime required. Includes virtual DOM engine, event system, and hooks mechanism. Framework code is added to the bundle.

Web Components
0 KB
Built into browsers

No additional runtime needed. Custom Elements, Shadow DOM, and HTML Templates are all built into browsers. Only your component code is bundled.

Component Definition

React
// A function IS the component
function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// Usage
<Greeting name="Alice" />
Web Components
// Define a class and register with browser
class GreetingElement extends HTMLElement {
  connectedCallback() {
    const name = this.getAttribute('name');
    this.innerHTML = `<h1>Hello, ${name}!</h1>`;
  }
}
customElements.define('x-greeting', GreetingElement);

// Usage
<x-greeting name="Alice"></x-greeting>

In React, any function returning JSX becomes a component. Web Components require defining a class and registering it with the browser. React component names must start with an uppercase letter, while Web Components element names must contain a hyphen.

Style Scoping

React (CSS Modules)
/* Card.module.css */
.card { border: 1px solid #ddd; padding: 1rem; }
.title { font-weight: bold; }

/* Card.jsx */
import styles from './Card.module.css';

function Card({ title, children }) {
  return (
    <div className={styles.card}>
      <h2 className={styles.title}>{title}</h2>
      {children}
    </div>
  );
}
Tool-dependent

Build tools make class names unique (e.g., Card_card__x7f2a) to achieve scoping. The browser itself is not involved.

Web Components (Shadow DOM)
class MyCard extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        .card { border: 1px solid #ddd; padding: 1rem; }
        .title { font-weight: bold; }
      </style>
      <div class="card">
        <h2 class="title"><slot name="title"></slot></h2>
        <slot></slot>
      </div>
    `;
  }
}
customElements.define('my-card', MyCard);
Browser-native

Shadow DOM completely isolates styles. No build tools needed. .card only applies within the Shadow DOM.

React's CSS Modules achieve scoping by making class names unique at build time (e.g., Card_card__x7f2a). Web Components' Shadow DOM uses a browser-native mechanism for complete style isolation. Both serve the same purpose — preventing style leakage — but the implementation approaches are fundamentally different.

State Management & Reactivity

React
import { useState } from 'react';

function Counter() {
  // Declare state → auto re-render on change
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked: {count}
    </button>
  );
}
Web Components
class MyCounter extends HTMLElement {
  #count = 0;

  connectedCallback() {
    this.render();
    // Manually register event listener
    this.addEventListener('click', () => {
      this.#count++;
      this.render(); // Manually update DOM
    });
  }

  render() {
    this.innerHTML =
      `<button>Clicked: ${this.#count}</button>`;
  }
}
customElements.define('my-counter', MyCounter);

React declares state with useState and automatically re-renders on state changes. Developers simply declare "recalculate the UI when state changes" and React handles DOM updates automatically. With Web Components, both state change detection and DOM updates must be handled manually.

Lifecycle

React
1 Render Phase Pure, no side effects

Execute the function component, return JSX. Generate virtual DOM and diff against previous.

2 Commit Phase DOM update

Update actual DOM based on diff.

useLayoutEffect Synchronous, after DOM update, before paint
▼ Browser paint
useEffect Async, after paint (primary)
3 Update Cycle setState / props

When state or props change, restart from render phase. Cleanup previous effect → run new effect.

4 Unmount

Run cleanup functions for all effects.

useState Hold & update state
useEffect Side effects (API calls, etc.)
useMemo Memoize computed values
useRef Persist values without re-render
useCallback Memoize functions
useContext Subscribe to context
Web Components
1 constructor()

Create element. Call super(), create Shadow DOM, set initial state. Attributes and children are not yet accessible.

2 connectedCallback()

When added to DOM. Render content, add event listeners, fetch data. May be called multiple times (when element is moved).

3 attributeChangedCallback()

When an observed attribute changes. Receives name, oldValue, newValue. Equivalent to React props changes, but requires manual DOM updates.

4 disconnectedCallback()

When removed from DOM. Remove event listeners, clear timers, release resources. Equivalent to React's cleanup function.

React has a multi-layered system with virtual DOM diffing, pre/post-paint hooks, and memoization. Web Components, in contrast, have just four simple callbacks. React is more complex but provides richer performance optimization tools. With Web Components, since you directly manipulate the DOM, optimization is the developer's responsibility.

When to Choose Which

React is better for

  • SPAs with complex state management
  • Leveraging the React ecosystem (Next.js, Remix, etc.)
  • Team is proficient in React
  • Efficient diffing updates via Virtual DOM are needed

Web Components is better for

  • Framework-agnostic design systems
  • Shared UI across React, Vue, and Svelte
  • Usage in CMS or static HTML
  • Micro frontends
Can be combined

You can use Web Components inside React apps, or mount React inside Web Components. Building design systems as Web Components and letting teams use their preferred framework is an increasingly common pattern.

Props vs Attributes

React Props can pass any JavaScript value (objects, arrays, functions). Web Components HTML attributes are strings only. Complex data must be passed via properties.

React
// Any value can be passed as Props
<UserList
  users={[{ name: "Alice" }, { name: "Bob" }]}
  onSelect={(user) => console.log(user)}
  config={{ theme: "dark", limit: 10 }}
/>
Web Components
<!-- Attributes are strings only -->
<user-list theme="dark" limit="10"></user-list>

<script>
// Complex data via properties
const el = document.querySelector('user-list');
el.users = [{ name: "Alice" }, { name: "Bob" }];
el.onSelect = (user) => console.log(user);
</script>
⚠️

When using Web Components from React, React 19+ automatically sets properties. React 18 and earlier require manual property setting via ref.

Event Handling

React uses SyntheticEvents to abstract browser differences. Web Components use native CustomEvents, with event propagation across Shadow DOM boundaries requiring attention.

React
function SearchBox({ onSearch }) {
  const [query, setQuery] = useState('');

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      onSearch(query);  // Callback Props
    }}>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
    </form>
  );
}

// Usage: pass function directly
<SearchBox onSearch={(q) => fetch(`/api?q=${q}`)} />
Web Components
class SearchBox extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `<form><input type="text"/></form>`;

    shadow.querySelector('form')
      .addEventListener('submit', (e) => {
        e.preventDefault();
        const query = shadow.querySelector('input').value;
        // Notify outside via CustomEvent
        this.dispatchEvent(new CustomEvent('search', {
          detail: { query },
          bubbles: true,     // Propagate to parents
          composed: true     // Cross Shadow DOM boundary
        }));
    });
  }
}

// Usage: subscribe with addEventListener
document.querySelector('search-box')
  .addEventListener('search', (e) => {
    fetch(`/api?q=${e.detail.query}`);
  });
composed: true — Required to propagate events across Shadow DOM boundaries. React doesn't have this concept.