Реалізація кросфреймворкових Web Components (використання в React/Vue/Angular)

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація кросфреймворкових Web Components (використання в React/Vue/Angular)
Складна
~5 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація кроссфреймворкових Web Components (React/Vue/Angular)

Головне обіцянняWeb Components — «напишіть один раз, використовуйте скрізь». Реалізація цього обіцяння вимагає вирішити ряд нетривіальних проблем: типізація в кожному фреймворку, обробка подій, двосторонній біндинг, SSR-сумісність.

Проблеми при використанні Web Components у фреймворках

React (до версії 19) не передає об'єкти та масиви через атрибути. Модель подій React не автоматично підхоплює кастомні DOM-события. З React 19 ситуація поліпшилася, але вимагає перевірки.

Angular вимагає схему CUSTOM_ELEMENTS_SCHEMA для роботи з нестандартними тегами.

Vue — найбільш дружній до Web Components фреймворк, обробляє більшість кейсів нативно.

SSRcustomElements.define не існує в Node.js. Компоненти не можна рендерити на сервері без спеціальних рішень.

React: коректна інтеграція

// Проблема: React передає об'єкти як рядок "[object Object]"
// Рішення: ref + useEffect для встановлення властивостей напрямку

import { useRef, useEffect, forwardRef } from 'react'

// Обгортка-компонент для Web Component з об'єктними props
interface DataTableProps {
  columns: Column[]
  rows: Row[]
  onRowSelect?: (row: Row) => void
  onSort?: (column: string, direction: 'asc' | 'desc') => void
}

export const DataTable = forwardRef<HTMLElement, DataTableProps>(
  ({ columns, rows, onRowSelect, onSort }, forwardedRef) => {
    const ref = useRef<HTMLElement>(null)

    // Пробрасуємо forwardedRef
    useEffect(() => {
      if (typeof forwardedRef === 'function') forwardedRef(ref.current)
      else if (forwardedRef) forwardedRef.current = ref.current
    }, [forwardedRef])

    // Об'єкти передаємо через властивості, не атрибути
    useEffect(() => {
      if (ref.current) {
        (ref.current as any).columns = columns
      }
    }, [columns])

    useEffect(() => {
      if (ref.current) {
        (ref.current as any).rows = rows
      }
    }, [rows])

    // Кастомні события
    useEffect(() => {
      const el = ref.current
      if (!el) return

      const handleRowSelect = (e: Event) => {
        onRowSelect?.((e as CustomEvent).detail)
      }
      const handleSort = (e: Event) => {
        const { column, direction } = (e as CustomEvent).detail
        onSort?.(column, direction)
      }

      el.addEventListener('row-select', handleRowSelect)
      el.addEventListener('sort', handleSort)

      return () => {
        el.removeEventListener('row-select', handleRowSelect)
        el.removeEventListener('sort', handleSort)
      }
    }, [onRowSelect, onSort])

    return <data-table ref={ref} />
  }
)

React 19: поліпшена підтримка

// React 19 нативно підтримує передачу об'єктів у Web Components
// та підписку на кастомні события через on* props

// Типізація для нової поведінки React 19
declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'data-table': {
        ref?: React.Ref<HTMLElement>
        columns?: Column[]      // React 19: об'єкт передається напрямку
        rows?: Row[]
        'on-row-select'?: (e: CustomEvent<Row>) => void
        onRowSelect?: (e: CustomEvent<Row>) => void  // React 19
      }
    }
  }
}

Angular: схема та обгортки

// app.module.ts — дозволити невідомі елементи
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],  // ← обов'язково
  bootstrap: [AppComponent],
})
export class AppModule {}
// Standalone компонент — без NgModule
@Component({
  selector: 'app-page',
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <ui-button
      variant="primary"
      [disabled]="isLoading"
      (uiClick)="handleClick($event)"
    >
      Зберегти
    </ui-button>
  `,
})
export class PageComponent {
  isLoading = false

  handleClick(e: CustomEvent) {
    this.isLoading = true
    // ...
  }
}

Angular директива-обгортка для двостороннього біндингу:

// Директива для <ui-input> з підтримкою [(ngModel)]
import { Directive, forwardRef, HostListener, ElementRef } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'

@Directive({
  selector: 'ui-input[formControlName], ui-input[ngModel]',
  standalone: true,
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => UiInputValueAccessor),
    multi: true,
  }],
})
export class UiInputValueAccessor implements ControlValueAccessor {
  private onChange: (value: string) => void = () => {}
  private onTouched: () => void = () => {}

  constructor(private el: ElementRef) {}

  @HostListener('sl-input', ['$event.target.value'])
  @HostListener('sl-change', ['$event.target.value'])
  onInput(value: string) {
    this.onChange(value)
  }

  @HostListener('sl-blur')
  onBlur() { this.onTouched() }

  writeValue(value: string) {
    this.el.nativeElement.value = value ?? ''
  }

  registerOnChange(fn: (v: string) => void) { this.onChange = fn }
  registerOnTouched(fn: () => void) { this.onTouched = fn }

  setDisabledState(disabled: boolean) {
    this.el.nativeElement.disabled = disabled
  }
}

Vue: нативна підтримка

Vue 3 працює з Web Components без налаштування:

// vite.config.ts — не парсити кастомні теги як Vue компоненти
export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          // Елементи з '-' в імені — Web Components
          isCustomElement: (tag) => tag.includes('-'),
        },
      },
    }),
  ],
})
<template>
  <!-- Props передаються як атрибути для примітивів -->
  <ui-button variant="primary" :disabled="isLoading" @ui-click="handleClick">
    Відправити
  </ui-button>

  <!-- Об'єкти через .prop модифікатор -->
  <data-table
    .columns="columns"
    .rows="rows"
    @row-select="handleRowSelect"
  />

  <!-- v-model для кастомного інпута -->
  <ui-input v-model="formValue" label="Email" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import '@company/ui-core/components/button.js'
import '@company/ui-core/components/input.js'
import '@company/ui-core/components/data-table.js'

const isLoading = ref(false)
const formValue = ref('')

// v-model для Web Component — потрібен defineCustomElement або вручну:
// v-model компілюється в :modelValue + @update:modelValue
// Web Component має емітити 'update:modelValue' событие
</script>

Svelte

<script lang="ts">
  import '@company/ui-core/components/button.js'

  let loading = false

  function handleClick(e: CustomEvent) {
    loading = true
    // ...
  }
</script>

<!-- Svelte: on:событие для кастомних подій -->
<ui-button
  variant="primary"
  disabled={loading}
  on:ui-click={handleClick}
>
  Відправити
</ui-button>

SSR: рішення server-side rendering

На сервері немає customElements, HTMLElement, window. Варіанти:

// 1. Lazy import тільки на клієнті (Next.js)
// components/UiButton.tsx
import dynamic from 'next/dynamic'

const UiButtonClient = dynamic(
  () => import('./UiButtonClient').then(m => m.UiButtonClient),
  { ssr: false }
)

export function UiButton(props: ButtonProps) {
  return <UiButtonClient {...props} />
}
// 2. Polyfil для SSR (experimental)
// @lit-labs/ssr — серверний рендер Lit компонентів
import { renderToString } from '@lit-labs/ssr'
import { html } from 'lit'

const result = renderToString(html`
  <ui-button variant="primary">Click</ui-button>
`)
// Отдає declarative shadow DOM розмітку
<!-- Declarative Shadow DOM — SSR-compatible -->
<ui-button>
  <template shadowrootmode="open">
    <style>/* ... */</style>
    <button class="btn btn--primary">Click</button>
  </template>
  Click
</ui-button>

Універсальні обгортки: @lit/react

Офіційне рішення від команди Lit для React-інтеграції:

import { createComponent } from '@lit/react'
import React from 'react'
import { UiButton } from '@company/ui-core'

export const Button = createComponent({
  tagName: 'ui-button',
  elementClass: UiButton,
  react: React,
  events: {
    onUiClick: 'ui-click',
    onUiFocus: 'ui-focus',
    onUiBlur: 'ui-blur',
  },
})

// Теперь Button працює як React компонент з типізацією
function App() {
  return (
    <Button
      variant="primary"
      onUiClick={(e) => console.log(e.detail)}
    >
      Click
    </Button>
  )
}

Чеклист сумісності

Перед публікацією кроссфреймворкової бібліотеки:

  • Всі кастомні события використовують composed: true та bubbles: true
  • Об'єктні властивості не дзеркалятися в атрибути (reflect: false для об'єктів)
  • Компонент коректно працює з disabled через ElementInternals
  • Немає прямих звернень до window, document в constructor — тільки в connectedCallback
  • Експортуються типи для кожного фреймворку
  • Додан custom-elements.json (CEM) для IDE-автодополнення

Терміни

Інтеграція існуючої Web Components бібліотеки в один фреймворк з типами та обгортками — 3–5 днів. Підтримка React + Vue + Angular + SSR з повним набором типів та документації — 3–4 тижні.