☰
JSX核心语法与实战避坑:从组件开发到条件渲染
2026/10/15 11:52:51 网站建设 项目流程

1. 先说透:JSX 到底是什么东西

JSX 这个缩写,全称是 JavaScript XML,第一次见到它的人十有八九会懵——这玩意儿看着既像 HTML 又像 JavaScript,到底算哪门子语法?

我用一句大白话给你解释清楚:JSX 就是让你在 JavaScript 文件里直接写类似 HTML 结构的语法扩展。它不是在浏览器里直接跑的东西,而是需要经过编译工具(最常见的是 Babel)转换成标准的 JavaScript 函数调用后,才能被浏览器识别和执行。

打个比方:JSX 就像是你用"中文拼音"写给朋友看的便签,真正要办事情的时候,还是得把拼音翻译成正式的汉字。这个"翻译"的过程,在工程里叫做编译转换。

先看一段最基础的 JSX 代码长什么样:

const element = <h1>欢迎来到我的博客</h1>;

这行代码看起来像 HTML,但它其实就是一个普通的 JavaScript 变量赋值。Babel 编译之后会变成:

const element = React.createElement("h1", null, "欢迎来到我的博客");

看到没?JSX 的本质是语法糖,最终会被转换成React.createElement这种函数调用。所以你可以把 JSX 理解为一种更直观、更接近最终 UI 形状的写法,写起来舒服,读起来也清楚。

那么问题来了:为什么 React 团队要搞出这么个东西?直接手写createElement不行吗?

手写当然行,但你要想象一个复杂页面——一个列表组件里有几十个元素,每个元素还有属性、子节点、事件绑定,全部用React.createElement("div", { className: "xxx", onClick: handleClick }, ...)这种嵌套写法,代码会变成一团乱麻。而 JSX 让代码结构和页面结构一一对应,源码长什么样,渲染出来大概就是什么样,开发效率自然就上去了。

适合谁看这篇?包括但不限于:刚接触 React 但被 JSX 各种写法绕晕的新手、从 Vue 转 React 想快速对齐心智模型的开发者、以及写了一阵 JSX 但总在"为什么这么写会报错"上卡壳的人。

2. JSX 的核心规则:表达式、属性与子节点

2.1 用 { } 嵌入 JavaScript 表达式

JSX 里有一套非常清晰的规则:静态文本直接写,动态内容用大括号包起来。

比如你要展示一个用户的名字:

const userName = "张三"; const element = <p>当前用户:{userName}</p>;

大括号里不仅可以放变量,还可以放任意 JavaScript 表达式——做得更复杂一点:

const user = { name: "张三", age: 18 }; const element = ( <div> <p>姓名:{user.name}</p> <p>年龄:{user.age >= 18 ? "成年人" : "未成年人"}</p> <p>问候:{user.name.toUpperCase()}</p> </div> );

操作符、函数调用、三元表达式、数组的 map 方法,只要你脑子里想着"这是一个 JavaScript 表达式",都可以往大括号里塞。

这里有个新手最常犯的错误:在大括号里写 if 语句。比如:

// 错误示范:if 是语句,不是表达式 const element = <p>{if (true) "你好"}</p>;

因为 JSX 的大括号只接受表达式(有返回值的代码片段),不接受语句(没有返回值的控制流)。if 语句本身不产生值,所以直接写会报错。正确的做法是用三元表达式,或者把 if 逻辑抽到组件函数外面。

再说一个容易忽略的细节:大括号里哪怕写数字和字符串也会有区别。{1}渲染出来是数字 1,{"1"}渲染出来是字符串 1。虽然视觉上没差别,但类型不同。如果你后续要做条件判断或传给 props,这个差别就会体现出来。

2.2 属性的写法:字符串、表达式与展开运算符

JSX 元素的属性(attributes)分两种情况:

静态属性——直接写字符串:

const element = <input placeholder="请输入用户名" maxLength="10" />;

动态属性——用大括号包裹表达式:

const placeholderText = "请输入手机号"; const element = <input placeholder={placeholderText} maxLength={10} />;

还有一种非常有用的写法:展开运算符。当你要批量传属性的时候,能省掉不少重复代码:

const inputProps = { type: "text", placeholder: "请输入内容", disabled: true, }; const element = <input {...inputProps} />;

这行代码等同于type="text" placeholder="请输入内容" disabled三个属性全写上。实际项目里,封装公共组件、透传 props 的时候,这种写法非常常见。

命名上有两个坑必须记住:

  • JSX 里 HTML 属性名用的是驼峰命名法(camelCase),不是 HTML 原生的中划线风格。class要写成className,for要写成htmlFor,tabindex要写成tabIndex,readonly要写成readOnly。因为 JSX 本质上是在写 JavaScript,class和for是 JavaScript 的保留字,直接用会冲突。
  • 自定义组件传值的时候,用小写开头会被当成 HTML 标签,大写开头才是组件。后面的章节我会单独展开。

2.3 子节点:嵌套、文本、表达式和数组

JSX 的嵌套关系和 HTML 一样,用标签的闭合结构来表示父子关系:

const element = ( <div className="card"> <h2>卡片标题</h2> <p>这是卡片的描述内容。</p> </div> );

子节点如果不是包在标签里,而是通过表达式计算出来的,那就用大括号:

const content = "动态内容"; const element = ( <div> <span>前缀文本</span> {content} <span>后缀文本</span> </div> );

还有一种情况:子节点是数组。数组里的每一项都会被当作一个独立的子节点渲染。这个特性配合数组的map方法,就成了 React 里渲染列表的核心手段:

const todos = ["学习 JSX", "写组件", "跑单元测试"]; const list = ( <ul> {todos.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> );

这里有个细节新手经常忽略:key属性。渲染数组的时候,React 需要key来识别每一个列表项的身份,方便后续的更新、复用和删除。key必须唯一且稳定,最好不要用数组的index(因为插入或删除元素后index会变,导致组件状态错乱),而是用数据自带的唯一 id。

关于子节点还有一个冷门但实用的点:空白字符和换行会被规则化。JSX 里写了很多空格和换行,编译时不一定全保留。我通常建议:格式化交给工具自动处理,逻辑上不要依赖 JSX 源码里的空白对齐。

3. 组件到底是怎样通过 JSX 配合的?

3.1 组件用大写开头:这是一条硬规则

JSX 里区分"原生 HTML 标签"和"自定义组件"靠的就是首字母大小写。

  • <div>、<span>、<a>这类小写开头,被识别为原生 HTML 标签。
  • <UserCard>、<MyButton>这类大写开头,被识别为组件引用。

原因非常简单:Babel 在编译时遇到小写标签会直接映射成字符串字面量(比如"div"),传给createElement的第一个参数;遇到大写标签则会被当成变量解析,拿到组件函数或类本身。

来看实际例子:

function Welcome() { return <p>欢迎你</p>; } const element = <Welcome />;

<Welcome />没有子节点,也没有传属性,这表示调用Welcome组件,并传入空的props对象。如果你不小心写成<welcome />,React 就会去找一个叫welcome的原始 HTML 标签——自然找不到,于是报错。

常见的小写错误场景:组件定义在另一个文件中,导入时没有重命名,导致引用和定义大小写不一致。比如定义了UserProfile,导出时误写成export default userProfile,引用时写<UserProfile />,就会报"元素类型无效"的错误。

3.2 给组件传参数:JSX 属性就是 Props

组件的强大之处在于可以接收外部传入的数据,然后根据数据做不同渲染。JSX 里最简单的传参方式就是加属性:

function UserCard(props) { return ( <div className="user-card"> <h3>{props.name}</h3> <p>{props.bio}</p> </div> ); } const element = <UserCard name="李四" bio="前端开发工程师" />;

name和bio会被打包进一个对象,作为props传给组件。除此之外,还有几类特殊写法你要掌握:

布尔类型的属性传值:

// 方式一:直接写属性名,默认为 true <Button disabled /> // 方式二:显式传布尔值 <Button disabled={true} />

注意:直接写disabled和写disabled={true}效果一样,但写disabled="true"传给组件的是一个字符串"true",不是布尔值,这是最常见的坑。

JSX 属性的表达式传值,之前说过,在这里再强调一下:任何非字符串类型的值,都必须用大括号包起来:

<Counter count={42} /> <DataList items={["a", "b", "c"]} /> <Callback onUpdate={() => handleUpdate()} />

3.3 组合与嵌套:children 的秘密

组件可以嵌套使用,而且嵌套的子内容会自动成为props.children。这是一个非常核心的概念,直接决定了组件灵活性的上限。

function Card(props) { return ( <div className="card"> <div className="card-header">{props.title}</div> <div className="card-body">{props.children}</div> </div> ); } const element = ( <Card title="通知"> <p>你有一条新消息</p> <p>请及时处理</p> </Card> );

这里的两个<p>标签被自动打包成props.children,在Card组件内部通过{props.children}渲染出来。这种模式让组件变成了一个"盒子",你可以往里塞任何内容——文本、其他组件、甚至一组混合内容。

实际项目里,很多通用组件(Modal、Panel、Layout 等)都大量依赖 children 来实现内容插槽的效果。

4. 条件渲染的几种典型写法

页面上经常要根据不同状态显示不同内容,React 里没有 Vue 那种v-if指令,全靠 JS 逻辑自己在 JSX 里处理。我把常用的几种写法都列出来,你按照场景挑。

4.1 三元表达式:最通用的选择

function Greeting({ isLoggedIn }) { return <p>{isLoggedIn ? "欢迎回来" : "请先登录"}</p>; }

结构性更强一点:直接决定渲染哪棵节点树。

function AuthPage({ isLoggedIn }) { return isLoggedIn ? ( <Dashboard /> ) : ( <LoginForm /> ); }

三元表达式可以直接写在 JSX 的大括号里,非常灵活。但如果条件分支特别多,嵌套三元会变得很难读,这时候就要考虑抽出独立变量再加 if 判断了。

4.2 逻辑与(&&)短路:只需要"有则显示,无则不显示"

function Notification({ message }) { return ( <div> {message && <p className="alert">{message}</p>} </div> ); }

message为真值的时候,渲染<p>;为假值(null、undefined、false)时不渲染任何内容。这个写法比三元表达式更简洁。

但要特别提醒一个经典 bug:当message是数字 0 时,页面会渲染出一个0字符。

原因是 JavaScript 里0 && anything的结果是0,React 渲染0时会当作真实内容输出,不是"不渲染"。所以项目里约定俗成:逻辑与短路判断的左边变量,要保证不是 0 或空字符串这类"假值但可渲染"的情况。如果你确实可能传数字,请写成{message > 0 && <p>{message}</p>}或先转成布尔值。

4.3 提前返回:渲染前先做分支判断

有时候逻辑复杂,在 JSX 里堆三元会很痛苦。这时候可以直接在函数体里做判断,分支各自 return:

function UserStatus({ role }) { if (role === "admin") { return <AdminPanel />; } if (role === "guest") { return <GuestView />; } return <NormalView />; }

这种写法结构清晰,每个分支一目了然,适合多分支的场景。注意:组件函数里return的是 JSX,但判断本身在 JSX 外面,这是完全合法且推荐的模式。

5. JSX 的语法陷阱:这些报错你得认得

5.1 Fragment:不想多包一层 div 怎么办?

JSX 要求最外层必须有且仅有一个根节点。如果你要返回并列的两个元素,直接这样写是错的:

// 错误:缺少根节点 function WrongComponent() { return ( <h2>标题</h2> <p>内容</p> ); }

解决方式有两种。

第一种:包一层<div>—— 但这样会在页面里多出无意义的 DOM 节点,可能影响样式和布局。第二种:用 Fragment:

import { Fragment } from "react"; function CorrectComponent() { return ( <Fragment> <h2>标题</h2> <p>内容</p> </Fragment> ); }

Fragment 本身不会渲染到 DOM 上,只是一个"虚拟容器"。更短的语法是直接写空标签:

function ShortComponent() { return ( <> <h2>标题</h2> <p>内容</p> </> ); }

这是我在日常开发里最常用的写法。有一个例外场景:当你要在map循环里给 Fragment 添加key时,必须用<Fragment key={xxx}>,不能直接写空标签,因为空标签语法不支持key属性(React 会无法识别你的key)。

5.2 注释怎么写在 JSX 里?

HTML 支持<!-- 注释 -->,但 JSX 里不认这个。在 JSX 表达式内部写注释,必须用 JavaScript 的注释语法包在大括号里:

const element = ( <div> {/* 这是 JSX 内的注释 */} <h2>标题</h2> {/* 多行注释 换行也是可以的 */} <p>内容</p> </div> );

注意:注释放在标签内部和放在大括号内部的写法略有不同。常见错误是直接在 JSX 里写// 注释或/* 注释 */,会直接导致语法错误。

5.3 危险的 HTML 注入与 dangerouslySetInnerHTML

JSX 默认会对所有文本内容进行转义,把<、>、&等字符转成 HTML 实体。这也是 React 内置的一种 XSS 防护:你传入的数据即使包含<script>,也不会被当作 HTML 执行。

const userInput = "<script>alert('xss')</script>"; const safe = <p>{userInput}</p>; // 页面渲染出来的是普通文本,不会执行脚本

但如果你需要渲染真正的 HTML 内容(比如从富文本编辑器拿到的 HTML 字符串),就必须显式告诉 React:

const richContent = "<p>这是<strong>富文本</strong>内容</p>"; const element = <div dangerouslySetInnerHTML={{ __html: richContent }} />;

这里的dangerouslySetInnerHTML名字本身就带有警告性质。使用它等于绕过了 React 的转义机制,如果内容来自用户且没有经过服务端过滤,就存在 XSS 风险。我的建议是:能用 JSX 结构描述的内容尽量不用它;必须用时,保证数据源的可靠性或惰性转义后再用。

6. 样式与类名:写 JSX 时怎么处理 CSS

6.1 className 与 style 对象

JSX 里给元素添加类名,前面提过要用className:

const element = <div className="container active">内容</div>;

多条件拼接类名时,用模板字符串或工具函数:

const isActive = true; const element = ( <div className={`container ${isActive ? "active" : "inactive"}`}>内容</div> );

项目如果复杂,推荐使用classnames这个工具库,写法更优雅,也避免了字符串拼接出错的可能。

内联样式同样特殊:JSX 的style属性接受的是一个对象,不是字符串。而且这个对象的属性名必须用驼峰命名法:

const element = ( <div style={{ backgroundColor: "#f0f0f0", padding: "16px", fontSize: "14px" }}> 内容 </div> );

大括号里再套一个大括号,外层表示"这是 JSX 表达式",内层表示"这是一个对象字面量"。很多新手第一次写都会漏掉一层大括号。

有一点要注意:style对象的属性值是字符串或数字,数字会被自动加上px(某些特定属性除外,比如opacity、zIndex等不需要单位)。

6.2 className 与 style 对比场景

给个实际选择建议:

  • 静态样式、常规布局:用className+ CSS 文件或 CSS Modules 方案。
  • 动态宽度、主题色、特殊交互效果:用内联style更方便。

两者结合是日常常态。不会有人蠢到把所有样式全放 JSX 里,页面会失去维护性;也不会有人顽固到一点内联样式不用,因为某些场景下(比如根据参数实时计算颜色),内联是最直接的手段。

7. JSX 里头的事件绑定:onClick 与传参

事件绑定在 JSX 里有一整套和原生 HTML 截然不同的写法,但逻辑上是自洽的。

原生 HTML 是onclick="handleClick()",JSX 里是驼峰式onClick,值是一个真正的函数引用:

function handleClick() { console.log("按钮被点击了"); } const element = <button onClick={handleClick}>点击我</button>;

注意:onClick={handleClick}是传函数引用,不是执行函数。写onClick={handleClick()}会导致页面渲染时立即执行一次handleClick,然后点击按钮时什么都不会发生(因为传进去的是函数执行后的返回值,通常是 undefined)。这是新手最高频的错误之一。

7.1 带参数的事件处理

如果要在事件回调里传参数,常见写法是箭头函数包裹:

function handleDelete(id) { console.log("删除 id 为", id, "的记录"); } const element = <button onClick={() => handleDelete(42)}>删除</button>;

这里() => handleDelete(42)是一个"延迟执行"的函数:点击时才会执行内部逻辑。如果直接写onClick={handleDelete(42)},一样会立即执行,这是大家都踩过的坑。

实际项目里,在map循环中经常需要传id,长这样:

const itemList = items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => handleDelete(item.id)}>删除</button> </li> ));

7.2 事件对象 event 的传递

事件处理函数默认可以接收到一个合成事件对象event:

function handleInputChange(event) { console.log("输入的值:", event.target.value); } const element = ( <input type="text" onChange={handleInputChange} placeholder="请输入" /> );

event是 React 的合成事件(SyntheticEvent),它封装了浏览器原生事件,做了跨浏览器兼容处理,也做了性能优化。注意 React 的event对象在事件处理完成后会被复用和清空,所以不要异步地缓存event,正确的做法是在回调里同步取值或手动保存需要的数据。

7.3 表单控件的受控与非受控

JSX 里,<input>的value属性直接绑定一个变量时,输入框就变成了"受控组件"——它的值完全由 React 状态驱动,用户输入要通过onChange事件更新状态才能反映到界面上:

import { useState } from "react"; function InputDemo() { const [value, setValue] = useState(""); return ( <input type="text" value={value} onChange={(e) => setValue(e.target.value)} placeholder="输入点什么" /> ); }

受控组件的好处是数据流单向清晰、容易做输入校验;代价是多写几行代码。非受控组件则通过defaultValue配合ref操作,适合不需要实时反馈的场景(比如表单提交时一次性读取数据)。

这里涉及到useState概念,是 React 的 Hooks 之一,如果你这一篇只是学 JSX 基础,可以先记住写法,后面深入 React 再细化原理。

8. 条件渲染与列表渲染一起用:综合例子

光讲概念容易飘,我给你一个完整的场景,把前面提到的 JSX 核心能力串起来。

假设我们要做一个简单的任务列表页面,包含:展示任务、状态过滤、空状态提示。代码结构如下:

function TaskList({ tasks, filter }) { const filteredTasks = tasks.filter((task) => { if (filter === "active") return !task.completed; if (filter === "completed") return task.completed; return true; }); return ( <div className="task-list"> <h2>任务列表</h2> {filteredTasks.length === 0 ? ( <p className="empty-tip">暂无任务,去添加一个吧</p> ) : ( <ul> {filteredTasks.map((task) => ( <li key={task.id} className={task.completed ? "done" : "pending"}> <input type="checkbox" checked={task.completed} readOnly /> <span>{task.title}</span> <button onClick={() => console.log("标记/取消任务", task.id)}> {task.completed ? "设为未完成" : "设为完成"} </button> </li> ))} </ul> )} </div> ); }

这个例子同时用到了:

  • 函数组件和props接收
  • 普通 JavaScript 逻辑(filter方法)处理数据
  • 三元表达式做空状态判断
  • map方法渲染列表,每一项都有key
  • checked属性结对布尔值
  • 箭头函数包裹事件回调

如果你能独立理解这段代码的每一行,说明 JSX 的基础语法你已经掌握了七八成。

9. 一段实用经验:JSX 开发中的避坑清单

最后分享一些我在实际开发中踩过的坑,以及积累下来的习惯,按重要性排列。

9.1 格式化工具从小抓起

JSX 的嵌套层级多,大量尖括号和大括号混在一起,不格式化非常容易看错。我在团队里统一要求用工具做自动格式化,提交代码前统一过一遍。千万别靠手调缩进,效率低还容易出错。

9.2 组件与标签的边界感

刚开始写 JSX 的人容易把"组件"想得太神秘,其实组件就是一个返回 JSX 的函数。当你想要复用一段界面结构时,就该考虑把它抽成组件。组件名用大写,放在单独文件里,职责单一,这一点比任何花哨写法都重要。

9.3 命名与 props 的组织

  • 组件文件名用PascalCase(大驼峰),比如UserCard.jsx
  • props命名用camelCase(小驼峰),避免与原生 HTML 属性混淆
  • 传布尔值属性时,明确写disabled={false},而不是直接省略——省略等于true,容易产生误导

9.4 记住 JSX 不是模板引擎

Vue 的模板语法和 JSX 的设计思路不太一样。Vue 的v-if、v-for是标签属性,而 JSX 里你就是用 JavaScript 的if、map怎么写都行。拥抱 JavaScript 本身,比记一堆指令要省心得多。遇到任何"JSX 该怎么写某个逻辑"的问题,先问问自己"原生 JavaScript 你怎么处理",然后套进去就行了。

9.5 调试时先确认编译错误不是罪魁祸首

JSX 的报错信息有时候比较隐晦,主要分两类:

  • 语法层面的报错(比如标签没闭合、括号不匹配),编译阶段就会挂掉,错误信息会直接指向源码位置。
  • 语义层面的报错(比如引用了一个未定义的组件变量、传了不合法 props),运行到浏览器控制台才暴露出来。

遇到报错先看最顶层的错误信息,不要盯着堆栈深处瞎猜。绝大多数错误信息里都会明确告诉你是哪一行出了问题。

最后说一点个人感受

我自己写 JSX 到现在,最大的体会是:它降低了"数据驱动界面"的心智负担,让你把 UI 理解成一种"数据到视图的函数映射"。刚开始会觉得别扭,因为大括号里塞逻辑、标签里又套组件的写法确实和传统 HTML 不一样。但坚持写一两周以后,反而会离不开这种直观的写法——因为你几乎不用再切换上下文,UI 长什么样,代码就长什么样,改起来非常顺畅。

如果这篇文章帮到了你,不妨自己动手把上面的任务列表示例跑一遍,改一改过滤条件和数据处理逻辑,感受一下 JSX 和普通 HTML 的差异。遇到报错也没关系,把错误信息贴出来对着排查,这条路就是这么走过来的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询