Všechny články
11. října 2026

Gov Design System v Nuxtu a hydratace, která potichu selhává

Postavil jsem ukázkovou aplikaci na Nuxtu 4 s komponentami Gov Design System CE. Vypadala v pořádku, konzole byla čistá. Přesto Vue při každém načtení zahodilo velkou část stránky, kterou poslal server, a postavilo ji znovu. Tady je, proč se to děje a co s tím.

Co se děje

Komponenty gov-* jsou webové komponenty napsané ve Stencilu a nepoužívají shadow DOM. Jakmile se komponenta zaregistruje, vezme své potomky a přesune je do vlastního HTML.

Nuxt mezitím dělá tohle:

  1. server pošle hotové HTML,
  2. prohlížeč ho zobrazí,
  3. Vue stránku „hydratuje“: projde DOM a porovná ho s tím, co by sám vykreslil.

Když se komponenty zaregistrují před krokem 3, Vue najde strom, který nepoznává. Potomci jsou jinde, přibyly obalové prvky. Vue to vyhodnotí jako nesoulad, komponenty zahodí a vykreslí je znovu. Uživatel vidí poskočení obsahu a formulářová pole na okamžik ztratí stav.

Proč to není vidět

V produkčním sestavení Vue podrobnosti o nesouladu nevypisuje. A pokud používáte nuxt-security, jeho volba removeLoggers odstraní z produkčního kódu volání console.* úplně. Konzole je pak čistá, i když se stránka při každém načtení staví dvakrát.

Chcete-li to změřit, zapněte si hlášení v produkčním sestavení:

// nuxt.config.ts
export default defineNuxtConfig({
  vite: {
    define: { __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'true' },
  },
  security: { removeLoggers: false },
})

Pak stačí otevřít stránku a spočítat varování Hydration ... mismatch. Hlídejte si, že měříte čerstvé sestavení: když build selže, starý výstup ve složce .output zůstane a měříte něco, co už neexistuje. Mně se to stalo třikrát.

Oprava: registrovat komponenty až po hydrataci

Stačí počkat, až Vue stránku převezme, a teprve potom komponenty zaregistrovat:

// app/plugins/design-system.client.ts
import { ComponentLibrary } from '@gov-design-system-ce/vue/plugin'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('app:suspense:resolve', async () => {
    await ComponentLibrary.install?.(nuxtApp.vueApp)
  })
})

Tím nesoulady zmizí. Objeví se ale tři vedlejší účinky.

1. Atributy zapsané pod špatným jménem

Na prvek, který ještě není zaregistrovaný, Vue neumí nastavit vlastnost, a tak zapíše atribut pod jejím jménem: z nativeType vznikne atribut nativetype. Komponenta ale čte native-type, takže odesílací tlačítko zůstane obyčejným tlačítkem.

Po registraci už komponenta ví, které atributy sleduje (observedAttributes), takže je lze dorovnat:

function passOnEarlyAttributes(): void {
  for (const element of document.querySelectorAll('*')) {
    if (!element.localName.startsWith('gov-')) continue

    const component = customElements.get(element.localName) as { observedAttributes?: string[] } | undefined
    for (const name of component?.observedAttributes ?? []) {
      const written = name.replaceAll('-', '')
      if (written !== name && element.hasAttribute(written) && !element.hasAttribute(name)) {
        element.setAttribute(name, element.getAttribute(written) ?? '')
      }
    }
  }
}

Volá se hned po ComponentLibrary.install.

2. Poskočení obsahu

Než se komponenty zaregistrují, mají nulovou výšku nebo ukazují jen svůj text. Po registraci vyrostou a stránka poskočí. Pomáhá vyhradit jim místo předem. Selektor :not(:defined) nestačí, protože prvek je „defined“ dřív, než se vykreslí. Stencil ale po vykreslení přidá třídu hydrated:

gov-form-input:not(.hydrated),
gov-form-select:not(.hydrated) {
  display: block;
  min-height: 2.5rem;
}

3. v-if uvnitř komponenty

v-if přímo uvnitř gov-* komponenty je past. Vue si pamatuje, kde má jeho zástupný uzel být, jenže komponenta ho mezitím přesunula. Při přepnutí podmínky pak spadne na removeChild. Uvnitř těchto komponent používejte v-show:

<GovFormControl>
  <GovFormInput :invalid="!!error" />
  <GovFormMessage v-show="error" variant="error">{{ error }}</GovFormMessage>
</GovFormControl>

Výsledek

Po úpravách hlásí produkční sestavení 0 nesouladů a kumulativní posun obsahu (CLS) je nejvýš 0,02. Celá ukázka včetně testů je na GitHubu.

Měřeno s Gov Design System CE 4.7, Nuxtem 4.6 a Vue 3.5.

© 2026 Myrha Software