How does writing JSX or TSX in Vue 3 differ from React JSX in attributes, events, children and TypeScript setup?
answer
- JSX has no fixed runtime meaning
- Vue's own JSX transform
- class and for, not className
- component children are slots
- jsxImportSource since 3.4
basics
~20 sVue compiles JSX with its own transform into Vue vnodes: you write class and for, events are onXxx props, and a component's children are slot functions or an object of them. TSX needs jsx: preserve and, since 3.4, jsxImportSource: vue.
solid answer
~40 sJSX has no fixed runtime meaning, and Vue uses its own transform, so React's cannot be used. Plain HTML attribute names work: `class` and `for` instead of `className` and `htmlFor`, with Vue's object and array class forms. Events are `onClick`-style props, and the `.capture`, `.once` and `.passive` modifiers are camelCased onto the name (`onClickCapture`). Component children are Vue **slots**, a function or an object like `{{ default: () => ..., footer: () => ... }}`, rather than a `children` prop. The JSX sits in a render function returned from `setup()`, which runs once. For TSX set `"jsx": "preserve"`; since Vue 3.4 the global `JSX` namespace is not registered, so add `"jsxImportSource": "vue"`.
code
ts · 17 lines// Card.tsx
import { defineComponent, ref } from 'vue'
import MyCard from './MyCard.vue'
export default defineComponent({
setup() {
const open = ref(false)
return () => (
<MyCard class={{ open: open.value }}>
{{
default: () => <p>Body</p>,
footer: () => <button onClick={() => (open.value = !open.value)}>Toggle</button>
}}
</MyCard>
)
}
})go deeper
Recall that Vue has its own JSX transform, that class and for work as-is, and that events are onXxx props.
Explain why component children must be slot functions in Vue JSX and how the render function returned from setup() re-runs.
Configure TSX correctly after the 3.4 JSX namespace change, and decide which components in a Vue codebase justify JSX over templates.
Weigh allowing a second authoring style across teams against consistency, tooling support and the template compiler's optimizations.
## Same syntax, different compiler JSX is an XML-like syntax extension to JavaScript. It has **no runtime semantics of its own**: a compiler turns each tag into a function call, and *which* call depends on the transform. Vue uses its **own JSX transform** (`@vue/babel-plugin-jsx`, which scaffolding tools can preconfigure), and it produces Vue vnodes. The Vue docs state plainly that React's JSX transform cannot be used in Vue applications. So `<div id={id}>hello, {name}</div>` looks identical in both frameworks, but in Vue it means "create a Vue vnode", with Vue's rules for props, events and children. ## The differences interviewers ask about | Topic | Vue 3 JSX | React JSX (public model) | |---|---|---| | CSS classes | `class`, with Vue's object and array forms | `className` | | Label target | `for` | `htmlFor` | | Events | `onClick`, and `onClickCapture`/`onKeyupOnce`-style modifiers | `onClick` | | Children of a component | **slots**: a function, or an object of slot functions | a `children` prop | | Where the JSX runs | a render function returned from `setup()` | the component function body | ### Attributes You write HTML attribute names: `class` and `for` work as props, with no need for `className` or `htmlFor`. `class` and `style` accept the same object and array forms as templates. ### Events Events use the same `on` plus capitalized name convention as `h()`. The `.passive`, `.capture` and `.once` modifiers are appended in camelCase (`onClickCapture`, `onKeyupOnce`), and other modifiers go through the `withModifiers` helper: `onClick={withModifiers(handler, ['self'])}`. ### Children are slots This is the difference that breaks React habits. Children of a **component** are Vue slots, so they are passed as a function or as an object of slot functions: ```tsx <MyCard>{() => 'body'}</MyCard> <MyCard>{{ default: () => 'body', footer: () => <small>footer</small> }}</MyCard> ``` The child renders them with `slots.default?.()`, and a scoped slot is a function with a parameter. Because the child calls these functions lazily, it tracks their reactive dependencies itself. ### Where the JSX lives A Vue component written in JSX returns a render function from `setup()`: ```tsx export default defineComponent({ props: { name: String }, setup(props) { const count = ref(0) return () => <button onClick={() => count.value++}>{props.name}: {count.value}</button> } }) ``` `setup()` runs **once**; only the returned function re-runs, when reactive values it reads change (including props the parent passes). `ref` and `computed` are created once in `setup()`, so there is no per-render call order to preserve. ## TypeScript setup - Set `"jsx": "preserve"` in `tsconfig.json` so TypeScript leaves the JSX for Vue's transform. - Since **Vue 3.4**, Vue no longer registers the global `JSX` namespace implicitly: set `"jsxImportSource": "vue"` (or a per-file `/* @jsxImportSource vue */` comment), or import `vue/jsx` to restore the old global behaviour. - `defineComponent()`'s function signature (Vue 3.3+) accepts a setup function directly and supports generic components in TSX. ## When JSX earns its place in a Vue codebase 1. Components whose output is mostly logic: recursive trees, table renderers driven by column definitions, wrappers that forward slots selectively. 2. Library code that must construct vnodes programmatically and still stay readable. 3. Teams already fluent in JSX, when the codebase accepts a second authoring style. The Vue docs still recommend templates by default: they are closer to HTML, and their deterministic syntax lets the template compiler apply compile-time optimizations. Most Vue codebases keep templates as the default and use JSX or `h()` for the few components that need it. ## Habits to unlearn when coming from React JSX - Writing `className` and `htmlFor` out of habit: Vue JSX expects `class` and `for`, and only `class` gets Vue's object and array normalization and merging with a parent's fallthrough `class`. - Passing content as plain children to a component and expecting a `children` prop: in Vue it becomes a non-function default slot and triggers a development warning. - Declaring state in the render function: it would be recreated on every render; state belongs in `setup()`. - Expecting emitted events to need special syntax: a listener for a child component's `close` event is simply the `onClose` prop.
- Why does TSX in a Vue 3.4+ project fail to type-check JSX elements after an upgrade from 3.3?Since 3.4, Vue no longer registers the global `JSX` namespace implicitly. Add `"jsxImportSource": "vue"` next to `"jsx": "preserve"` in tsconfig, or put `/* @jsxImportSource vue */` at the top of a file. Code that relies on the global namespace can import `vue/jsx` to restore the pre-3.4 behaviour.
- How do you apply the .self event modifier in Vue JSX?Only `.passive`, `.capture` and `.once` can be appended to the prop name in camelCase. Other event and key modifiers use the `withModifiers` helper from `vue`: `onClick={withModifiers(handler, ['self'])}`. It wraps the handler so it runs only when the modifier conditions hold.
saying these in an interview costs you the question
- Says React's JSX transform can compile Vue components
- Uses className because it is JSX, not a template
- Passes plain children to a Vue component expecting a children prop
- Believes the setup function re-runs on every render, like a React component body
- Assumes the global JSX namespace is still registered in Vue 3.4+