这个{ title }: { title: string } 对象 :对象 是什么意思?
javascript
function MyButton({ title }: { title: string }) {
return (
<button>{title}</button>
);
}
export default function MyApp() {
return (
<div>
<h1>欢迎来到我的应用</h1>
<MyButton title="我是一个按钮" />
</div>
);
}
这段代码是 TypeScript 中一种非常常见的写法,叫做行内类型注解 。它的作用是告诉 TypeScript 编译器 MyButton 组件期望接收什么样的属性(props)。
我们可以把它拆解成两个部分来理解:{ title } 和 { title: string }。
🧩 第一部分:{ title } (解构赋值)
这是 JavaScript 的语法,叫做解构赋值。
MyButton函数接收一个参数,这个参数是一个对象,也就是 React 组件的props。{ title }的作用是从这个props对象中,直接把title这个属性"拿"出来,方便你在函数内部直接使用title变量,而不需要每次都写props.title。
举个例子:
javascript
// 不使用解构
function MyButton(props) {
// 需要通过 props.title 来访问
return <button>{props.title}</button>;
}
// 使用解构
function MyButton({ title }) {
// 可以直接使用 title
return <button>{title}</button>;
}
🏷️ 第二部分:: { title: string } (类型注解)
这是 TypeScript 的语法,叫做类型注解。
- 它紧跟在
:后面,用来定义前面{ title }这个参数的类型。 { title: string }是一个对象类型 的定义。它明确地规定了props对象必须长什么样:- 它必须是一个对象。
- 这个对象里必须有一个名为
title的属性。 title属性的值必须是string(字符串) 类型。
它的作用是进行类型检查,保证代码的健壮性。 如果你不小心传错了类型,TypeScript 会在你写代码的时候就报错。
typescript
// ✅ 正确:传入了一个 string 类型的 title
<MyButton title="我是一个按钮" />
// ❌ 错误:传入了一个 number,TypeScript 会报错
<MyButton title={123} />
✨ 总结一下
所以,function MyButton({ title }: { title: string }) 这行代码合起来的意思就是:
"我定义了一个叫
MyButton的函数,它接收一个对象参数。我会从这个对象中解构出title属性来用。同时,我要求这个传入的对象必须包含一个类型为字符串的title属性。"
💡 更优雅的写法
当组件的 props 变得复杂时,这种行内写法会显得有点乱。通常我们会把类型定义提取出来,让代码更清晰、更易维护。
typescript
// 1. 先定义一个接口 (Interface) 来描述 props 的形状
interface MyButtonProps {
title: string;
// 可以在这里添加更多属性,比如 disabled, onClick 等
}
// 2. 在组件中使用这个接口
function MyButton({ title }: MyButtonProps) {
return <button>{title}</button>;
}
这种写法在大型项目中更为常见,因为它让类型定义和组件逻辑分离开来,结构更清晰。
useState 是什么
useState 是 React 中最基础且最常用的 Hook,它的作用是为函数组件添加"状态"(state),让组件能够拥有"记忆"。
简单来说,它解决了函数组件中普通变量无法触发页面更新的问题。
🤔 为什么需要 useState?
想象一下,你想做一个计数器,点击按钮数字加 1。如果使用普通变量,代码可能是这样的:
javascript
function Counter() {
let count = 0; // 普通变量
return (
<div>
<p>计数:{count}</p>
<button onClick={() => count++}>加 1</button>
</div>
);
}
当你点击按钮时,count 的值确实会增加,但页面上的数字不会有任何变化。这是因为 React 并不知道数据已经改变了,所以它不会重新渲染组件来更新视图。
useState 就是为了解决这个问题而生的。它提供了一种方式,让你声明的变量在改变时,能够通知 React 重新渲染组件,从而实现"数据驱动视图"。
🛠️ useState 怎么用?
useState 的用法非常简洁,它接收一个初始值,并返回一个包含两个元素的数组:
javascript
import { useState } from 'react';
function Counter() {
// count 是当前状态值
// setCount 是用来更新状态的函数
const [count, setCount] = useState(0);
return (
<div>
<p>当前数值:{count}</p>
<button onClick={() => setCount(count + 1)}>加 1</button>
</div>
);
}
const [count, setCount] = useState(0);useState(0): 声明一个状态变量,并将其初始值设为0。count: 当前状态的值,你可以在 JSX 中直接使用它来显示。setCount: 一个函数,必须 通过调用它来更新count的值。直接给count赋值是无效的。
✨ 核心特性
理解 useState 的核心特性,能帮你写出更高质量的 React 代码。
1. 状态更新是异步的
当你调用 setCount 时,状态并不会立即更新。React 为了性能优化,会将短时间内的多次状态更新合并成一次处理。
javascript
const handleClick = () => {
setCount(count + 1);
console.log(count); // 输出的是旧值,而不是更新后的值
};
在这个例子中,console.log 打印的仍然是更新前的 count 值。
2. 依赖旧状态时,使用函数式更新
当新状态的计算依赖于前一个状态时(比如连续累加),直接使用 setCount(count + 1) 可能会因为异步更新而导致结果不符合预期。这时,应该使用函数式更新。
javascript
// 反例:连续调用三次,但可能只加了 1
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// 正例:使用函数式更新,保证每次都是基于最新的状态进行计算
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
prevCount 参数会接收到最新的状态值,确保更新的准确性。
3. 惰性初始化
如果初始状态需要通过一个耗时的计算才能得到,你可以传入一个函数。这个函数只会在组件首次渲染时执行一次,避免每次渲染都重复计算,从而提升性能。
javascript
// expensiveCalc() 是一个耗时的计算函数
const [state, setState] = useState(() => expensiveCalc());
4. 状态是私有的
useState 创建的状态是组件私有的。如果你在页面上渲染了同一个组件两次,它们各自的状态是完全独立、互不影响的。
📌 总结
你可以把 useState 理解为给组件加了一个"记忆"功能。
useState(初始值):创建一份记忆。- 状态值 (如
count):读取记忆的内容。 - 更新函数 (如
setCount):修改记忆的内容,并告诉组件"嘿,我的记忆变了,你该重新渲染一下了!"。
如果 useState 的内容是一个对象 ,用法上大体相同,但在更新数据 时需要特别注意一个核心原则:不可变性 (Immutability)。
简单来说,你不能直接修改对象里的属性,而是必须创建一个新对象来替换旧对象。
🛠️ 基本用法
初始化时,直接把对象传进去即可:
javascript
import { useState } from 'react';
function UserProfile() {
// 初始值是一个对象
const [user, setUser] = useState({
name: '张三',
age: 18,
city: '北京'
});
// ... 组件逻辑
}
⚠️ 常见错误:直接修改属性
这是新手最容易犯的错误。如果你直接修改对象的属性,React 不会检测到变化,页面也就不会更新。
javascript
const handleUpdate = () => {
// ❌ 错误写法:直接修改属性
user.age = 19;
setUser(user); // React 发现引用地址没变,认为数据没变,不更新页面
};
✅ 正确写法:创建新对象
要更新对象中的某个属性,你需要使用 展开运算符 (...) 来复制旧对象,并覆盖你要修改的属性。
javascript
const handleUpdate = () => {
// ✅ 正确写法:创建一个新对象
setUser({
...user, // 1. 把旧对象的所有属性复制过来
age: 19 // 2. 覆盖或添加新的属性
});
};
💡 进阶技巧:函数式更新
如果更新逻辑依赖于旧状态(或者为了代码更严谨),推荐使用函数式更新。这样可以避免闭包陷阱,确保拿到的是最新的状态。
javascript
const handleUpdate = () => {
setUser(prevUser => ({
...prevUser, // 展开旧状态
age: prevUser.age + 1 // 基于旧状态计算新值
}));
};
📦 处理嵌套对象
如果对象里还有对象(嵌套结构),你需要逐层展开,不能只展开最外层。
假设数据结构是这样的:
javascript
const [settings, setSettings] = useState({
theme: 'dark',
user: {
name: '李四',
vip: false
}
});
如果要修改深层的 vip 属性,必须这样写:
javascript
setSettings(prev => ({
...prev, // 展开第一层 (settings)
user: { // 创建新的 user 对象
...prev.user, // 展开第二层 (user)
vip: true // 修改具体属性
}
}));
📌 总结对比
| 操作 | 错误写法 (直接修改) | 正确写法 (不可变更新) |
|---|---|---|
| 修改属性 | user.age = 20; setUser(user); |
setUser({ ...user, age: 20 }); |
| 添加属性 | user.newProp = 'x'; setUser(user); |
setUser({ ...user, newProp: 'x' }); |
| 删除属性 | delete user.age; setUser(user); |
const { age, ...newUser } = user; setUser(newUser); |
核心口诀: 把 State 里的对象当成"只读"的,每次更新都要"克隆"一个新的出来。