Solving Reactivity Challenges: Native Web Components in Vue
Solving Reactivity Challenges: Native Web Components in Vue

Integrating Native Web Components in Vue: Why Props Aren’t Reactive

Learn to fix reactivity issues when integrating native web components with Vue by properly binding props and attributes.6 min


Integrating native web components into Vue applications opens up intriguing possibilities for developers. Still, it’s not always a seamless journey. One particularly tricky issue developers often encounter is the lack of reactivity when passing props to native web components. If you’ve run into this problem, you’re not alone. Let’s untangle why this happens and explore some effective solutions to enhance functionality.

Understanding the Code Structure Behind the Web Component

Before diving into why props aren’t reactive, let’s briefly explore the anatomy of a basic native web component. Consider we have a simple HelloWorld class that extends the standard HTMLElement.

The first essential aspect is the definition of the observedAttributes. This static getter indicates which attribute changes the component listens to:


class HelloWorld extends HTMLElement {
    static get observedAttributes() {
        return ['name', 'surname'];
    }

    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.name = '';
        this.surname = '';
    }

    connectedCallback() {
        this.render();
    }

    attributeChangedCallback(name, oldValue, newValue) {
        this[name] = newValue;
        this.render();
    }

    render() {
        this.shadowRoot.innerHTML = `
            

Hello ${this.name} ${this.surname}!

`; } } customElements.define('hello-world', HelloWorld);

In the example above, the constructor initializes shadow DOM, while connectedCallback runs when the component is added to the DOM. The attributeChangedCallback method triggers whenever observed attributes change, inspiring updates in the component’s UI.

To make our custom element available globally, we usually register it alongside our main application’s settings. In most Vue apps, especially those created with Vite, you could simply import it in your main.js file:


import './components/HelloWorld.js';

This ensures your web component is recognized across your Vue application.

Including the Web Component in a Vue Component

Integrating a native web component into Vue is straightforward initially. In a typical Vue component, like App.vue, you’d import your web component and then use Vue’s reactive refs to bind data.

Here’s how you commonly do that inside your Vue component’s “script setup”:


<script setup>
import { ref } from 'vue';

const name = ref('John');
const surname = ref('Doe');
</script>

And in your component’s template, you’d add something like this to use your web component:


<template>
  <hello-world :name="name" :surname="surname"></hello-world>
  <input v-model="name" placeholder="Type your name" />
  <input v-model="surname" placeholder="Type your surname" />
</template>

At first glance, this should work well. You might expect your web component to dynamically reflect changes made through the input fields. However, when you type into the inputs, your Vue refs change—but the web component doesn’t respond. Let’s figure out why.

Why Props Aren’t Reactive in Native Web Components Using Vue

So, what’s causing this behavior? The crux of the problem lies in the distinction between attributes and properties within web components.

When using Vue’s syntax (:name=”name”), Vue initially sets the attribute “name” correctly. Your component’s observedAttributes trigger, calling attributeChangedCallback, and rendering happens as expected.

The catch? After initialization, Vue usually updates DOM properties instead of directly altering attributes. As a result, Vue’s reactive data doesn’t trigger the native component’s attributeChangedCallback when your data changes.

Think of attributes like permanent labels stitched onto a piece of clothing. Changing labels directly notifies the tailor (your web component) to adjust things accordingly. But properties, on the other hand, act like temporary sticky notes put on after the sewing. The tailor doesn’t notice sticky notes—and hence, your web component doesn’t rerender.

The Vue refs you’ve used (name and surname) might change internally, but the component’s attributeChangedCallback doesn’t fire because the attributes themselves aren’t updated on later changes.

Exploring Solutions for Reactive Property Binding

So, how can we tackle this reactivity issue with native web components?

One straightforward solution involves manually updating attributes whenever Vue’s refs change. Vue’s watch function lets you observe changes explicitly, helping you set the DOM attributes to trigger your component’s attributeChangedCallback.

Here’s a quick fix using Vue’s watch:


<script setup>
import { ref, watch, onMounted } from 'vue';

const name = ref('John');
const surname = ref('Doe');
const helloRef = ref(null);

onMounted(() => {
    watch([name, surname], ([newName, newSurname]) => {
        helloRef.value.setAttribute('name', newName);
        helloRef.value.setAttribute('surname', newSurname);
    }, { immediate: true });
});
</script>

<template>
    <hello-world ref="helloRef"></hello-world>
    <input v-model="name" placeholder="Type your name" />
    <input v-model="surname" placeholder="Type your surname" />
</template>

This method ensures the web component correctly reacts to changes, since attributes are explicitly updated through setAttribute, triggering attributeChangedCallback every time your input values update.

Another promising avenue to explore is adopting JavaScript frameworks specifically designed for easier custom element creation, such as Lit. Lit simplifies reactive property and attribute handling for native web components, significantly streamlining integrations with frameworks like Vue or Angular.

With Lit, properties are inherently reactive, and it’s much simpler to establish attribute-property synchronization:


import { LitElement, html } from 'lit';

class HelloWorld extends LitElement {
    static properties = {
        name: { type: String },
        surname: { type: String }
    };

    render() {
        return html`<p>Hello ${this.name} ${this.surname}!</p>`;
    }
}

customElements.define('hello-world', HelloWorld);

Once defined through Lit, Vue’s default methods tend to integrate more smoothly, bypassing much of the difficulty we’ve outlined with vanilla custom elements.

Recommendations and Best Practices

Integrating native web components into Vue can initially seem deceptively easy, but real-world complications—like prop reactivity—quickly emerge.

For robust, maintainable implementations:

  • Use Vue’s watch to manually handle updates to observed attributes.
  • Consider frameworks like Lit that manage reactivity efficiently.
  • Clearly understand the difference between attributes and properties in web components.

Given current limitations, the Vue community occasionally experiences challenges when working directly with native web components. However, with these simple techniques, you can smoothly iron out most rough edges.

Do you have experience integrating web components into your Vue projects? Were there other roadblocks you’ve encountered or novel solutions worth mentioning? Share your insights in the comments below!


Like it? Share with your friends!

Shivateja Keerthi
Hey there! I'm Shivateja Keerthi, a full-stack developer who loves diving deep into code, fixing tricky bugs, and figuring out why things break. I mainly work with JavaScript and Python, and I enjoy sharing everything I learn - especially about debugging, troubleshooting errors, and making development smoother. If you've ever struggled with weird bugs or just want to get better at coding, you're in the right place. Through my blog, I share tips, solutions, and insights to help you code smarter and debug faster. Let’s make coding less frustrating and more fun! My LinkedIn Follow Me on X

0 Comments

Your email address will not be published. Required fields are marked *