React 组件是一个返回 JSX 的 JavaScript 函数:一段可复用的 UI,比如一个按钮、一张卡片,甚至整个页面。你像使用 HTML 标签一样使用它,比如 <Profile />,React 会调用这个函数,弄清楚要在屏幕上显示什么。应用就是由一个个组件层层嵌套搭建起来的。
App 渲染了两次 Profile,每个 Profile 又渲染一个 Avatar。修改 Avatar 里的首字母缩写,两个资料卡会一起更新,因为它们共用同一份定义。现在列表里的每个人都一样;下一页 props 会讲如何给每份副本传入各自的数据。
编写组件
函数组件有两个要求:
- 名字以大写字母开头:写
Profile,而不是profile。 - 返回 React 能渲染的内容:JSX、字符串、数字、
null(什么都不渲染),或者由这些组成的数组。
function Welcome() {
return <h1>Welcome back</h1>;
}
// Arrow functions work the same way
const Welcome = () => <h1>Welcome back</h1>;
当 JSX 跨越多行时,要在 return 后面用括号把它包起来。没有括号的话,return 后面紧跟换行会直接返回 undefined,后面的内容全部被忽略。
为什么组件名必须大写
JSX 会看标签的首字母。小写表示内置的 HTML 元素,所以 <div> 依然是 <div>。大写表示“作用域中的一个变量”,所以 <Profile /> 会调用你的 Profile 函数。如果组件名用小写,React 会创建一个同名的未知 HTML 元素:
第二行只显示了标签文字,控制台里也只有 Greeting ran。React 往页面里放了一个空的 <greeting> 标签,从没调用那个小写的函数。开发版本还会警告 The tag <greeting> is unrecognized in this browser;预览按生产版本运行,所以你只会看到文字缺失。把函数改名为 Greetings,标签改成 <Greetings />,问题就解决了。
组合组件
组件天生就是用来嵌套的。页面是一棵树:最上面是 App,往下是各个布局部分,再往下是更小的部件,直到单个按钮。每个组件只知道自己直接渲染的那些部分,所以你可以一次只考虑一个组件。
每个 LikeButton 都有自己的状态,所以给一条帖子点赞不会影响其他帖子。想给每条帖子加一个分享按钮,只需写一次,然后在 Post 里加上 <ShareButton />。
什么时候拆分组件
没有固定的规则,但有几个信号说明某一块值得独立成组件:同样的标记出现在两个地方,JSX 的某一部分有自己的状态(比如上面的点赞按钮),或者函数长到要滚动才能看到 return。按这些界线拆分,能让每个组件小到一屏就能读完。拆得太早也没什么代价:组件只是函数,再合并回去很容易。
导出和导入组件
这里的编辑器只有一个文件,但真实项目会把组件放到各自的文件里。一个文件可以有一个默认导出和任意数量的命名导出:
// Avatar.jsx
export default function Avatar({ initials }) {
return <div className="avatar">{initials}</div>;
}
// Badge.jsx
export function Badge({ label }) {
return <span className="badge">{label}</span>;
}
export function NewBadge() {
return <Badge label="new" />;
}
// Profile.jsx
import Avatar from './Avatar.jsx';
import { Badge, NewBadge } from './Badge.jsx';
export default function Profile() {
return (
<div>
<Avatar initials="AL" />
<Badge label="admin" />
<NewBadge />
</div>
);
}
默认导出可以用任意名字导入;命名导出要放在花括号里,并使用完全相同的名字。团队通常选定一种风格并坚持下去。文件名一般和组件同名:Avatar.jsx 导出 Avatar。
保持组件纯净
React 要求组件在渲染时表现得像纯函数:相同的 props 和 state 得到相同的 JSX,而且渲染不会改变它运行之前就已存在的任何东西。React 可能会在你无法控制的时机多次渲染一个组件,所以在渲染期间修改外部变量的组件,每次都会产生不同的输出。
不纯的杯子一开始是 1、2、3。点击按钮:客人没有任何变化,不纯的列表却变成了 4、5、6,因为每次渲染都给共享变量 guest 加了数。纯的杯子从 props 拿到编号,始终正确。在开发环境中,Strict Mode 会把每个组件渲染两次,专门暴露这类 bug;预览按生产版本运行,所以这里只有再次渲染时才会看到编号偏移。
修改东西没问题,只是不要在渲染期间修改。在事件处理函数里更新状态,在副作用(Effect)里和外部世界打交道(定时器、订阅、文档标题)。
组件与元素
组件是函数本身,即 Profile。元素是你写下 <Profile /> 时 JSX 创建的东西:一个小对象,意思是“用这些 props 渲染 Profile”。React 创建元素,调用元素指向的组件,再把结果变成 DOM。你永远不要自己用 Profile() 调用组件;写 <Profile />,让 React 去调用它,这样 React 才能跟踪它的状态和它在树中的位置。
这个位置很重要。下面的示例在一个组件内部定义了另一个组件,这是个常见错误:
输入一些文字,然后点击按钮:你输入的文字消失了。App 每次渲染都会创建一个全新的 NameField 函数,React 看到那个位置上是不同类型的组件,就把旧的连同它的状态一起丢掉。把 NameField 移到 App 上方,放在文件顶层,文字就能在每次点击后保留下来。渲染一页讲解了 React 如何决定保留什么。
函数组件和类组件
在 React 16.8 引入 Hook 之前,需要状态的组件要写成类:
import { Component } from 'react';
class Welcome extends Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
类组件在 React 19 中仍然可用,你会在老代码库里遇到它们,错误边界也仍然必须用类来写。新代码使用函数组件和 useState、useEffect 等 Hook,用更少的代码完成同样的工作。类组件一页把类的每个功能都对应到了相应的 Hook。
常见问题
React 中的组件是什么?
组件是一段可复用的 UI,写成一个返回 JSX 的 JavaScript 函数。你像使用 HTML 标签一样使用它,比如 <Profile />,React 会调用这个函数,弄清楚要画出什么。
为什么 React 组件名必须以大写字母开头?
JSX 根据首字母判断一个标签是什么。小写标签如 <profile /> 会被当成名为 profile 的 HTML 元素,你的函数根本不会被调用。大写标签如 <Profile /> 才指向你的组件。
函数组件和类组件有什么区别?
两者都用来描述 UI。函数组件是普通函数,用 Hook 处理状态和副作用;类组件继承 React.Component,使用 render、componentDidMount 等方法。新代码都用函数组件。
React 中的纯组件是什么?
纯组件在 props 和 state 相同时总是返回相同的 JSX,并且渲染过程中不修改自身以外的任何东西。React 依赖这一点,才能按需多次渲染组件而不出意外。
可以在一个组件里面定义另一个组件吗?
最好不要。在组件内部定义的组件每次渲染都是一个新函数,React 每次都会把它当成不同的组件,并重置它的状态。请把组件定义在文件的顶层。