Skip to content
React 基本概念:组件与 JSX
JSX 与 UI 描述
JSX 是 JavaScript 的语法扩展,允许在 .js 或 .tsx 文件中直接书写类似 HTML 的标签结构。React 组件最终返回这些标签,浏览器在渲染时会将其转换为真正的 DOM 节点。JSX 和 React 经常共同出现,但它们是两个独立的部分 —— JSX 本身是一种语法糖,Babel 等工具会将其编译为 React.createElement 调用,因此即使不使用 React,也可以在其他框架中采用 JSX 风格的语法。
直接将 HTML 复制到 JSX 中往往无法正常工作,下面说明必须遵守的规则。
JSX 语法规则
标签必须闭合
JSX 中的标签遵循 XML 规范,所有标签都必须闭合。自闭合标签需要以 /> 结尾。
jsx
// 正确:自闭合标签
<img src="avatar.png" />
// 错误:HTML 风格的不闭合写法
<img src="avatar.png">同样,普通元素必须有开始和结束标签:
jsx
// 正确
<div></div>
// 错误
<div>单一根元素
每个组件返回的 JSX 只能有一个根元素。如果想返回多个元素,需要用父元素或片段(Fragment)包裹:
jsx
// 正确
return (
<div>
<h1>Title</h1>
<p>Description</p>
</div>
);
// 错误:两个并列的根元素
return (
<h1>Title</h1>
<p>Description</p>
);片段可以写成空标签 <></>,在 DOM 中不会生成额外的节点:
jsx
return (
<>
<h1>Title</h1>
<p>Description</p>
</>
);属性名使用驼峰命名
JSX 中的属性对应 DOM 属性,而不是 HTML 属性,因此采用驼峰式命名。常见变化包括:
class→classNamefor→htmlFortabindex→tabIndex
jsx
// 正确
<div className="card" tabIndex={0}></div>
// 错误
<div class="card" tabindex="0"></div>内联样式使用对象
style 属性接收一个 JavaScript 对象,而不是字符串。CSS 属性名同样使用驼峰式:
jsx
const cardStyle = { backgroundColor: '#f0f0f0', fontSize: 16 };
return <div style={cardStyle}>...</div>;直接在 JSX 中书写:
jsx
<div style={{ marginTop: 8, color: 'blue' }}>...</div>外层 {} 是表达式插值,内层 {} 是对象字面量。
布尔属性与默认值
如果传递的属性值为 true,可以省略赋值:
jsx
<input type="checkbox" checked />等价于 checked={true}。如果值为 false,React 不会渲染该属性。
注释
JSX 中的注释必须包裹在 {/* */} 中:
jsx
<div>
{/* 这是一个注释 */}
<p>Content</p>
</div>在 JSX 中嵌入表达式
JSX 里大括号 {} 内可以嵌入任何合法的 JavaScript 表达式。这是连接标签结构和数据的关键机制。
嵌入变量与字符串拼接
jsx
const name = 'Ada';
const greeting = <h1>Hello, {name}</h1>;
// 计算也可以直接放在花括号里
const element = <p>Next year, {new Date().getFullYear() + 1}</p>;函数调用与表达式计算
jsx
function formatUser(user) {
return user.firstName + ' ' + user.lastName;
}
const user = { firstName: 'Alan', lastName: 'Turing' };
const element = <div>{formatUser(user)}</div>;花括号内部的表达式在每次渲染时重新计算,因此可以放入简单的逻辑运算,但不能放入语句(如 if 或 for)。条件判断可以使用三元运算符或逻辑短路:
jsx
const isLoggedIn = true;
const message = (
<div>
{isLoggedIn ? <UserPanel /> : <LoginButton />}
{isLoggedIn && <WelcomeBack />}
</div>
);嵌入对象(谨慎使用)
表达式也可以直接输出对象,但 React 不会渲染对象本身,通常会导致错误。对象主要用在属性传递和样式上:
jsx
const config = { size: 32, color: '#333' };
<Icon {...config} /> // 展开属性(后文 Props 部分会提到)函数组件:最小 UI 单元
React 组件本质是一个接收输入(props)并返回 JSX 的 JavaScript 函数。这种函数组件是当前推荐的定义方式。
最简单的组件:
tsx
function Greeting() {
return <h1>Hello</h1>;
}箭头函数同样可行:
tsx
const Greeting = () => <h1>Hello</h1>;为了让其他模块使用,需要导出:
tsx
export default function Greeting() {
return <h1>Hello</h1>;
}
// 或者命名导出
export function Card() {
return <div className="card">...</div>;
}组件名必须以大写字母开头,因为 JSX 在编译时通过首字母大小写区分原生标签(小写)和自定义组件(大写)。
组件可以被当作标签使用:
tsx
function App() {
return (
<div>
<Greeting />
</div>
);
}每次 <Greeting /> 出现,React 都会调用 Greeting 函数并将返回的 JSX 插入到对应位置。函数组件内部可以包含任意 JavaScript 逻辑,只要最终返回合法的 JSX:
tsx
function RandomNumber() {
const num = Math.floor(Math.random() * 100);
return <span>{num}</span>;
}数据单向流动:初识 Props
组件需要根据外部数据调整输出。React 通过 props 实现父组件向子组件传递数据。props 是一个只读的对象,子组件不应该直接修改它。
父组件使用属性(attribute)语法传递数据:
tsx
function App() {
return <UserCard name="Ada" bio="Loves coding" />;
}子组件通过函数参数接收 props:
tsx
function UserCard(props) {
return (
<div>
<h2>{props.name}</h2>
<p>{props.bio}</p>
</div>
);
}通常使用解构让代码更清晰:
tsx
function UserCard({ name, bio }) {
return (
<div>
<h2>{name}</h2>
<p>{bio}</p>
</div>
);
}传递的内容不限于字符串,可以是数字、布尔值、对象、数组甚至其他 React 元素:
tsx
<UserCard
name="Ada"
age={25}
favorites={['React', 'TypeScript']}
avatar={<img src="ada.png" />}
/>注意非字符串的值必须放在 {} 中,否则会被当作字符串处理(例如 age="25")。
因为 props 是单向传递的,数据总是从父到子,这强制了清晰的数据流向。子组件如果需要向父节点反馈信息,通常使用回调函数(属于事件处理范畴,本篇不展开)。
组件树:组合成页面
实际页面很少只由一两个组件构成。开发时,会将界面拆分成多个小组件,再组合成更大的组件,最终形成一棵组件树。
组件的拆分与复用
考虑一个用户概览卡片。一个简单的单体组件可能是这样的:
tsx
function UserProfile() {
return (
<div className="profile">
<img className="avatar" src="avatar.png" />
<div className="info">
<h2>Ada Lovelace</h2>
<p>Mathematics enthusiast</p>
</div>
</div>
);
}这种写法可以工作,但如果页面中需要多个不同用户的卡片,或者头像组件要在其他地方复用,单体组件就难以应对。拆分后每个组件可以只专注自身职责:
tsx
function Avatar({ src }) {
return <img className="avatar" src={src} alt="avatar" />;
}
function UserInfo({ name, bio }) {
return (
<div className="info">
<h2>{name}</h2>
<p>{bio}</p>
</div>
);
}
function UserCard({ name, bio, avatarSrc }) {
return (
<div className="card">
<Avatar src={avatarSrc} />
<UserInfo name={name} bio={bio} />
</div>
);
}现在 Avatar 和 UserInfo 都是可独立复用的单元。如果页面中需要展示一个群组成员列表,只需将多个 UserCard 组合起来:
tsx
function MemberList() {
return (
<div>
<UserCard name="Ada" bio="..." avatarSrc="ada.png" />
<UserCard name="Alan" bio="..." avatarSrc="alan.png" />
<UserCard name="Grace" bio="..." avatarSrc="grace.png" />
</div>
);
}这种嵌套结构形成的就是组件树。每个组件只关心自己的输入(props),其内部实现细节对外隐藏,这是组合模式的核心优势。
children:插槽式组合
有时组件的“内容”并非只有数据,还可能是另一段 JSX 结构。例如一个 Card 容器,希望外部传入任意的标题和正文内容。props 虽然可以传递 JSX,但还有一个更自然的机制:children。
在 JSX 中,标签之间的内容会被自动注入到 props.children 中:
tsx
function Card({ children }) {
return <div className="card">{children}</div>;
}
function App() {
return (
<Card>
<h2>Title</h2>
<p>Some description...</p>
</Card>
);
}这里 <Card> 和 </Card> 之间的所有内容都会作为 children 传入,Card 组件只需决定将它们放置在哪里。这种方式类似“插槽”(slot),让容器组件无需预先知道内部结构。
children 可以是任意合法的 JSX,包括文本、元素、甚至多个元素(此时 children 是一个数组)。如果要做出更精细的布局,比如一个左右分栏的页面壳子,可以结合具名 props 和 children 一起使用:
tsx
function Layout({ sidebar, children }) {
return (
<div className="layout">
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}
function App() {
return (
<Layout sidebar={<Nav />}>
<Content />
</Layout>
);
}这里 sidebar 是一个具名 prop,children 用来放置主要内容,组合起来就形成了灵活的页面骨架。
示例:组装一个用户卡片页面
以下将前述概念整合成一个可运行的示例。定义三个组件:Avatar、UserCard 和 UserList。
tsx
// Avatar.tsx
export function Avatar({ src, size = 48 }: { src: string; size?: number }) {
return (
<img
className="avatar"
src={src}
width={size}
height={size}
style={{ borderRadius: '50%' }}
/>
);
}
// UserCard.tsx
import { Avatar } from './Avatar';
interface User {
name: string;
bio: string;
avatar: string;
}
export function UserCard({ name, bio, avatar }: User) {
return (
<div className="user-card">
<Avatar src={avatar} size={64} />
<div className="user-card-body">
<h3>{name}</h3>
<p>{bio}</p>
</div>
</div>
);
}
// UserList.tsx
import { UserCard } from './UserCard';
export function UserList() {
return (
<div className="user-list">
<UserCard
name="Ada Lovelace"
bio="First programmer"
avatar="/ada.png"
/>
<UserCard
name="Alan Turing"
bio="Enigma codebreaker"
avatar="/alan.png"
/>
</div>
);
}这个示例中,UserList 通过组合两个 UserCard 构成页面,而每个 UserCard 又嵌套了 Avatar。组件拆分、props 传递以及 children 的组合思想都在这一简单结构中得以体现。
注意点
JSX 不是模板语言
花括号中的表达式是真实的 JavaScript,不是受限的 DSL。因此像 if 语句这种需要控制流的地方要改用三元表达式、逻辑与、自执行函数或者提取到组件外部。
标签闭合与大小写
HTML 中有些元素可以不闭合(如 <input>),在 JSX 中会直接编译报错。另外 class 和 for 等属性是 JavaScript 保留字,必须用 className 和 htmlFor 替代。自定义组件必须以大写字母开头,否则会被当作普通 HTML 标签。
函数组件必须是纯函数
React 要求函数组件在给定相同 props 时总是返回相同的 JSX,不应修改传入的 props,也不应在渲染期间执行副作用(如网络请求、直接操作 DOM)。这些由后续的 Hooks 和事件处理来承担。
在 TypeScript 中为 props 声明类型
使用 TypeScript 时,为组件 props 提供接口或类型可以避免属性拼写错误,并在编辑器中获得提示:
tsx
interface ButtonProps {
label: string;
disabled?: boolean;
}
function Button({ label, disabled }: ButtonProps) {
return <button disabled={disabled}>{label}</button>;
}可选属性用 ? 标记,没有默认值时为 undefined。
条件与循环
如果需要条件渲染或重复渲染,应将逻辑放在组件函数体内,而不是 JSX 深处。例如提前计算列表,或使用三元表达式。一旦 JSX 内部逻辑过于复杂,就是拆分新组件的信号。
