Several Apps and Mount Targets

createApp builds one isolated app: mount it once into any element or hydrate server HTML, and unmount to tear it down.

SyntaxcreateApp(rootComponent, rootProps?).mount(container: Element | string)

createApp(rootComponent, rootProps?) builds an application instance. It holds the plugins, global components, directives, config and app-level provides for one component tree. Nothing renders until app.mount(container), which accepts a DOM element or a CSS selector string — the first match — and returns the root component instance. Each app instance can be mounted once.

Several apps on one page

This is normal whenever Vue runs inside a page it does not own: a server-rendered template with a rating widget in one slot and a comment counter in another. Each createApp is a separate world. Plugins, global properties, directives and provides are per app, so a store or a router must be installed into every app that needs one, and two apps share state only if you hand them the same object. Since 3.5, set app.config.idPrefix on each app so the ids from useId() cannot collide across apps.

Mounting into existing markup

If the root component has a template or a render function, mounting replaces whatever was inside the container. To adopt HTML that a server already rendered, use createSSRApp instead: it hydrates the existing nodes in place, and repairs mismatches by replacing the offending nodes rather than keeping them.

Unmounting

app.unmount() walks the tree and runs the unmount hooks, which is where watchers, listeners and timers registered inside components get cleaned up. The instance is finished afterwards. To show the app again, call createApp for a fresh one.

The example widget renders a row of numbered buttons; clicking one marks that number as the chosen rating and leaves it pressed.

Example

Example · vue
<script setup lang="ts">
import { computed, ref } from 'vue'

const props = withDefaults(
  defineProps<{ articleId: string; max?: number }>(),
  { max: 5 }
)

const score = ref<number | null>(null)
const stars = computed(() =>
  Array.from({ length: props.max }, (_, index) => index + 1)
)

function choose(value: number) {
  score.value = value
}
</script>

<template>
  <div class="rating" :data-article="props.articleId">
    <button
      v-for="star in stars"
      :key="star"
      type="button"
      :aria-pressed="score === star"
      @click="choose(star)"
    >
      {{ star }}
    </button>
  </div>
</template>

<style scoped>
.rating {
  display: flex;
  gap: 0.25rem;
}
.rating button[aria-pressed='true'] {
  font-weight: 700;
}
</style>

When to use it

  • Adding two independent Vue widgets to a page rendered by a CMS or a server framework.
  • Hydrating markup that was rendered on the server instead of replacing it.
  • Tearing down a Vue panel that non-Vue page code mounted and then closed.
  • Keeping useId() output unique when one page runs more than one app.

More examples

Two apps on one server-rendered page

The pattern for adding Vue to a page rendered by something else - a CMS, a Rails or Laravel template. Each widget is its own app, so each needs its own plugins and its own config. The second argument to createApp is the root props, which is how page data reaches the root component without a global. Guarding on the element matters because the failure is quiet: mount with a selector that matches nothing does not throw - Vue logs "Failed to mount app: mount target selector returned null" in development only and mount() returns undefined - and this script will be loaded on pages that have only one of the two widgets. Without idPrefix, both apps number their useId() output from zero and two elements on the page can end up with the same id.

Example · ts
// src/widgets.ts
import { createApp } from 'vue'
import RatingWidget from './components/RatingWidget.vue'
import CommentCount from './components/CommentCount.vue'

const ratingEl = document.querySelector<HTMLElement>('[data-rating]')
if (ratingEl) {
  const app = createApp(RatingWidget, {
    articleId: ratingEl.dataset.article ?? ''
  })
  app.config.idPrefix = 'rating' // 3.5+: useId() returns "rating:0", "rating:1"
  app.mount(ratingEl)
}

const countEl = document.querySelector<HTMLElement>('[data-comment-count]')
if (countEl) {
  const app = createApp(CommentCount)
  app.config.idPrefix = 'comments'
  app.mount(countEl)
}

Mounting and unmounting on demand

Reach for this when non-Vue page code controls the lifetime of a Vue tree. unmount() runs every component's unmount hooks, so an interval started in a component, a document listener added by a directive, or a watcher stopped on unmount all get their chance to clean up; dropping the reference without calling unmount leaves all of that running. Note that openPanel builds a new app every time rather than re-mounting the old one, because a mounted app cannot be mounted again.

Example · ts
// src/panel.ts
import { createApp } from 'vue'
import type { App } from 'vue'
import HelpPanel from './components/HelpPanel.vue'

let current: App | null = null

export function openPanel(target: Element): void {
  closePanel()
  current = createApp(HelpPanel)
  current.mount(target)
}

export function closePanel(): void {
  current?.unmount()
  current = null
}

Adopting server-rendered HTML

The client entry of a server-rendered app. createSSRApp takes exactly the same arguments as createApp and returns the same kind of app instance; the difference is what mount does with the container's existing children. In hydration mode Vue attaches to the nodes that are already there, which is why the first paint is the server's HTML rather than an empty element. If the client's render does not agree with that HTML, Vue logs a mismatch and morphs the existing nodes to match - correct, but slower than rendering once, so the usual causes (locale-dependent dates, random values, invalid nesting) are worth removing rather than tolerating.

Example · ts
// src/entry-client.ts
import { createSSRApp } from 'vue'
import App from './App.vue'

// The HTML inside #app was rendered on the server.
// createSSRApp hydrates those nodes; createApp would discard them.
createSSRApp(App).mount('#app')

Discussion

  • Be the first to comment on this lesson.