跳转到内容

React JSX、表单处理与生命周期

建议学习 50-70 分钟更新于 2026/5/20

学习 React 时,JSX、表单处理、生命周期经常会连在一起出现。

JSX 负责描述界面,表单负责接收用户输入,生命周期负责解释组件从创建、更新到销毁的过程。把这三块串起来之后,再看 React 组件就会清晰很多。

这篇笔记主要整理三个问题:

  • JSX 到底是什么,和 HTML 有什么区别。
  • React 中表单数据应该怎么处理。
  • 生命周期在类组件和函数组件中分别怎么理解。

JSX 是 JavaScript 的语法扩展。

它看起来像 HTML,但本质上仍然是 JavaScript。React 使用 JSX 来描述组件最终要渲染出来的 UI。

比如下面这个组件:

function App() {
const name = 'Veyliss'
return <h1>Hello, {name}</h1>
}
export default App

这里的 <h1>Hello, {name}</h1> 就是 JSX。

浏览器本身并不能直接识别 JSX,它需要经过构建工具转换成普通的 JavaScript。可以简单理解为:JSX 是 React 为了让 UI 描述更直观而提供的一种写法。

JSX 虽然长得像 HTML,但写法上有一些规则需要特别注意。

组件返回的 JSX 必须有一个根节点。

如果有多个相邻元素,可以使用一个外层元素包起来:

function App() {
return (
<div>
<h1>React</h1>
<p>JSX 让组件结构更直观。</p>
</div>
)
}

如果不想额外生成一个真实 DOM,也可以使用 Fragment:

function App() {
return (
<>
<h1>React</h1>
<p>JSX 让组件结构更直观。</p>
</>
)
}

JSX 中所有标签都必须闭合。

function App() {
return (
<div>
<input type="text" />
<img src="/logo.png" alt="logo" />
</div>
)
}

在 HTML 中,有些标签可以不写闭合符号。但在 JSX 中不行,因为 JSX 更接近 JavaScript 表达式,结构必须完整。

JSX 中不能直接使用 class,而是使用 className

labelfor 属性也要写成 htmlFor

function LoginForm() {
return (
<form className="login-form">
<label htmlFor="username">用户名</label>
<input id="username" type="text" />
</form>
)
}

原因是 classfor 在 JavaScript 中有特殊含义。JSX 最终会变成 JavaScript,所以 React 使用了更贴近 JavaScript 的属性名。

JSX 中可以使用 {} 插入 JavaScript 表达式。

function UserCard() {
const user = {
name: '小溪',
age: 18,
}
return (
<section>
<h2>{user.name}</h2>
<p>年龄:{user.age}</p>
<p>明年:{user.age + 1}</p>
</section>
)
}

注意,{} 中放的是表达式,不是完整语句。

可以这样写:

<p>{count + 1}</p>
<p>{isLogin ? '已登录' : '未登录'}</p>
<p>{user.name}</p>

不能这样写:

<p>{if (isLogin) { return '已登录' }}</p>

因为 if 是语句,不是表达式。

React 中常见的条件渲染有三种写法。

第一种是三元表达式,适合二选一的展示:

function LoginStatus({ isLogin }: { isLogin: boolean }) {
return <p>{isLogin ? '欢迎回来' : '请先登录'}</p>
}

第二种是逻辑与,适合满足条件才展示:

function MessageTip({ count }: { count: number }) {
return <div>{count > 0 && <p>你有 {count} 条新消息</p>}</div>
}

第三种是提前返回,适合页面状态差异比较大:

function UserPanel({ user }: { user: { name: string } | null }) {
if (!user) {
return <p>请先登录</p>
}
return <p>你好,{user.name}</p>
}

如果结构很简单,可以用三元表达式。如果分支内容比较多,提前返回会更清晰。

列表渲染一般使用 map

function ArticleList() {
const articles = [
{ id: 1, title: 'React JSX 基础' },
{ id: 2, title: 'React 表单处理' },
{ id: 3, title: 'React 生命周期' },
]
return (
<ul>
{articles.map((article) => (
<li key={article.id}>{article.title}</li>
))}
</ul>
)
}

key 用来帮助 React 识别列表中的每一项。它应该是稳定且唯一的值,比如数据库 ID。

尽量不要使用数组下标作为 key。如果列表会新增、删除、排序,下标会变化,可能导致组件状态错位。

JSX 中可以通过 className 添加样式类。

function Button() {
return <button className="primary-button">保存</button>
}

也可以使用 style 属性,不过它接收的是一个对象。

function Notice() {
return (
<p
style={{
color: '#2563eb',
fontSize: 16,
lineHeight: 1.7,
}}
>
这是一条提示信息。
</p>
)
}

注意,行内样式属性要使用驼峰命名,比如 fontSizebackgroundColor

React 中事件名使用驼峰命名。

function Counter() {
const handleClick = () => {
console.log('clicked')
}
return <button onClick={handleClick}>点击</button>
}

如果需要传递参数,可以包一层箭头函数:

function ArticleItem({ id }: { id: number }) {
const handleOpen = (articleId: number) => {
console.log('打开文章:', articleId)
}
return <button onClick={() => handleOpen(id)}>阅读</button>
}

不要写成下面这样:

<button onClick={handleOpen(id)}>阅读</button>

这会在组件渲染时立刻执行函数,而不是点击时执行。

表单是 React 中非常重要的一块。

用户在输入框里输入内容,本质上是在改变页面中的数据。React 的核心思想是数据驱动 UI,所以表单处理的重点就是:让输入内容和组件状态保持一致。

React 表单通常有两种处理方式:

  • 受控组件。
  • 非受控组件。

受控组件指的是表单的值由 React 的 state 控制。

最常见的写法是:

import { useState } from 'react'
function LoginForm() {
const [username, setUsername] = useState('')
return (
<form>
<label htmlFor="username">用户名</label>
<input
id="username"
type="text"
value={username}
onChange={(event) => setUsername(event.target.value)}
/>
<p>当前输入:{username}</p>
</form>
)
}
export default LoginForm

这里有两个关键点:

  • valueusername 控制。
  • onChange 负责把用户输入同步回 username

这样,输入框显示什么,完全由 React 状态决定。

受控组件的好处是数据更可控。

因为表单值都在 state 中,所以你可以很方便地做这些事情:

  • 实时校验输入是否合法。
  • 根据输入内容控制按钮是否可点击。
  • 提交时直接读取 state。
  • 重置表单时统一清空 state。

例如:

import { useState } from 'react'
function RegisterForm() {
const [username, setUsername] = useState('')
const isValid = username.trim().length >= 3
return (
<form>
<input
value={username}
onChange={(event) => setUsername(event.target.value)}
placeholder="请输入至少 3 个字符"
/>
{!isValid && <p>用户名长度不能少于 3 个字符。</p>}
<button disabled={!isValid}>注册</button>
</form>
)
}

如果表单字段很多,可以把它们放到一个对象中。

import { useState } from 'react'
function ProfileForm() {
const [form, setForm] = useState({
username: '',
email: '',
bio: '',
})
const handleChange = (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const { name, value } = event.target
setForm((prevForm) => ({
...prevForm,
[name]: value,
}))
}
return (
<form>
<input
name="username"
value={form.username}
onChange={handleChange}
placeholder="用户名"
/>
<input
name="email"
value={form.email}
onChange={handleChange}
placeholder="邮箱"
/>
<textarea
name="bio"
value={form.bio}
onChange={handleChange}
placeholder="个人介绍"
/>
</form>
)
}

这段代码的重点是 name

每个表单项通过 name 告诉 React 自己对应 state 中哪个字段,然后在 handleChange 里统一更新。

更新对象状态时,要创建新对象:

setForm((prevForm) => ({
...prevForm,
[name]: value,
}))

不要直接修改旧对象:

form.username = 'new name'
setForm(form)

直接修改旧对象可能导致 React 无法正确感知状态变化。

在 React 中,textarea 也通过 valueonChange 控制。

function BioInput() {
const [bio, setBio] = useState('')
return (
<textarea
value={bio}
onChange={(event) => setBio(event.target.value)}
placeholder="写一点介绍"
/>
)
}

select 也是同样的思路:

function LanguageSelect() {
const [language, setLanguage] = useState('javascript')
return (
<select
value={language}
onChange={(event) => setLanguage(event.target.value)}
>
<option value="javascript">JavaScript</option>
<option value="typescript">TypeScript</option>
<option value="go">Go</option>
</select>
)
}

React 表单的统一思路就是:表单显示的值来自 state,用户修改后再更新 state。

普通输入框使用 value,复选框使用 checked

import { useState } from 'react'
function Agreement() {
const [checked, setChecked] = useState(false)
return (
<label>
<input
type="checkbox"
checked={checked}
onChange={(event) => setChecked(event.target.checked)}
/>
我已阅读并同意协议
</label>
)
}

单选按钮通常多个共享同一个状态。

function GenderSelect() {
const [gender, setGender] = useState('male')
return (
<form>
<label>
<input
type="radio"
name="gender"
value="male"
checked={gender === 'male'}
onChange={(event) => setGender(event.target.value)}
/>
</label>
<label>
<input
type="radio"
name="gender"
value="female"
checked={gender === 'female'}
onChange={(event) => setGender(event.target.value)}
/>
</label>
</form>
)
}

表单提交时通常需要阻止浏览器默认刷新页面。

import { useState } from 'react'
function LoginForm() {
const [form, setForm] = useState({
username: '',
password: '',
})
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
console.log('提交表单:', form)
}
return (
<form onSubmit={handleSubmit}>
<input
value={form.username}
onChange={(event) =>
setForm((prevForm) => ({
...prevForm,
username: event.target.value,
}))
}
placeholder="用户名"
/>
<input
type="password"
value={form.password}
onChange={(event) =>
setForm((prevForm) => ({
...prevForm,
password: event.target.value,
}))
}
placeholder="密码"
/>
<button type="submit">登录</button>
</form>
)
}

如果不调用 event.preventDefault(),浏览器会按照传统表单行为提交并刷新页面,这通常不是单页应用想要的效果。

非受控组件指的是表单值不由 React state 实时控制,而是通过 DOM 自己保存。

需要读取值时,再通过 ref 获取。

import { useRef } from 'react'
function SearchForm() {
const inputRef = useRef<HTMLInputElement>(null)
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
console.log(inputRef.current?.value)
}
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} defaultValue="React" />
<button type="submit">搜索</button>
</form>
)
}

非受控组件适合一些简单场景,比如:

  • 只在提交时读取一次输入值。
  • 文件上传。
  • 接入某些非 React 的第三方库。

但如果表单需要实时校验、联动展示、动态禁用按钮,受控组件会更合适。

文件上传通常使用非受控方式。

import { useRef } from 'react'
function UploadForm() {
const fileRef = useRef<HTMLInputElement>(null)
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
const file = fileRef.current?.files?.[0]
console.log(file)
}
return (
<form onSubmit={handleSubmit}>
<input ref={fileRef} type="file" />
<button type="submit">上传</button>
</form>
)
}

文件输入框的值不能像普通文本框那样由 React 设置,所以更适合通过 ref 读取。

生命周期描述的是组件从创建到销毁经历的过程。

可以分成三个阶段:

  • 挂载:组件第一次出现在页面上。
  • 更新:组件因为 state 或 props 变化重新渲染。
  • 卸载:组件从页面上移除。

如果用更直白的话说,生命周期关心的是:

  • 组件什么时候第一次渲染。
  • 组件什么时候因为数据变化重新渲染。
  • 组件什么时候被移除。
  • 在这些时间点应该做什么事情。

在类组件中,生命周期是通过固定方法体现的。

import React from 'react'
class Counter extends React.Component {
state = {
count: 0,
}
componentDidMount() {
console.log('组件挂载完成')
}
componentDidUpdate() {
console.log('组件更新完成')
}
componentWillUnmount() {
console.log('组件即将卸载')
}
render() {
return (
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
{this.state.count}
</button>
)
}
}
export default Counter

常见生命周期方法可以这样理解:

  • componentDidMount:组件第一次渲染到页面后执行。
  • componentDidUpdate:组件更新后执行。
  • componentWillUnmount:组件卸载前执行。

类组件把不同阶段拆成不同的方法,结构清晰,但也容易让同一个业务逻辑分散在多个生命周期中。

函数组件没有类组件那种生命周期方法。

在函数组件中,更多是通过 useEffect 来处理副作用。

useEffect 不是生命周期方法本身,但它可以覆盖很多生命周期场景。

如果只想在组件第一次渲染后执行一次,可以传入空依赖数组。

import { useEffect } from 'react'
function App() {
useEffect(() => {
console.log('组件挂载后执行')
}, [])
return <div>App</div>
}

这个场景接近类组件中的 componentDidMount

适合做:

  • 首次请求数据。
  • 初始化第三方库。
  • 添加全局事件监听。

如果副作用依赖某个状态,就把这个状态放到依赖数组中。

import { useEffect, useState } from 'react'
function SearchPage() {
const [keyword, setKeyword] = useState('')
useEffect(() => {
if (!keyword.trim()) return
console.log('根据关键词请求数据:', keyword)
}, [keyword])
return (
<input
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
placeholder="搜索"
/>
)
}

这个场景可以理解为:当 keyword 变化后,执行和它相关的副作用。

它不完全等同于 componentDidUpdate,因为 useEffect 更强调“这段副作用依赖哪些数据”。

如果副作用创建了订阅、定时器、事件监听,就应该在清理函数中移除。

import { useEffect, useState } from 'react'
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth)
useEffect(() => {
const handleResize = () => {
setWidth(window.innerWidth)
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
return <p>当前窗口宽度:{width}</p>
}

清理函数会在两个时机执行:

  • 组件卸载时执行。
  • 依赖变化导致下一次副作用执行前,先清理上一次副作用。

这个场景接近类组件中的 componentWillUnmount,但它比类组件更细,因为它会清理“上一次副作用”。

useEffect 的执行时机主要看依赖数组。

useEffect(() => {
console.log('每次渲染后都会执行')
})

没有依赖数组:组件首次渲染后执行,之后每次更新后也执行。

useEffect(() => {
console.log('只在首次渲染后执行')
}, [])

空依赖数组:组件首次渲染后执行一次。

useEffect(() => {
console.log('count 变化后执行')
}, [count])

指定依赖:首次渲染后执行一次,之后依赖变化时再次执行。

可以先用下面这个表建立直觉。

类组件生命周期函数组件中的常见写法说明
componentDidMountuseEffect(() => {}, [])首次渲染后执行
componentDidUpdateuseEffect(() => {}, [deps])依赖变化后执行
componentWillUnmountuseEffect(() => { return () => {} }, [])卸载时清理

但要注意,这只是帮助理解的对应关系,不要把 useEffect 机械地当成生命周期替代品。

函数组件更推荐按照“副作用依赖什么数据”来组织逻辑,而不是按照“组件现在处于哪个生命周期”来组织逻辑。

在函数组件中请求数据,常见写法是在 useEffect 内部定义异步函数。

import { useEffect, useState } from 'react'
type Article = {
id: number
title: string
}
function ArticleList() {
const [articles, setArticles] = useState<Article[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
async function fetchArticles() {
try {
const response = await fetch('/api/articles')
const data = await response.json()
setArticles(data)
} finally {
setLoading(false)
}
}
fetchArticles()
}, [])
if (loading) {
return <p>加载中...</p>
}
return (
<ul>
{articles.map((article) => (
<li key={article.id}>{article.title}</li>
))}
</ul>
)
}

不要直接把 useEffect 的回调函数写成 async

useEffect(async () => {
const response = await fetch('/api/articles')
}, [])

这种写法不推荐,因为 async 函数返回的是 Promise,而 useEffect 期望回调函数返回 undefined 或清理函数。

如果项目开启了 React Strict Mode,在开发环境中,某些副作用可能会执行两次。

这是 React 帮你检查副作用是否安全的一种方式。生产环境不会因此重复执行。

所以写 useEffect 时,要尽量保证副作用可以被清理,避免出现:

  • 重复注册事件。
  • 重复创建定时器。
  • 请求结果回来后更新已经卸载的组件。

遇到开发环境重复执行时,不要第一反应认为 React 出错了,先检查副作用是否缺少清理逻辑。

下面这个例子把 JSX、表单处理和副作用放在一起。

import { useEffect, useState } from 'react'
type Todo = {
id: number
text: string
done: boolean
}
function TodoApp() {
const [text, setText] = useState('')
const [todos, setTodos] = useState<Todo[]>([])
useEffect(() => {
document.title = `待办数量:${todos.length}`
}, [todos.length])
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
const value = text.trim()
if (!value) return
setTodos((prevTodos) => [
...prevTodos,
{
id: Date.now(),
text: value,
done: false,
},
])
setText('')
}
const toggleTodo = (id: number) => {
setTodos((prevTodos) =>
prevTodos.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo,
),
)
}
return (
<section>
<h1>Todo List</h1>
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={(event) => setText(event.target.value)}
placeholder="输入待办事项"
/>
<button type="submit">添加</button>
</form>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
{todo.done ? <del>{todo.text}</del> : todo.text}
</label>
</li>
))}
</ul>
</section>
)
}
export default TodoApp

这个例子里:

  • JSX 描述页面结构。
  • text 控制输入框内容。
  • todos 控制列表数据。
  • 表单提交时阻止默认刷新。
  • useEffect 根据待办数量更新页面标题。

把这几个点连起来,React 的基础组件开发流程就比较完整了。

不是。

JSX 看起来像 HTML,但它本质上是 JavaScript 的语法扩展。它最终会被转换成 JavaScript 代码。

因为 JSX 更接近 JavaScript,而 class 是 JavaScript 中的关键字。React 使用 className 来表示 HTML 中的 class 属性。

不是。

如果只是简单读取一次输入值,非受控组件也可以。但如果需要校验、联动、提交控制、重置表单,受控组件通常更合适。

不完全是。

它可以覆盖很多生命周期场景,但它的核心思想是处理副作用,并根据依赖决定什么时候重新执行。

学习这三块时,可以按下面的顺序练习:

  1. 先用 JSX 写静态结构。
  2. 再用 useState 让表单可输入、可提交。
  3. 最后用 useEffect 处理请求、标题、事件监听这类副作用。

React 的很多知识看起来分散,但真正写组件时,它们通常会自然组合在一起。