Using JSX/TSX in Vue 3: Component Definition, Syntax, Props, Scoped CSS, and Slots
This article provides a comprehensive guide to using JSX/TSX in Vue 3, covering project setup, the defineComponent API, option and function syntax, data binding, event handling, slots, scoped CSS, and various approaches to defining and simplifying props with TypeScript.
Preface
JSX syntax gives developers a comfortable experience; using JSX in Vue 3 combines JavaScript flexibility with Vue's reactivity, and migrating to TSX is straightforward.
Project Setup
Run npm init vue to create a Vue project.
Project Repository
The example project can be found at https://gitee.com/z-shichao/vue_tsx .
Component Definition: defineComponent
In Vue 3, defineComponent is a function API for defining components. It provides three main capabilities:
Creating the component options object: defineComponent lets you define an object containing props, methods, lifecycle hooks, etc.
Type inference support: using defineComponent helps TypeScript infer component types, improving development efficiency and code robustness.
Providing a unified component definition context: internally it processes the options object, converts lifecycle hooks, compiles templates, and offers a clear, consistent component definition.
See a dedicated article: https://juejin.cn/post/6994617648596123679
TSX Syntax
TSX supports two writing modes: option syntax and function syntax.
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #75715e; line-height: 26px">// Option syntax</span>
<span style="color: #f92672; font-weight: bold; line-height: 26px">import</span> { defineComponent } <span style="color: #f92672; font-weight: bold; line-height: 26px">from</span> "vue"
<span style="color: #f92672; font-weight: bold; line-height: 26px">export</span> <span style="color: #f92672; font-weight: bold; line-height: 26px">default</span> defineComponent({
setup() {
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> () => (<div>vue3+tsx</div>)
}
})</code> <code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #75715e; line-height: 26px">// Function syntax</span>
<span style="color: #f92672; font-weight: bold; line-height: 26px">export</span> <span style="color: #f92672; font-weight: bold; line-height: 26px">default</span> defineComponent(() => {
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> () => (<div>vue3+tsx</div>)
})</code>Choosing between option and function syntax depends on personal habit; the following examples use option syntax.
Note: The returned value is a function that returns a JSX element. JSX elements must have a single root tag. If you do not want a root tag, you can use <></>, which does not render in the page.
Interpolation Syntax
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #f92672; font-weight: bold; line-height: 26px">export</span> <span style="color: #f92672; font-weight: bold; line-height: 26px">default</span> defineComponent({
setup() {
<span style="color: #f92672; font-weight: bold; line-height: 26px">let</span> text = ref<string>('我是文本内容')
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> () => (<div>{text.value}</div>)
}
})</code>Note: When defining reactive data with ref, you need to access the value using .value in the template.
Event Binding
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #f92672; font-weight: bold; line-height: 26px">export</span> <span style="color: #f92672; font-weight: bold; line-height: 26px">default</span> defineComponent({
setup() {
<span style="color: #f92672; font-weight: bold; line-height: 26px">let</span> text = '我是文本内容'
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> () => (
<>
<div>{text}</div>
<button onClick={() => { alert('您点击了我') }}>点我</button>
</>
)
}
})</code>Event Modifiers
Add camelCase event modifiers.
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #f92672; font-weight: bold; line-height: 26px">export</span> <span style="color: #f92672; font-weight: bold; line-height: 26px">default</span> defineComponent({
setup() {
<span style="color: #f92672; font-weight: bold; line-height: 26px">let</span> text = '我是文本内容'
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> () => (
<>
<div>{text}</div>
<button onClickStop={() => { alert('您点击了我') }}>点我</button>
</>
)
}
})</code>Note: Use curly braces {} for interpolation, and on+eventName (camelCase) for event binding; custom events follow the same pattern.
JSX Removes Some Directives: v-bind, v-for, v-if
v-bind: use curly braces {} to wrap.
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #f92672; font-weight: bold; line-height: 26px">export</span> <span style="color: #f92672; font-weight: bold; line-height: 26px">default</span> defineComponent({
setup() {
<span style="color: #f92672; font-weight: bold; line-height: 26px">let</span> text = '我是文本内容'
<span style="color: #f92672; font-weight: bold; line-height: 26px">let</span> style = { background: 'red' }
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> () => (
<>
<div style={style}>{text}</div>
<button onClick={() => { alert('您点击了我') }}>点我</button>
</>
)
}
})</code> v-for: use array method map.
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #f92672; font-weight: bold; line-height: 26px">export</span> <span style="color: #f92672; font-weight: bold; line-height: 26px">default</span> defineComponent({
setup() {
<span style="color: #f92672; font-weight: bold; line-height: 26px">let</span> items = ['张三', '李四', '王五']
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> () => (
<>
{items.map(item => <div>{item}</div>)}
</>
)
}
})</code> v-if: use ternary expression.
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menno, monospace; font-size: 12px"><span style="color: #f92672; font-weight: bold; line-height: 26px">export</span> <span style="color: #f92672; font-weight: bold; line-height: 26px">default</span> defineComponent({
setup() {
<span style="color: #f92672; font-weight: bold; line-height: 26px">let</span> isShow = ref<boolean>(true)
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> () => (
<>
<div>{isShow.value ? <div>我出来饿了</div> : ''}</div>
<button onClick={() => { isShow.value = !isShow.value }}>点我</button>
</>
)
}
})</code>Note: JavaScript must be placed inside {}, and the return value should be a renderable element or an array of elements.
Slots
Default Slot
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #f92672; font-weight: bold; line-height: 26px">import</span> { defineComponent, ref } <span style="color: #f92672; font-weight: bold; line-height: 26px">from</span> "vue"
<span style="color: #f92672; font-weight: bold; line-height: 26px">export</span> <span style="color: #f92672; font-weight: bold; line-height: 26px">default</span> defineComponent({
setup(props, { slots }) {
<span style="color: #f92672; font-weight: bold; line-height: 26px">let</span> text = ref<string>('我是默认插槽')
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> () => (<Child>{text.value}</Child>)
}
})
let Child = (props: any, { slots }: any) => {
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> <div>{slots.default()}</div>
}</code>Named Slot
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #f92672; font-weight: bold; line-height: 26px">import</span> { defineComponent, ref } <span style="color: #f92672; font-weight: bold; line-height: 26px">from</span> "vue"
<span style="color: #f92672; font-weight: bold; line-height: 26px">export</span> <span style="color: #f92672; font-weight: bold; line-height: 26px">default</span> defineComponent({
setup(props, { slots }) {
<span style="color: #f92672; font-weight: bold; line-height: 26px">let</span> text = ref<string>('我是默认插槽')
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> () => (
<Child>
{{
default: () => text.value,
name1: () => '我是插槽1',
name2: () => '我是插槽2'
}}
</Child>
)
}
})
let Child = (props: any, { slots }: any) => {
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> <div>
<div>{slots.default()}</div>
<div>{slots.name1()}</div>
<div>{slots.name2()}</div>
</div>
}</code>Scoped Slot
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #f92672; font-weight: bold; line-height: 26px">export</span> <span style="color: #f92672; font-weight: bold; line-height: 26px">default</span> defineComponent({
setup(props, { slots }) {
<span style="color: #f92672; font-weight: bold; line-height: 26px">let</span> text = ref<string>('我是默认插槽')
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> () => (
<Child>
{{
default: () => text.value,
name1: () => '我是插槽1',
name2: (parms: string) => <div>{parms}</div>
}}
</Child>
)
}
})
let Child = (props: any, { slots }: any) => {
<span style="color: #f92672; font-weight: bold; line-height: 26px">let</span> parms: string = '我是参数'
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> <div>
<div>{slots.default()}</div>
<div>{slots.name1()}</div>
<div>{slots.name2(parms)}</div>
</div>
}</code>You can also pass elements to child components via props.
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #f92672; font-weight: bold; line-height: 26px">import</span> { defineComponent, ref } <span style="color: #f92672; font-weight: bold; line-height: 26px">from</span> "vue"
<span style="color: #f92672; font-weight: bold; line-height: 26px">export</span> <span style="color: #f92672; font-weight: bold; line-height: 26px">default</span> defineComponent({
setup(props, { slots }) {
<span style="color: #f92672; font-weight: bold; line-height: 26px">let</span> text = ref<string>('我是默认插槽')
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> () => (
<Child childs={[
<div>我是一</div>,
<div>我是二</div>
]} />
)
}
})
let Child = (props: any, { slots }: any) => {
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> <div>
<div>{props?.childs[0]}</div>
<div>{props?.childs[1]}</div>
</div>
}</code>Using Scoped CSS in Vue 3 TSX
In Vue 3, scoped CSS cannot be directly written in TSX files. Write TSX code inside the <script lang="tsx"> tag of a .vue single‑file component, and add the scoped attribute to the <style> tag.
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><script lang="tsx">
import { defineComponent } from "vue"
export default defineComponent({
setup() {
return () => (
<>
<div class='text'>css scoped</div>
</>
)
}
})
</script>
<style scoped>
.text {
background: red;
}
</style></code>Defining Props in Vue 3 + TSX
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #f92672; font-weight: bold; line-height: 26px">import</span> { defineComponent, PropType } <span style="color: #f92672; font-weight: bold; line-height: 26px">from</span> "vue"
<span style="color: #f92672; font-weight: bold; line-height: 26px">export</span> <span style="color: #f92672; font-weight: bold; line-height: 26px">default</span> defineComponent({
setup() {
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> () => (
<>
<div>我是父组件</div>
<Text name={'我 是 传进来的name'} />
</>
)
}
})
// child component
<span style="color: #f92672; font-weight: bold; line-height: 26px">const</span> Text = defineComponent({
props: {
name: {
type: String <span style="color: #f92672; font-weight: bold; line-height: 26px">as</span> PropType<string>,
required: true
}
},
setup(props) {
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> () => (<div>{props.name}</div>)
}
})</code>Note: In TSX you can only define generic types like Object or String. For more specific types you need to use as PropType.
Simplifying Props Usage
Because defineComponent provides type inference, you may choose to define types manually with TypeScript.
Option 1: Without defineComponent, use TypeScript for type definitions.
Using TypeScript for type definitions works, but JSX cannot use this approach.
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #f92672; font-weight: bold; line-height: 26px">import</span> { defineComponent } <span style="color: #f92672; font-weight: bold; line-height: 26px">from</span> "vue"
<span style="color: #f92672; font-weight: bold; line-height: 26px">export</span> <span style="color: #f92672; font-weight: bold; line-height: 26px">default</span> defineComponent({
setup() {
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> () => (
<>
<div>我是父组件</div>
<Text name='张三' />
</>
)
}
})
interface Props {
name: string,
age?: number
}
<span style="color: #f92672; font-weight: bold; line-height: 26px">const</span> Text = (props: Props, ctx: any) => {
<span style="color: #f92672; font-weight: bold; line-height: 26px">return</span> <div>{props.name}</div>
}</code>Option 2: Mixed Compilation
Reference: https://juejin.cn/post/7143053446365577253?searchId=202404031524149C416A3419D9B87C6B19
Option 3: Use Vue Macros to Build the Project
Reference: https://vue-macros.dev/zh-CN/guide/getting-started.html
Pros and Cons of JSX vs Template Syntax in Vue
JSX Syntax:
Advantages:
More flexible syntax: JSX allows JavaScript expressions inside the template, providing stronger expressive power and easier readability for developers familiar with JavaScript.
Component logic is more intuitive and maintainable: JSX keeps structure and logic together, enabling related components to be placed in the same file.
Better performance: JSX is compiled to pure JavaScript code, avoiding runtime template parsing.
Disadvantages:
Steeper learning curve: New developers, especially those unfamiliar with React or JSX, may need extra time to master it.
Mixing HTML with JavaScript: Some developers find embedding HTML in JavaScript less intuitive.
Differences from template syntax: TSX/JSX differs from Vue templates and does not support some template‑specific directives.
Template Syntax:
Advantages:
Easy to learn and start: It resembles traditional HTML, making it approachable for beginners.
HTML code is clear: The component structure is visually obvious.
Good editor support: Most editors provide syntax highlighting and auto‑completion for Vue templates.
Disadvantages:
Limited expressive ability: Templates cannot embed arbitrary JavaScript expressions.
Separation of component and logic: Some logic must be placed in the script section, leading to a clearer split between structure and behavior.
Overall, the choice between JSX and template syntax depends on personal preference, team technology stack, and project requirements. Vue 3 offers solid JSX support for developers comfortable with JavaScript, while template syntax remains a straightforward option for teams familiar with HTML.
Click the Follow button on the " Technical Dry Goods " public account to receive timely updates!
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
