Skip to main content

Command Palette

Search for a command to run...

Web Component Sanity

Updated
5 min read
Web Component Sanity
D

Senior Tech Lead at PTC with 9+ years in full-stack development and team leadership. Experienced in Angular, React, Java, Node.js, Electron.js, Ionic, and domains like PLM, Automation, IoT, AR. Passionate about code, architecture, and exploring science & history - from micro worlds to the cosmos.

We love Web Components for their promise of reusability, but let's be real: in the wild west of modern web development, managing data flow across a sprawling landscape of components can quickly turn into a chaotic rodeo. We've all been there: tangled event listeners, data inconsistencies, and the dreaded "prop drilling" nightmare. Today, we'll explore a practical approach to wrangling this data chaos with localized, Lit-inspired stores, a pattern designed to bring sanity and interoperability to your Web Component ecosystems. But, let's also have a real talk about the potential bumps on the road, and remember that this article is primarily an exploration and insight into the Web Component world, stemming from my own experiences working and playing with various frameworks.

Context and Disclaimer : An Exploration, Not a Definitive Solution (From My Experiments)

This article is a reflection of my own experiments and explorations while working with different frameworks, and more recently, deeply diving into the world of Web Components. It's born out of the challenges I've personally faced and the solutions I've been experimenting with to address them. This is not a definitive, production-ready framework or library. Instead, it's a collection of insights and a proposed pattern that I've found helpful in my own development journey. My goal is to share these findings, spark discussion, and encourage others to explore the possibilities of Web Component development.

The Problem: The Untamed Frontier of Component Data (Real-World Scenarios)

Imagine building a simple e-commerce page with Web Components:

  • Product Display and Cart: You have a product-card component displaying product details, and a shopping-cart component showing the items added to the cart.

  • User Preferences: You want a theme-switcher component that allows users to switch between light and dark modes, affecting the entire page.

  • Form Validation: You have a checkout-form component with multiple input fields, and you need to validate the form data before submission.

Using traditional approaches, you might face these challenges:

  • Prop Drilling (Product to Cart): Passing product data from product-card to shopping-cart through multiple parent components, making your code messy.

  • Event Listener Overload (Theme Switching): Firing custom events from theme-switcher and adding event listeners to every other component, leading to complex event handling.

  • Data Inconsistencies (Form Validation): Managing form data and validation states across multiple input fields, resulting in inconsistent UI updates.

  • Performance Issues (Cart Updates): Re-rendering the entire cart component every time an item is added, leading to performance bottlenecks.

The Need: A Sherriff for Your Component Town

Think of your Web Components as a small town. Each component needs to communicate and share information with others. We need a "sheriff" (our localized store) to keep things organized and prevent chaos.

The Solution: Localized, Lit-Inspired Stores - Your Data Wranglers

class CartStore {
  constructor() {
    this._items = [];
    this._listeners = new Set();
  }

  getItems() {
    return [...this._items];
  }

  addItem(item) {
    this._items.push(item);
    this._notifyListeners();
  }

  subscribe(listener) {
    this._listeners.add(listener);
    return () => this._listeners.delete(listener);
  }

  _notifyListeners() {
    this._listeners.forEach((listener) => listener());
  }
}
import { LitElement, html } from 'lit';

class ProductCard extends LitElement {
  static properties = {
    product: { type: Object },
    cartStore: { type: Object }
  };

  render() {
    return html`
      <div>
        <h3>${this.product.name}</h3>
        <p>${this.product.price}</p>
        <button @click="${() => this.cartStore.addItem(this.product)}">Add to Cart</button>
      </div>
    `;
  }
}

class ShoppingCart extends LitElement {
  static properties = {
    cartStore: { type: Object }
  };

  connectedCallback() {
    super.connectedCallback();
    this.unsubscribe = this.cartStore.subscribe(() => this.requestUpdate());
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    this.unsubscribe();
  }

  render() {
    return html`
      <h2>Shopping Cart</h2>
      <ul>
        ${this.cartStore.getItems().map(item => html`<li>${item.name}</li>`)}
      </ul>
    `;
  }
}

customElements.define('product-card', ProductCard);
customElements.define('shopping-cart', ShoppingCart);
const cartStore = new CartStore();
const product1 = { name: 'Laptop', price: 1200 };
const product2 = { name: 'Mouse', price: 20 };

const productCard1 = document.createElement('product-card');
productCard1.product = product1;
productCard1.cartStore = cartStore;

const productCard2 = document.createElement('product-card');
productCard2.product = product2;
productCard2.cartStore = cartStore;

const shoppingCart = document.createElement('shopping-cart');
shoppingCart.cartStore = cartStore;

document.body.append(productCard1, productCard2, shoppingCart);

Why This Works

  • Shared "Information Hubs": Stores act as central "information hubs" for related components, allowing them to share data without prop drilling.

  • Automatic Updates: Components automatically update when data changes, keeping the UI in sync.

  • Clear Communication: Stores provide a clear and predictable way for components to communicate.

Alternatives and Why This Approach Stands Out

  • Lit's Reactive Properties: Great for component-level state, but less ideal for complex cross-component interactions.

  • Redux: Robust for global state, but overkill for localized component data.

  • RxJS: Powerful for asynchronous streams, but adds complexity for simple state management.

  • Context API (React): Framework-specific and not inherently reactive.

  • Signals: Framework specific and not built for web components.

This approach offers a lightweight, Web Component-native solution that combines the best of these technologies, focusing on interoperability and context awareness.

The Real Talk: Potential Challenges and Considerations

While localized stores offer many benefits, it's crucial to acknowledge potential challenges:

  • Over-Engineering: For very simple components, introducing a store might be overkill. It's essential to assess the complexity of your component and data needs.

  • Store Management Overhead: If you have many components with individual stores, managing them can become complex. It's important to establish clear patterns for store creation and sharing.

  • Performance Considerations: While localized stores can improve performance in many cases, if not implemented correctly they can create performance bottlenecks. Be careful to only update the dom when needed.

  • Debugging Complexity: Debugging data flow across multiple stores can be challenging, especially in large applications. Tools and clear logging are essential.

  • Learning Curve: Developers unfamiliar with reactive patterns might need time to grasp the concept of stores and subscriptions.

  • Testing: Testing components that use stores requires careful attention to data flow and side effects.

How to Mitigate Challenges

  • Start Small: Introduce stores gradually, focusing on components with complex data needs.

  • Establish Clear Conventions: Define clear patterns for store creation, sharing, and naming.

  • Use Debugging Tools: Leverage browser developer tools and logging to track data flow.

  • Write Thorough Tests: Implement unit and integration tests to ensure data consistency and component behavior.

  • Consider Global State: For application-wide data, consider integrating with global state management libraries like Redux or Pinia.

  • Documentation: Document your store architecture.

Conclusion

By using localized, Lit-inspired stores, we can build Web Components that are easier to manage, maintain, and understand. This pattern brings order to the data chaos and allows us to focus on building great user experiences. But, like any tool, it's important to use it wisely and be aware of potential challenges. And, remember, this is an exploration, a stepping stone in the ongoing journey of Web Component innovation, born from my own hands on experience.