Subscribe on changes!

Using `v-html` directive on custom component is not server-rendering properly

avatar
Aug 26th 2022

Vue version

3.2.37

Link to minimal reproduction

https://github.com/HeavyMedl/vue-3-ssr-vhtml-custom-component-bug

Steps to reproduce

git clone https://github.com/HeavyMedl/vue-3-ssr-vhtml-custom-component-bug.git
cd vue-3-ssr-vhtml-custom-component-bug/
npm ci

In development mode

npm run dev

Or, in production mode

npm run build
npm run serve

Visit http://localhost:6173

What is expected?

The server-rendered HTML to reflect the same structure which is generated by hydrating client-side without server-rendering, and for the DOM to reflect this.

<div>
  <section>This is a section without using v-html</section>
  <div class="custom-component">
    <div>I replace whats inside custom component</div>
  </div>
  <article class="custom-dynamic-component">
    <div>I replace whats inside custom component</div>
  </article>
  <div class="custom-dynamic-component">
    <!--[-->Dynamic component without v-html<!--]-->
  </div>
  <div><div>Normal div using v-html</div></div>
</div>

What is actually happening?

In either case, development or production, observe the resultant HTML generated by renderToString. The HTML from v-html is missing:

<div>
  <!-- This works -->
  <section>This is a section without using v-html</section>
  <!-- This fails: v-html -->
  <div class="custom-component"><!--[--><!--]--></div>
  <!-- This fails: v-html -->
  <article class="custom-dynamic-component"><!--[--><!--]--></article>
  <!-- This works -->
  <div class="custom-dynamic-component">
    <!--[-->Dynamic component without v-html<!--]-->
  </div>
  <!-- This works -->
  <div><div>Normal div using v-html</div></div>
</div>

We can see the difference in the compiled bundles

Client bundle (expected)

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_custom_component = resolveComponent("custom-component");
  const _component_custom_dynamic_component = resolveComponent("custom-dynamic-component");
  return openBlock(), createElementBlock("div", null, [
    _hoisted_1,
    createVNode(_component_custom_component, { innerHTML: $data.customComponentHTML }, null, 8, ["innerHTML"]),
    createVNode(_component_custom_dynamic_component, {
      tag: "article",
      innerHTML: $data.customComponentHTML
    }, null, 8, ["innerHTML"]),
    createVNode(_component_custom_dynamic_component, { tag: "div" }, {
      default: withCtx(() => [
        _hoisted_2
      ]),
      _: 1
    }),
    createBaseVNode("div", { innerHTML: $data.normalDiv }, null, 8, _hoisted_3)
  ]);
}

Server bundle

function _sfc_ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
  const _component_custom_component = resolveComponent("custom-component");
  const _component_custom_dynamic_component = resolveComponent("custom-dynamic-component");
  _push(`<div${ssrRenderAttrs(_attrs)}><section>This is a section without using v-html</section>`);
  _push(ssrRenderComponent(_component_custom_component, null, null, _parent));
  _push(ssrRenderComponent(_component_custom_dynamic_component, { tag: "article" }, null, _parent));
  _push(ssrRenderComponent(_component_custom_dynamic_component, { tag: "div" }, {
    default: withCtx((_, _push2, _parent2, _scopeId) => {
      if (_push2) {
        _push2(`Dynamic component without v-html`);
      } else {
        return [
          createTextVNode("Dynamic component without v-html")
        ];
      }
    }),
    _: 1
  }, _parent));
  _push(`<div>${$data.normalDiv}</div></div>`);
}

System Info

System:
    OS: macOS 12.5.1
    CPU: (10) arm64 Apple M1 Max
    Memory: 385.73 MB / 32.00 GB
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 16.13.0 - ~/.nvm/versions/node/v16.13.0/bin/node
    npm: 8.1.0 - ~/.nvm/versions/node/v16.13.0/bin/npm
  Browsers:
    Chrome: 104.0.5112.101
    Firefox: 104.0
    Safari: 15.6.1
  npmPackages:
    vue: ^3.2.37 => 3.2.37

Any additional comments?

My use case is that I have a private Vue 3 component library using vite. Consumers need to inherit the CSS delcarations of components from the library, but should be able to inject escaped HTML using the v-html directive.

import { CompA } from '@company/lib'

<template>
  <div>
    <comp-a v-html='htmlFromCMS'>
  </div>
</template>

<script>
export default {
  name: 'App',
  components: {
    CompA,
  }
  props: {
    htmlFromCMS: {
      type: String,
      default: '<div>My HTML</div>',
    },
  },
}
</script>
avatar
Aug 31st 2022

Ugh, you should never use v-html on a component. A component is supposed to manage its own DOM tree. Using v-html completely defeats the purpose of using a component at all, why not just use it on a plain <div>?

We should probably just make this a compile time error.

avatar
Jul 20th 2023

Ugh, you should never use v-html on a component. A component is supposed to manage its own DOM tree. Using v-html completely defeats the purpose of using a component at all, why not just use it on a plain <div>?

We should probably just make this a compile time error.

It is quite practical for reusing styles, event handlers, etc. for static (<slot />) and dynamic (from backend/CMS) content.

Let's say I have a <Text></Text> component which has special styles for text (e.g. setting h1-h6 font size & weight). I can use this component as usual by putting inside some HTML. And in other places, where content is dynamic, I can just use the same component but provide its innerHTML with v-html.