React#002 | 添加交互
React 的核心思想是通过组件构建用户界面,而添加交互是让这些界面变得生动和有用的关键。在本文中,我们将探讨如何为 React 应用添加交互性,包括响应事件、使用状态管理数据、组件记忆以及理解 React 的渲染机制等核心概念。
响应事件
在 React 中,你可以添加事件处理函数来响应用户交互,如点击、悬停、表单输入聚焦等操作。
添加事件处理函数
要添加事件处理函数,首先需要定义一个函数,然后将其作为 prop 传递给 JSX 标签。React 事件处理有一些特点:
- 事件处理函数以小驼峰命名(如
onClick
而非onclick
) - 通过 JSX 传递函数作为事件处理器,而不是字符串
function MyButton() {
function handleClick() {
alert('你点击了按钮!');
}
return (
<button onClick={handleClick}>
点击我
</button>
);
}
注意 onClick={handleClick}
结尾没有括号。不要调用事件处理函数,只需传递它即可。当用户点击按钮时,React 会调用你的事件处理函数。
你也可以使用箭头函数定义内联事件处理器:
<button onClick={() => {
alert('你点击了按钮!');
}}>
点击我
</button>
在事件处理函数中读取 props
事件处理函数可以读取组件的 props:
function AlertButton({ message, children }) {
return (
<button onClick={() => alert(message)}>
{children}
</button>
);
}
export default function Toolbar() {
return (
<div>
<AlertButton message="正在播放!">
播放电影
</AlertButton>
<AlertButton message="正在上传!">
上传图片
</AlertButton>
</div>
);
}
向事件处理函数传递数据
有时你需要向事件处理函数传递额外的参数。例如,如果你有一个产品列表,并希望在点击"购买"按钮时传递产品ID:
function ProductList() {
const products = [
{ id: 1, name: '苹果' },
{ id: 2, name: '橙子' }
];
function handleBuy(productId) {
console.log('购买产品:', productId);
}
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name}
<button onClick={() => handleBuy(product.id)}>
购买
</button>
</li>
))}
</ul>
);
}
阻止默认行为
在 React 中,不能通过返回 false
来阻止默认行为,必须显式调用 e.preventDefault()
:
function Form() {
function handleSubmit(e) {
e.preventDefault();
console.log('你提交了表单');
}
return (
<form onSubmit={handleSubmit}>
<input type="text" />
<button type="submit">提交</button>
</form>
);
}
这里的 e
是一个合成事件对象,React 根据 W3C 规范定义了这个对象,因此你不必担心跨浏览器兼容性问题。
事件传播
事件处理器也会捕获来自组件子元素的事件。我们称事件会在树中"冒泡"或"传播":它从事件发生的地方开始,然后沿着树向上传播。
function Toolbar() {
return (
<div className="Toolbar" onClick={() => {
alert('你点击了工具栏!');
}}>
<button onClick={() => alert('正在播放!')}>
播放电影
</button>
<button onClick={() => alert('正在上传!')}>
上传图片
</button>
</div>
);
}
如果你点击任一按钮,首先会执行它的 onClick
,然后执行父 <div>
的 onClick
。因此会显示两条消息。如果你直接点击工具栏,则只会执行父 <div>
的 onClick
。
如果你想阻止事件传播,需要调用 e.stopPropagation()
:
function Button({ onClick, children }) {
return (
<button onClick={e => {
e.stopPropagation();
onClick();
}}>
{children}
</button>
);
}
状态:组件的记忆
组件通常需要根据交互改变屏幕上显示的内容。用户在输入框中输入文本、点击按钮切换面板、导航到不同页面等操作都需要组件"记住"一些信息,这就是状态(state)的作用。
添加状态变量
要在组件中添加状态变量,需要从 React 导入 useState
Hook:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
点击了 {count} 次
</button>
);
}
useState
返回一个数组,包含两个值:
- 当前状态值(
count
) - 更新状态的函数(
setCount
)
状态的基本特性
状态有几个重要特性:
- 状态是隔离和私有的:同一个组件的多个实例各自拥有独立的状态
- 状态是组件的"记忆":状态在重新渲染之间保持不变
- 状态更新会触发重新渲染:当你更新状态时,React 会重新渲染组件
使用多个状态变量
一个组件可以有多个状态变量:
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [isValid, setIsValid] = useState(false);
// ...
}
当状态结构比较复杂时,可以考虑使用对象或数组:
const [form, setForm] = useState({
name: '',
email: ''
});
// 更新对象状态时,需要保留其他字段
function handleNameChange(e) {
setForm({
...form,
name: e.target.value
});
}
状态更新的注意事项
React 中的状态更新有一些重要特性:
- 状态更新是异步的:调用状态设置函数后,变量不会立即改变
- React 会批量处理状态更新:多个状态更新可能会被合并为一次重新渲染
- 状态应该被视为只读的:不要直接修改状态对象,而是创建新的副本
当新的状态依赖于前一个状态时,应该使用函数形式的更新:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(prevCount => prevCount + 1);
// 可以连续调用多次
setCount(prevCount => prevCount + 1);
}
return (
<button onClick={handleClick}>
点击增加
</button>
);
}
渲染和提交
当组件的状态更新时,React 会重新渲染组件。理解 React 的渲染过程对于构建高效的应用非常重要。
渲染过程
React 渲染组件的过程分为三个步骤:
- 触发渲染:初次渲染或状态更新时
- 组件渲染:React 调用组件函数获取新的 JSX
- 提交到 DOM:React 更新 DOM 以匹配新的 JSX
渲染触发条件
组件渲染有两种情况:
- 初次渲染:当应用启动时
- 状态更新时:组件或其祖先的状态被更新时
渲染不等于 DOM 更新
重要的是要理解,"渲染"并不意味着 React 会更新 DOM。React 只会计算需要对 DOM 进行的最小必要更改。
function Counter() {
const [count, setCount] = useState(0);
console.log('组件渲染了!'); // 每次状态更新都会执行
return (
<div>
<p>计数: {count}</p>
<button onClick={() => setCount(count + 1)}>
增加
</button>
</div>
);
}
即使组件函数在每次状态更新时都会执行,React 也只会更新实际发生变化的 DOM 元素(在这个例子中是计数文本)。
状态作为快照
React 中的状态更像是一个快照,而不是可变变量。设置状态不会改变已有的状态变量,而是触发一次新的渲染。
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1); // 使用的是当前渲染中的 count 值
console.log(count); // 仍然显示设置前的值
}
return (
<button onClick={handleClick}>
点击了 {count} 次
</button>
);
}
这种行为可能看起来很奇怪,但它有助于避免许多常见的错误。理解状态的这种"快照"特性对于正确处理异步代码至关重要。
在一次事件中多次更新状态
如果你需要在同一个事件中多次更新同一个状态,可以使用函数形式的更新:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(c => c + 1); // 使用前一个状态值
setCount(c => c + 1); // 使用更新后的状态值
setCount(c => c + 1); // 使用更新后的状态值
}
return (
<button onClick={handleClick}>
+3
</button>
);
}
更新对象和数组状态
虽然 React 中的状态是不可变的,但这并不意味着你不能存储对象和数组。只是在更新它们时,需要创建新的对象或数组,而不是修改现有的。
更新对象
function Form() {
const [person, setPerson] = useState({
name: '张三',
age: 25
});
function handleNameChange(e) {
setPerson({
...person, // 复制其他字段
name: e.target.value // 更新特定字段
});
}
return (
<input
value={person.name}
onChange={handleNameChange}
/>
);
}
更新数组
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: '学习 React' },
{ id: 2, text: '找工作' }
]);
function handleAddTodo() {
const newTodo = { id: todos.length + 1, text: '新任务' };
setTodos([...todos, newTodo]); // 创建新数组
}
function handleRemoveTodo(id) {
setTodos(todos.filter(todo => todo.id !== id));
}
function handleUpdateTodo(id, newText) {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
));
}
return (
<>
<button onClick={handleAddTodo}>添加任务</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => handleRemoveTodo(todo.id)}>
删除
</button>
<button onClick={() => handleUpdateTodo(todo.id, todo.text + ' (已更新)')}>
更新
</button>
</li>
))}
</ul>
</>
);
}
常见的数组操作
以下是一些常见的数组操作及其在 React 中的不可变更新方式:
- 添加元素:使用展开运算符
[...arr, newItem]
或[newItem, ...arr]
- 删除元素:使用
filter()
方法arr.filter(item => item.id !== itemToRemove.id)
- 更新元素:使用
map()
方法arr.map(item => item.id === itemToUpdate.id ? { ...item, ...updates } : item)
- 替换元素:结合
slice()
和展开运算符 - 排序元素:先复制数组,再排序
[...arr].sort()
Immer 简化不可变更新
对于复杂的嵌套对象或数组,使用扩展运算符进行不可变更新可能会变得很繁琐。这时可以考虑使用 Immer 库来简化更新逻辑:
import { useImmer } from 'use-immer';
function TodoList() {
const [todos, updateTodos] = useImmer([
{ id: 1, text: '学习 React', done: false },
{ id: 2, text: '找工作', done: false }
]);
function handleToggleTodo(id) {
updateTodos(draft => {
const todo = draft.find(t => t.id === id);
todo.done = !todo.done;
});
}
// ...
}
Immer 让你可以编写看起来像是直接修改对象的代码,但实际上它会在内部创建一个新的不可变副本。
表单处理
在 React 中处理表单是一个常见的需求,通常我们会使用"受控组件"模式。
受控组件
在 HTML 中,表单元素如 <input>
、<textarea>
和 <select>
通常维护自己的状态。在 React 中,我们通常将表单元素的状态存储在组件的 state 中:
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
function handleSubmit(e) {
e.preventDefault();
console.log('提交的数据:', { name, email });
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
placeholder="姓名"
/>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="邮箱"
/>
<button type="submit">提交</button>
</form>
);
}
这种方式的优点是:
- 可以立即对用户输入进行验证
- 可以有条件地禁用提交按钮
- 可以强制输入格式
处理多个输入
当有多个输入字段时,可以使用一个对象来存储所有字段的值:
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
function handleChange(e) {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value
});
}
function handleSubmit(e) {
e.preventDefault();
console.log('提交的数据:', formData);
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="姓名"
/>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="邮箱"
/>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
placeholder="留言"
/>
<button type="submit">提交</button>
</form>
);
}
总结
在本文中,我们探讨了 React 中添加交互的核心概念:
- 响应事件:通过事件处理函数响应用户交互
- 使用状态:通过
useState
Hook 为组件添加"记忆" - 理解渲染过程:触发渲染、组件渲染和提交到 DOM
- 状态作为快照:理解 React 中状态的不可变特性
- 更新对象和数组:正确处理复杂状态的更新
- 表单处理:使用受控组件管理表单数据
掌握这些概念是构建交互式 React 应用的基础。随着你对 React 的深入理解,你将能够创建更加复杂和高效的用户界面。