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?
answer
- second argument of createApp
- declared with defineProps
- must be a plain object
- read once at mount
- reactive values inside still work
basics
~20 sPass 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 linesimport { 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 valuego deeper
Recall that createApp's second argument becomes the root component's props and is declared with defineProps like any other props.
Explain why root props are read once, the object-only rule with its warning, and how a reactive prop value stays live.
Choose between root props, provide and a store for embedded widgets, and convert server-rendered attribute strings safely at the boundary.
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