Skip to content

fix(core): render async dangerouslySetInnerHTML as content in client render - #9194

Merged
Varixo merged 1 commit into
QwikDev:mainfrom
intellix:fix/async-inner-html
Oct 11, 2026
Merged

Varixo merged 1 commit into
QwikDev:mainfrom
intellix:fix/async-inner-html

Conversation

@intellix

@intellix intellix commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

What is it?

  • Bug

Description

In client render, dangerouslySetInnerHTML bound to a useComputed$ is written as a literal attribute, and the element stays empty:

const Html = component$(() => {
  const html = useComputed$(() => '<b>I should be bold text</b>');
  return <div dangerouslySetInnerHTML={html.value} />;
});
// renders <div dangerouslysetinnerhtml="<b>I should be bold text</b>"></div>

A useSignal value or a plain string works. It only happens when the value is still a promise as the element is created, for example a useComputed$ whose QRL hasn't loaded yet, or an async computed. We hit it on a loading spinner whose SVG came from a useComputed$: every client-rendered spinner was blank, so a page that renders its loading state client-side looked empty until its data arrived.

Reproduction

Open in the playground

import { component$, useComputed$, useSignal } from '@qwik.dev/core';

const Html = component$(() => {
  const html = useComputed$(() => '<b>I should be bold text</b>');
  return <div id="target" dangerouslySetInnerHTML={html.value} />;
});

export default component$(() => {
  const show = useSignal(false);
  return (
    <>
      <button onClick$={() => (show.value = true)}>Render on the client</button>
      {show.value && <Html />}
    </>
  );
});

Click the button: no bold text appears, and #target carries a dangerouslysetinnerhtml attribute instead of the HTML as content.

Cause and fix

When creating an element, a const attribute whose value is a promise goes through queueConstAttributePromise, which always calls directSetAttribute once the promise resolves. The synchronous path right below it sends dangerouslySetInnerHTML to applyConstInnerHtml instead. The fix sends the resolved value through that same helper:

   const attributePromise = value.then((resolvedValue) =>
-    directSetAttribute(
+    key === dangerouslySetInnerHTML
+      ? applyConstInnerHtml(element, resolvedValue)
+      : directSetAttribute(

The rest of the diff is Prettier re-indenting the existing call.

Not covered: SSR

SSR has a similar problem. A promise-valued attribute is written later by the backpatch, which only sets attributes, so dangerouslySetInnerHTML from an async computed is server-rendered as an attribute too. Fixing that means teaching the backpatch to set inner HTML, which is a bigger change, so I've left it out of this PR to keep it small. I'm happy to open an issue or follow-up for it.

Tests

A new test in client-render.spec.tsx renders dangerouslySetInnerHTML from an async computed and checks the HTML is the element's content, not an attribute. It fails on main (expected '' to be '<b>async</b>') and passes with the fix. The full packages/qwik/src/core suite passes (2395 tests).

Checklist

  • My code follows the developer guidelines of this project
  • I performed a self-review of my own code
  • I added a changeset with pnpm change
  • I added new tests to cover the fix / functionality

@intellix
intellix requested a review from a team as a code owner October 9, 2026 16:16
@changeset-bot

changeset-bot Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1b6a280

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 8 packages
Name Type
@qwik.dev/core Patch
@qwik.dev/devtools Patch
eslint-plugin-qwik Patch
@qwik.dev/mcp Patch
@qwik.dev/react Patch
@qwik.dev/router Patch
@qwik.dev/utils Patch
create-qwik Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@qwik.dev/core

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/core@9194

@qwik.dev/router

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/router@9194

@qwik.dev/utils

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/utils@9194

eslint-plugin-qwik

npm i https://pkg.pr.new/QwikDev/qwik/eslint-plugin-qwik@9194

create-qwik

npm i https://pkg.pr.new/QwikDev/qwik/create-qwik@9194

@qwik.dev/optimizer

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/optimizer@9194

@qwik.dev/devtools

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/devtools@9194

@qwik.dev/mcp

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/mcp@9194

commit: 1b6a280

…render

When a const attribute's value was still a promise at element creation, the
resolved value was always set as an attribute, so dangerouslySetInnerHTML
from a useComputed$ ended up as a literal attribute and the element stayed
empty. Route it through the same inner-HTML handling as the sync path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@intellix
intellix force-pushed the fix/async-inner-html branch from 9682392 to 1b6a280 Compare October 9, 2026 16:40

@Varixo Varixo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Varixo
Varixo merged commit 50efc61 into QwikDev:main Oct 11, 2026
@intellix
intellix deleted the fix/async-inner-html branch October 11, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants