skip to content

How do Vue 2 functional components, `functional: true` or `<template functional>`, migrate to Vue 3, and should they stay functional?

level: middleimportance: nice to knowfreq 25%

answer

  1. Vue 2 used them for speed
  2. multiple roots now allowed everywhere
  3. a plain function of props and context
  4. template attribute simply removed

basics

~20 s

Vue 3 removed the functional option and the <template functional> attribute. A functional component is now a plain function taking props and a context of attrs, slots and emit; most former ones should simply become ordinary stateful components.

solid answer

~50 s

Vue 2 teams wrote functional components for two reasons: they skipped instance creation, so they initialised faster, and they could return several root nodes. Vue 3 removed both motives: stateful components are fast enough that the difference is negligible, and every component can have multiple roots. The Vue 2 syntaxes are gone. For an SFC with `<template functional>`, the migration guide's path is to delete the attribute, rename `props` to `$props` and `attrs` to `$attrs`, and drop `listeners`, which now live in `$attrs`, so it becomes a normal component. For `{ functional: true, render(h, ctx) }`, Vue 3 expects a **plain function** `(props, { attrs, slots, emit })` that returns vnodes, with `h` imported from `vue` rather than passed in, and props declared as a `props` property on the function. Keep that form only for tiny presentational helpers.

go deeper

for a junior

Recall that functional: true and <template functional> are gone in Vue 3, and most such components simply become normal components.

for a middle

Explain why Vue 2 used them, speed and multiple roots, why neither reason survives in Vue 3, and the plain function signature with props and context.

for a senior

Migrate them deliberately: convert template-based ones to stateful SFCs, keep plain functions only for tiny helpers, and use migration-build warnings to find stragglers.

for a principal

See the removal as a simplification opportunity: collapsing two component styles into one lowers the cost of maintaining a shared library.

## Why Vue 2 had functional components A **functional component** in Vue 2 was a component without an instance: no reactive state, no lifecycle, no `this`. It was declared either with `functional: true` and a `render(h, context)` function, or in an SFC with `<template functional>`. Teams used them for two reasons: - **Performance**: skipping instance creation made them noticeably cheaper to initialise, which mattered in long lists. - **Multiple root nodes**: a stateful Vue 2 component had to render exactly one root element, while a functional one could return several. ## What Vue 3 changed Both motives disappeared: - stateful components got fast enough that the migration guide calls the performance difference **negligible** in most use cases; - **every** component can now render multiple root nodes (fragments). So the guide's recommendation is to use stateful components as you normally would, keeping functional components only for simple cases such as a dynamic heading. And the Vue 2 syntaxes were removed: - the `functional` attribute on an SFC `<template>` is gone; - the `{ functional: true }` component option is gone. ## Migrating `<template functional>` SFCs The guide's path turns these into ordinary components: 1. Remove the `functional` attribute from `<template>`. 2. Rename `props` to `$props` and `attrs` to `$attrs` inside the template, or reference props directly. 3. Drop `v-on="listeners"`: listeners are now part of `$attrs`, so `v-bind="$attrs"` already forwards them. ```vue <!-- Vue 3: formerly <template functional> --> <template> <component :is="`h${$props.level}`" v-bind="$attrs" /> </template> <script> export default { props: ['level'] } </script> ``` ## Migrating `functional: true` render functions In Vue 3 a functional component is **just a function**: - it receives `props` and a `context` object containing `attrs`, `slots` and `emit`; - `h` is **imported from `vue`**, not passed in as the first argument of `render`; - declared props are attached as a `props` property on the function; without it, the `props` object holds every attribute, the same as `attrs`, and names are not normalised to camelCase. ```ts import { h } from 'vue' import type { FunctionalComponent } from 'vue' const DynamicHeading: FunctionalComponent<{ level: number }> = (props, { attrs, slots }) => h(`h${props.level}`, attrs, slots) DynamicHeading.props = ['level'] export default DynamicHeading ``` ## Common mistakes when converting - **Forgetting `.props`.** Without a `props` property, `props` simply mirrors `attrs`: names stay un-normalised (`heading-level` is not camelCased) and only `class`, `style` and `onXxx` listeners fall through to the root by default. - **Expecting `h` as a parameter.** Vue 2 passed `h` as the first argument of `render`; in Vue 3 the first argument is `props`, so code that calls its first parameter as a function breaks. - **Reading Vue 2 context fields.** The Vue 3 context holds `attrs`, `slots` and `emit`. Vue 2 fields such as `children`, `data`, `parent` and `listeners` do not exist; children arrive through `slots`, listeners through `attrs`. - **Converting stateful-looking code.** If the old component reached for `parent` or injections to behave like a stateful one, make it stateful rather than forcing it into the function form. ## Should it stay functional? | Keep it as a plain function when… | Convert to a stateful component when… | |---|---| | It is a tiny presentational wrapper with no state | It needs state, lifecycle hooks, `provide` or watchers | | Its render logic reads best as a function | It was functional only for Vue 2 speed or multiple roots | | You are comfortable with render functions | The team works mostly in `<template>` SFCs | For most Vue 2 libraries the honest outcome is that nearly every functional component becomes a normal SFC, which removes a second component style from the codebase. If you run the migration build, its `COMPONENT_FUNCTIONAL` warning flags each remaining one.

  • In a Vue 3 functional component, what changes if you do not declare `props` on the function?
    Without a `props` declaration, the `props` argument contains all attributes, the same as `attrs`, and names are not normalised to camelCase. Fallthrough is also narrower: only `class`, `style` and `onXxx` listeners are inherited by the root by default. Declaring `DynamicHeading.props = ['level']` extracts real props and makes fallthrough behave like a normal component.
  • Why was converting `<template functional>` components to stateful ones considered safe for performance?
    The migration guide states that in Vue 3 the performance of stateful components improved to the point that the difference from functional ones is negligible in most use cases. The main Vue 2 reasons, cheaper initialisation and multiple roots, no longer apply, so converting trades an insignificant cost for one consistent component style.

saying these in an interview costs you the question

  • Vue 3 still compiles <template functional> into a function component
  • Functional components are still much faster than stateful ones in Vue 3
  • Only functional components can return multiple root nodes in Vue 3
  • Vue 3 still passes h as the first argument of the render function
  • Listeners must be forwarded separately from attrs in Vue 3 functional components