React JSX、表单处理与生命周期
学习 React 时,JSX、表单处理、生命周期经常会连在一起出现。
JSX 负责描述界面,表单负责接收用户输入,生命周期负责解释组件从创建、更新到销毁的过程。把这三块串起来之后,再看 React 组件就会清晰很多。
这篇笔记主要整理三个问题:
- JSX 到底是什么,和 HTML 有什么区别。
- React 中表单数据应该怎么处理。
- 生命周期在类组件和函数组件中分别怎么理解。
JSX 是什么
Section titled “JSX 是什么”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 的基本规则
Section titled “JSX 的基本规则”JSX 虽然长得像 HTML,但写法上有一些规则需要特别注意。
必须有一个根节点
Section titled “必须有一个根节点”组件返回的 JSX 必须有一个根节点。
如果有多个相邻元素,可以使用一个外层元素包起来:
function App() { return ( <div> <h1>React</h1> <p>JSX 让组件结构更直观。</p> </div> )}如果不想额外生成一个真实 DOM,也可以使用 Fragment:
function App() { return ( <> <h1>React</h1> <p>JSX 让组件结构更直观。</p> </> )}标签必须闭合
Section titled “标签必须闭合”JSX 中所有标签都必须闭合。
function App() { return ( <div> <input type="text" /> <img src="/logo.png" alt="logo" /> </div> )}在 HTML 中,有些标签可以不写闭合符号。但在 JSX 中不行,因为 JSX 更接近 JavaScript 表达式,结构必须完整。
使用 className 和 htmlFor
Section titled “使用 className 和 htmlFor”JSX 中不能直接使用 class,而是使用 className。
label 的 for 属性也要写成 htmlFor。
function LoginForm() { return ( <form className="login-form"> <label htmlFor="username">用户名</label> <input id="username" type="text" /> </form> )}原因是 class 和 for 在 JavaScript 中有特殊含义。JSX 最终会变成 JavaScript,所以 React 使用了更贴近 JavaScript 的属性名。
在 JSX 中使用 JavaScript 表达式
Section titled “在 JSX 中使用 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> )}注意,行内样式属性要使用驼峰命名,比如 fontSize、backgroundColor。
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这里有两个关键点:
value由username控制。onChange负责把用户输入同步回username。
这样,输入框显示什么,完全由 React 状态决定。
为什么推荐受控组件
Section titled “为什么推荐受控组件”受控组件的好处是数据更可控。
因为表单值都在 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> )}处理多个表单字段
Section titled “处理多个表单字段”如果表单字段很多,可以把它们放到一个对象中。
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 无法正确感知状态变化。
textarea 和 select
Section titled “textarea 和 select”在 React 中,textarea 也通过 value 和 onChange 控制。
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。
checkbox 和 radio
Section titled “checkbox 和 radio”普通输入框使用 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 读取。
生命周期是什么
Section titled “生命周期是什么”生命周期描述的是组件从创建到销毁经历的过程。
可以分成三个阶段:
- 挂载:组件第一次出现在页面上。
- 更新:组件因为 state 或 props 变化重新渲染。
- 卸载:组件从页面上移除。
如果用更直白的话说,生命周期关心的是:
- 组件什么时候第一次渲染。
- 组件什么时候因为数据变化重新渲染。
- 组件什么时候被移除。
- 在这些时间点应该做什么事情。
类组件生命周期
Section titled “类组件生命周期”在类组件中,生命周期是通过固定方法体现的。
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:组件卸载前执行。
类组件把不同阶段拆成不同的方法,结构清晰,但也容易让同一个业务逻辑分散在多个生命周期中。
函数组件中的生命周期理解
Section titled “函数组件中的生命周期理解”函数组件没有类组件那种生命周期方法。
在函数组件中,更多是通过 useEffect 来处理副作用。
useEffect 不是生命周期方法本身,但它可以覆盖很多生命周期场景。
首次渲染后执行
Section titled “首次渲染后执行”如果只想在组件第一次渲染后执行一次,可以传入空依赖数组。
import { useEffect } from 'react'
function App() { useEffect(() => { console.log('组件挂载后执行') }, [])
return <div>App</div>}这个场景接近类组件中的 componentDidMount。
适合做:
- 首次请求数据。
- 初始化第三方库。
- 添加全局事件监听。
依赖变化后执行
Section titled “依赖变化后执行”如果副作用依赖某个状态,就把这个状态放到依赖数组中。
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,但它比类组件更细,因为它会清理“上一次副作用”。
不同依赖数组的执行时机
Section titled “不同依赖数组的执行时机”useEffect 的执行时机主要看依赖数组。
useEffect(() => { console.log('每次渲染后都会执行')})没有依赖数组:组件首次渲染后执行,之后每次更新后也执行。
useEffect(() => { console.log('只在首次渲染后执行')}, [])空依赖数组:组件首次渲染后执行一次。
useEffect(() => { console.log('count 变化后执行')}, [count])指定依赖:首次渲染后执行一次,之后依赖变化时再次执行。
useEffect 和生命周期的对应关系
Section titled “useEffect 和生命周期的对应关系”可以先用下面这个表建立直觉。
| 类组件生命周期 | 函数组件中的常见写法 | 说明 |
|---|---|---|
componentDidMount | useEffect(() => {}, []) | 首次渲染后执行 |
componentDidUpdate | useEffect(() => {}, [deps]) | 依赖变化后执行 |
componentWillUnmount | useEffect(() => { return () => {} }, []) | 卸载时清理 |
但要注意,这只是帮助理解的对应关系,不要把 useEffect 机械地当成生命周期替代品。
函数组件更推荐按照“副作用依赖什么数据”来组织逻辑,而不是按照“组件现在处于哪个生命周期”来组织逻辑。
请求数据示例
Section titled “请求数据示例”在函数组件中请求数据,常见写法是在 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 或清理函数。
开发环境中的重复执行
Section titled “开发环境中的重复执行”如果项目开启了 React Strict Mode,在开发环境中,某些副作用可能会执行两次。
这是 React 帮你检查副作用是否安全的一种方式。生产环境不会因此重复执行。
所以写 useEffect 时,要尽量保证副作用可以被清理,避免出现:
- 重复注册事件。
- 重复创建定时器。
- 请求结果回来后更新已经卸载的组件。
遇到开发环境重复执行时,不要第一反应认为 React 出错了,先检查副作用是否缺少清理逻辑。
一个完整的小例子
Section titled “一个完整的小例子”下面这个例子把 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 吗
Section titled “JSX 是 HTML 吗”不是。
JSX 看起来像 HTML,但它本质上是 JavaScript 的语法扩展。它最终会被转换成 JavaScript 代码。
为什么 JSX 中要用 className
Section titled “为什么 JSX 中要用 className”因为 JSX 更接近 JavaScript,而 class 是 JavaScript 中的关键字。React 使用 className 来表示 HTML 中的 class 属性。
表单一定要用受控组件吗
Section titled “表单一定要用受控组件吗”不是。
如果只是简单读取一次输入值,非受控组件也可以。但如果需要校验、联动、提交控制、重置表单,受控组件通常更合适。
useEffect 是生命周期吗
Section titled “useEffect 是生命周期吗”不完全是。
它可以覆盖很多生命周期场景,但它的核心思想是处理副作用,并根据依赖决定什么时候重新执行。
学习这三块时,可以按下面的顺序练习:
- 先用 JSX 写静态结构。
- 再用
useState让表单可输入、可提交。 - 最后用
useEffect处理请求、标题、事件监听这类副作用。
React 的很多知识看起来分散,但真正写组件时,它们通常会自然组合在一起。