Skip to content

bug: Can't use Stencil components with Angular SSR - RuntimeError: NG0500 #6781

Description

@tsteuwer-accesso

Prerequisites

Stencil Version

4.43.5

Current Behavior

When using Angular SSR and StencilJS components, I would expect the server to generate the same HTML markup as what it generates in the browser.

Example markup generated on the server:

<my-button ngh="0" class="my-button hydrated" role="button" tabindex="0" s-id="5">
   <!--r.5-->
   <!--o.0.5-->
   <!--s.5.0.0.0.-->
   <!--t.0.5-->
   Hello
</my-button>

Expected Behavior

The server-rendered version is including extra HTML comments that don't exist in the browser-rendered version, causing hydration errors and causing the page to be unusable.

Example markup generated on the browser:

<my-button ngh="0" class="my-button hydrated" role="button" tabindex="0" s-id="5">
   <!--r.5-->
   <!--s.5.0.0.0.-->
   Hello
</my-button>

System Info

System: node 24.15.0
    Platform: darwin (25.5.0)
   CPU Model: Apple M4 Max (14 cpus)
    Compiler: /Users/my-user/development/my-repo/node_modules/.pnpm/@stencil+core@4.43.5/node_modules/@stencil/core/compiler/stencil.js
       Build: 1779994050
     Stencil: 4.43.5 🍐
  TypeScript: 5.8.3
      Rollup: 4.44.0
      Parse5: 7.2.1
      jQuery: 4.0.0-pre
      Terser: 5.37.0

Steps to Reproduce

This is hard to give reproduction for due to us having a separate repository for the components + react + angular component projects that we use to distribute these, however, I will give you as much info as possible:

Here is our stencil.config.ts:

export const config: Config = {
  namespace: "component-lib",
  watchIgnoredRegex: [/readme\.md$/],
  hashFileNames: false,
  devServer: {
    openBrowser: false,
    reloadStrategy: "pageReload",
  },
  plugins: [sass()],
  globalStyle: "src/global.scss",
  outputTargets: [
    {
      type: "dist",
      esmLoaderPath: "../loader",
      copy: [
        {
          src: "../node_modules/@accesso/icons/content/webfonts",
          dest: "../webfonts",
        },
      ],
    },
    {
      type: "dist-custom-elements",
      customElementsExportBehavior: "auto-define-custom-elements",
      externalRuntime: false,
    },
    {
      type: "dist-hydrate-script",
      dir: "./dist/hydrate",
    },
    {
      type: "docs-json",
      file: "./custom-elements.json",
    },
    {
      type: "www",
      serviceWorker: null, // disable service workers
      copy: [{ src: "components/*/*.html", dest: "pages" }],
    },
    angularOutputTarget({
      esModules: true,
      componentCorePackage: "@accesso/component-lib",
      outputType: "standalone",
      directivesProxyFile: "../angular-workspace/projects/ng-component-lib/src/lib/stencil-generated/components.ts",
      directivesArrayFile: "../angular-workspace/projects/ng-component-lib/src/lib/stencil-generated/index.ts",
      valueAccessorConfigs: [
        {
          elementSelectors: ["quantity-stepper"],
          event: "valueChange",
          targetAttr: "value",
          type: "number",
        },
      ],
    }),
    reactOutputTarget({
      esModules: true,
      outDir: "../react-component-lib/lib/components/stencil-generated/",
    }),
  ],
};

When we build, we then have our @accesso/component-lib (web components), @accesso/ng-component-lib (Angular output), and @accesso/react-component-lib.

In our Angular repository, we do the following in app.config.ts:

export const appConfig: ApplicationConfig = {
    ...other configs...
    provideAppInitializer(() => {
        return import('@accesso/ng-component-lib/loader').then(({ defineCustomElements }) => defineCustomElements());
    }),
}

And in our app.config.server.ts file, we do:

// Follow how Ionic hydrates their Angular server components
export function hydrateComponents(doc: Document, appId: string) {
	return () => {
		return hydrateDocument(doc, {
			excludeComponents: [
				// overlays
				'adk-alert',
			],
		}).then((hydrateResults) => {
			hydrateResults.diagnostics.forEach((d) => {
				if (d.type === 'error') {
					console.error(d.messageText);
				} else if (d.type === 'debug') {
					console.debug(d.messageText);
				} else {
					console.log(d.messageText);
				}
			});

			if (doc.head != null) {
				const styleElms = doc.head.querySelectorAll('style[data-styles]') as NodeListOf<HTMLStyleElement>;
				// eslint-disable-next-line @typescript-eslint/prefer-for-of
				for (let i = 0; i < styleElms.length; i++) {
					styleElms[i]?.setAttribute('ng-transition', appId);
				}
			}
		});
	};
}

const serverConfig: ApplicationConfig = {
   providers: [
       ... server providers ...
      {
	 provide: BEFORE_APP_SERIALIZED,
	 useFactory: hydrateComponents,
	 multi: true,
	 deps: [DOCUMENT, APP_ID],
      },
   }
};

export const config = mergeApplicationConfig(browserConfig, serverConfig);

Code Reproduction URL

Too complicated, see my steps above.

Additional Information

The only way to fix this is to add the ngSkipHydration to all the Stencil components which is a huge headache.

Here's the Angular error in the browser console:

RuntimeError: NG0500: During hydration Angular expected a text node but found a comment node.

Angular expected this DOM:

  #text(Hello)  <-- AT THIS LOCATION
  …


Actual DOM is:

<my-button class="my-button thydrated" role="button" tabindex="0">
  <!-- r.3 -->  <-- AT THIS LOCATION
  …
</my-button>

Note: attributes are only displayed to better represent the DOM but have no effect on hydration mismatches.

To fix this problem:
  * check the "_HomeComponent" component for hydration-related issues
  * check to see if your template has valid HTML structure
  * check if there are any third-party scripts that manipulate the DOM. More info: https://v21.angular.dev/guide/hydration#third-party-scripts-with-dom-manipulation
  * or skip hydration by adding the `ngSkipHydration` attribute to its host node in a template

 Find more at https://v21.angular.dev/errors/NG0500
    at validateMatchingNode (_debug_node-chunk.mjs:6269:11)
    at locateOrCreateTextNodeImpl (_debug_node-chunk.mjs:16298:16)
    at Module.ɵɵtext (_debug_node-chunk.mjs:16279:22)
    at HomeComponent_Template (home.component.ts:30:16)
    at executeTemplate (_debug_node-chunk.mjs:5088:5)
    at renderView (_debug_node-chunk.mjs:5488:7)
    at renderComponent (_debug_node-chunk.mjs:5467:5)
    at renderChildComponents (_debug_node-chunk.mjs:5517:5)
    at renderView (_debug_node-chunk.mjs:5502:7)
    at ComponentFactory2.createComponentRef (_debug_node-chunk.mjs:8764:7)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions