skip to content

In Vue 3, how do you pass initial data to the root component through createApp's second argument, and what are the limits of root props?

level: middleimportance: should knowfreq 40%

answer

  1. second argument of createApp
  2. declared with defineProps
  3. must be a plain object
  4. read once at mount
  5. reactive values inside still work

basics

~20 s

Pass an object as createApp(App, { userId: 42 }) and declare it in the root with defineProps. The object must be an object, and its top-level keys are read once at mount, so later mutations to it do not update the root.

solid answer

~40 s

`createApp(rootComponent, rootProps)` takes an optional second argument that becomes the root component's props, declared in the root with `defineProps` like any other props. A common source is server-rendered data, such as `data-*` attributes on the mount element. Two limits matter. Root props must be an object: anything else is dropped with a development warning. And they are a one-time input: Vue builds the root virtual node from the object at mount, shallow-copying it if it is reactive, and no parent ever re-renders the root, so changing a top-level key later has no effect. For live data, pass a reactive object as a prop **value**, whose inner changes are tracked, or share state through a store or `provide`.

code

ts · 10 lines
ts
import { createApp, reactive } from 'vue'
import ProfileCard from './ProfileCard.vue'

const settings = reactive({ theme: 'light' })
const props = { userId: 42, settings }

createApp(ProfileCard, props).mount('#profile')

props.userId = 7 // no effect: root props are read once at mount
settings.theme = 'dark' // re-renders: a tracked read inside a prop value

go deeper

for a junior

Recall that createApp's second argument becomes the root component's props and is declared with defineProps like any other props.

for a middle

Explain why root props are read once, the object-only rule with its warning, and how a reactive prop value stays live.

for a senior

Choose between root props, provide and a store for embedded widgets, and convert server-rendered attribute strings safely at the boundary.

for a principal

Define the contract between a host page and embedded Vue widgets: typed root props for configuration, explicit channels for anything that changes.

## What the second argument is The signature is `createApp(rootComponent, rootProps?)`. The root component is the top of the tree, and normally nobody passes it props, because there is no parent template. The second argument fills that gap: its keys become props of the root, and undeclared keys become fallthrough attributes on the root element, exactly as they would for a child component. ```ts const el = document.querySelector<HTMLElement>('#profile')! createApp(ProfileCard, { userId: Number(el.dataset.userId), locale: el.dataset.locale ?? 'en', }).mount(el) ``` In `ProfileCard.vue`, `defineProps<{ userId: number; locale: string }>()` declares them, with the same validation and TypeScript types as any component's props. ## Where root props come from - **Data attributes** on the mount element, written by the server-side template. - **A JSON payload** embedded in a `<script type="application/json">` tag and parsed before `createApp`. - **Configuration objects** built by the host page when Vue is embedded in a larger application. Whatever the source, remember that attribute values arrive as strings: convert numbers and booleans before passing them. ## Limit 1: it must be an object If `rootProps` is neither `null` nor an object, Vue discards it and, in development, warns *root props passed to app.mount() must be an object.* Passing a bare string or number is therefore a silent no-op in production. ## Limit 2: it is read once The root props object is turned into the root component's virtual node when `mount()` runs: 1. If the object is reactive, Vue makes a shallow copy of it first. 2. The root component's props are initialised from that virtual node. 3. Props of any component change only when its **parent re-renders** and passes new ones. The root has no parent component, so that never happens. | What you change later | Does the root update? | |---|---| | a top-level key of the original root props object | no | | a key of a `reactive()` object that was passed as a prop value | yes, reads of it are tracked | | state in a store or an app-level provide that the root reads | yes | The middle row is the useful loophole: `createApp(App, { settings })`, where `settings` is `reactive({...})`, lets the host page change `settings.theme` and the root re-renders, because the root's render reads `settings.theme` through a reactive proxy. What does not work is replacing `settings` itself or adding a new top-level key. ## Typing root props end to end `createApp`'s second parameter is typed loosely, as a generic object of named values, so TypeScript does not check it against the root's `defineProps`. The root component's runtime prop validation still runs in development and warns about missing required props or wrong types, but a mismatch does not fail the build. Two habits close the gap: - build the props object in a small typed function, for example `readProfileProps(el: HTMLElement): ProfileProps`, whose return type is the same interface the root passes to `defineProps`; - convert and validate at that boundary, failing early, so the component can trust its props. This matters most when values come from server-rendered markup, where a template change on the server can silently rename an attribute. ## Root props versus other ways to pass data - **Root props**: small, per-mount configuration known before the first render. Typed and validated. - **`app.provide`**: services and configuration shared by many components below the root. - **A store**: state that changes over time and is read in many places. Root props shine for the embedded-widget case: the same component mounted in several places on a server-rendered page, each with different ids or options. ## Common mistakes - Passing `el.dataset` values without converting types, then failing prop validation. - Mutating the root props object after mount and expecting the UI to follow. - Passing a non-object, such as a single id, and receiving nothing. - Reading data attributes from inside the root component instead of passing them in, which couples the component to the host page's markup.

  • Why does wrapping the whole root props object in reactive() not make it live?
    When the root virtual node is created, Vue shallow-copies props that are a reactive proxy, so the root receives a plain snapshot. Even without the copy, props only update when a parent re-renders, and the root has none. Reactivity works one level down: a reactive object passed as a prop value is read through its proxy during render.
  • What happens to a root prop key that the root component does not declare?
    It is treated like any undeclared attribute: it lands in the root's `$attrs` and, by default, falls through onto the root element as an HTML attribute. Declaring every key with `defineProps`, or disabling attribute inheritance, avoids stray attributes in the markup.

saying these in an interview costs you the question

  • Mutating the root props object after mount updates the root component
  • Root props can be a bare string or number
  • The root cannot declare props because it has no parent
  • Data attribute values arrive already typed as numbers or booleans
  • Wrapping root props in reactive() makes top-level changes flow in