JSX 语法学习知识点(第 8 - 11 天)
第 8 天:JSX 基本语法
元素创建
JSX 允许我们像编写 HTML 一样创建 React 元素,它本质上是 JavaScript 的语法扩展。例如:
const element = <h1>Hello, JSX!</h1>;
这种写法既简洁又直观,React 会将 JSX 编译为 React.createElement() 函数调用,上述代码会被编译为:
const element = React.createElement('h1', null, 'Hello, JSX!');
属性定义
在 JSX 中定义元素属性时,有些属性名与 HTML 中的有所不同,需要特别注意:
- HTML 中的
class属性在 JSX 中需写为className,因为class是 JavaScript 的关键字。例如:<div className="container">Content</div>。 - HTML 中的
for属性在 JSX 中需写为htmlFor,同理,for也是 JavaScript 的关键字。例如:<label htmlFor="username">Username:</label>。 - 其他大多数属性名与 HTML 一致,如
id、src、href等,例如:<img src="image.jpg" alt="Example" />、<a href="https://example.com">Link</a>。
嵌入变量和表达式
在 JSX 中,我们可以通过 {} 来嵌入变量、表达式,实现动态内容渲染。
嵌入变量
先定义变量,然后在 JSX 中用 {} 包裹变量名。例如:
const name = "Alice";
const element = <h1>Hello, {name}!</h1>; // 渲染结果为 <h1>Hello, Alice!</h1>
嵌入表达式
可以在 {} 中放入任意的 JavaScript 表达式,如算术运算、函数调用等。例如:
const a = 10;
const b = 20;
const element = <p>Sum: {a + b}</p>; // 渲染结果为 <p>Sum: 30</p>
function getGreeting(name) {
return `Hello, ${name}!`;
}
const greetingElement = <h1>{getGreeting("Bob")}</h1>; // 渲染结果为 <h1>Hello, Bob!</h1>
第 9 天:JSX 条件渲染和列表渲染基础
条件渲染方式
在 JSX 中,我们可以根据不同的条件渲染不同的内容,常用的方式有以下几种:
-
三元表达式:通过
condition ? exprIfTrue : exprIfFalse的形式进行条件判断。例如:const isLoggedIn = true; const element = isLoggedIn ? <p>Welcome back!</p> : <p>Please log in.</p>; // 当 isLoggedIn 为 true 时渲染 <p>Welcome back!</p>,否则渲染 <p>Please log in.</p> -
逻辑与运算符(&&):当条件为真时,渲染后面的元素;当条件为假时,不渲染。例如:
const hasMessage = true; const message = "This is a message"; const element = hasMessage && <p>{message}</p>; // 当 hasMessage 为 true 时渲染 <p>This is a message</p>,否则不渲染 -
变量赋值:先根据条件将不同的元素赋值给变量,然后在 JSX 中使用该变量。例如:
let element; const isAdmin = false; if (isAdmin) { element = <p>Admin Dashboard</p>; } else { element = <p>User Dashboard</p>; }
列表渲染基础
使用 map 方法遍历简单数组,可以生成 JSX 元素列表。例如:
const fruits = ["Apple", "Banana", "Orange"];
const fruitList = fruits.map((fruit) => <li>{fruit}</li>);
const element = <ul>{fruitList}</ul>;
上述代码会渲染出一个包含三个列表项的无序列表,每个列表项分别显示数组中的水果名称。map 方法会对数组中的每个元素进行处理,返回一个新的 JSX 元素,最终将这些元素组成的数组嵌入到 <ul> 元素中。
第 10 天:深入学习列表渲染
为元素添加唯一 key 属性
在使用 map 方法进行列表渲染时,需要为每个生成的元素添加唯一的 key 属性。key 是 React 用于识别列表中元素的唯一标识,它帮助 React 高效地更新 DOM。例如:
const fruits = [
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Orange" }
];
const fruitList = fruits.map((fruit) => (
<li key={fruit.id}>{fruit.name}</li>
));
const element = <ul>{fruitList}</ul>;
这里使用水果对象的 id 作为 key,因为 id 是唯一的。如果数组中没有唯一的 id,也可以使用元素的索引作为 key,但不推荐在列表可能重新排序的情况下使用索引作为 key,因为这可能会导致性能问题和状态错误。
key 的作用和重要性
- 作用:
key帮助 React 确定哪些元素发生了变化、被添加或被移除,从而避免不必要的 DOM 操作,提高渲染性能。 - 重要性:如果没有正确设置
key,React 可能会错误地更新 DOM 元素,导致界面显示异常。例如,当列表中的元素顺序发生变化时,没有key或key不唯一会导致 React 无法准确识别元素,可能会重新创建所有元素,而不是只移动它们的位置。
处理复杂数据结构的列表渲染
当数组中的元素是复杂对象(包含嵌套对象或数组)时,我们可以通过逐层访问对象的属性来进行渲染。例如:
const users = [
{
id: 1,
name: "Alice",
contact: {
email: "alice@example.com",
phone: "123-456-7890"
},
hobbies: ["Reading", "Music"]
},
{
id: 2,
name: "Bob",
contact: {
email: "bob@example.com",
phone: "987-654-3210"
},
hobbies: ["Sports", "Gaming"]
}
];
const userList = users.map((user) => (
<div key={user.id} className="user-card">
<h3>{user.name}</h3>
<p>Email: {user.contact.email}</p>
<p>Phone: {user.contact.phone}</p>
<p>Hobbies:</p>
<ul>
{user.hobbies.map((hobby, index) => (
<li key={index}>{hobby}</li>
))}
</ul>
</div>
));
const element = <div>{userList}</div>;
在这个例子中,我们遍历 users 数组,每个用户对象包含 contact 嵌套对象和 hobbies 数组。我们通过 user.contact.email 访问嵌套对象的属性,通过再次使用 map 方法遍历 user.hobbies 数组来渲染爱好列表,这里因为 hobbies 数组没有唯一 id,所以使用索引作为 key。
第 11 天:JSX 样式添加和注释写法
内联样式添加
在 JSX 中,内联样式通过对象形式定义,并且使用 style={{}} 来应用。样式对象的属性名采用驼峰式命名法(如 fontSize 而不是 font-size)。例如:
const style = {
color: "blue",
fontSize: "18px",
fontWeight: "bold",
backgroundColor: "#f0f0f0",
padding: "10px"
};
const element = <div style={style}>This has inline styles</div>;
也可以直接在 style 属性中定义样式对象:
const element = (
<p style={{ color: "red", textDecoration: "underline" }}>
This is a styled paragraph
</p>
);
外部样式添加
我们可以将样式定义在外部 CSS 文件中,然后通过 import 导入,再使用 className 引用样式类。
首先创建外部 CSS 文件(如 styles.css):
.container {
width: 80%;
margin: 0 auto;
border: 1px solid #ccc;
padding: 20px;
}
.title {
color: green;
text-align: center;
}
然后在 JSX 文件中导入并使用:
import './styles.css';
const element = (
<div className="container">
<h2 className="title">Using External Styles</h2>
<p>This is content with external styles.</p>
</div>
);
JSX 中的注释写法
在 JSX 中,注释需要写在 {/* */} 中。例如:
const element = (
<div>
{/* 这是一个注释 */}
<h1>Hello, JSX!</h1>
{/*
这是一个
多行注释
*/}
<p>This is a paragraph.</p>
</div>
);
需要注意的是,注释必须写在 JSX 元素内部或元素之间,不能写在 {} 外部的 JavaScript 代码区域。
React 组件学习知识点(第 11 - 15 天)
第 11 天:函数组件的定义、导出导入与使用
函数组件的定义
函数组件是使用 JavaScript 函数定义的 React 组件,它是 React 中最基础也最常用的组件形式之一。
-
通过
function关键字创建:函数名即为组件名,且组件名必须首字母大写,这是 React 识别组件的约定。函数可以返回 JSX 元素作为组件内容。例如:function Button() { return <button>Click Me</button>; } -
通过箭头函数创建:箭头函数创建的组件同样需要遵循首字母大写的命名规范,返回值为 JSX 元素。例如:
const Text = () => { return <p>This is a text component</p>; };
组件的导出与导入
导出组件:要在其他文件中使用组件,需要先将组件导出。常用的导出方式有默认导出(export default)和命名导出(export)。
-
默认导出:一个文件中只能有一个默认导出,导入时可以自定义组件名。例如:
// Button.js function Button() { return <button>Click Me</button>; } export default Button; -
命名导出:一个文件中可以有多个命名导出,导入时必须使用对应的名称。例如:
// Text.js export const Text = () => { return <p>This is a text component</p>; };
导入组件:在需要使用组件的文件中,通过 import 语句导入组件。
-
导入默认导出的组件:
// App.js import Button from './Button'; -
导入命名导出的组件:需要使用大括号包裹组件名。
// App.js import { Text } from './Text';
在父组件中使用子组件
在父组件中,导入子组件后,就可以像使用 HTML 标签一样使用子组件。例如,在 App 父组件中使用 Button 和 Text 子组件:
// App.js
import Button from './Button';
import { Text } from './Text';
function App() {
return(
<div className="app">
<Text />
<Button />
</div>
);
}
export default App;
上述代码中,App 是父组件,Text 和 Button 是子组件,父组件通过在 JSX 中写入子组件标签的方式使用子组件,实现了简单的信息展示区域。
第 12 天:类组件的定义、导出导入、使用及与函数组件的对比
类组件的定义
类组件是使用 ES6 类语法定义的 React 组件,它需要继承 React.Component 类,并在类中实现 render() 方法,render() 方法返回组件的 JSX 内容。例如:
import React from 'react';
class ClassButton extends React.Component {
render() {
return <button>Class Component Button</button>;
}
}
类组件的导出与导入
类组件的导出和导入方式与函数组件类似。
-
导出类组件:
- 默认导出:
// ClassButton.js import React from 'react'; class ClassButton extends React.Component { render() { return <button>Class Component Button</button>; } } export default ClassButton; - 命名导出:
// ClassText.js import React from 'react'; export class ClassText extends React.Component { render() { return <p>Class Component Text</p>; } }
- 默认导出:
-
导入类组件:
- 导入默认导出的类组件:
// App.js import ClassButton from './ClassButton'; - 导入命名导出的类组件:
// App.js import { ClassText } from './ClassText';
- 导入默认导出的类组件:
类组件的使用
在父组件中使用类组件的方式与使用函数组件相同,导入后在 JSX 中使用组件标签。例如:
// App.js
import ClassButton from './ClassButton';
import { ClassText } from './ClassText';
function App() {
return (
<div>
<ClassText />
<ClassButton />
</div>
);
}
函数组件和类组件的异同
相同点:
- 都是 React 组件,用于构建 UI 界面,都可以接收 Props 传递数据。
- 都遵循 React 的组件生命周期相关特性(函数组件通过 Hooks 实现类似功能)。
- 都需要遵循组件命名规范(首字母大写),且返回的 JSX 结构需有一个根元素。
不同点:
| 特性 | 函数组件 | 类组件 |
|---|---|---|
| 定义方式 | 使用函数定义 | 使用类定义且继承 React.Component |
| 状态管理 | 通过 Hooks(如 useState)管理状态 | 通过 this.state 和 this.setState() 管理 |
| 生命周期 | 通过 useEffect 等 Hooks 实现 | 有明确的生命周期方法(如 componentDidMount) |
| this 指向 | 没有 this | this 指向组件实例,需注意绑定 |
| 代码简洁性 | 代码更简洁 | 代码相对繁琐 |
第 13 - 14 天:组件的拆分原则与嵌套组合
组件的拆分原则
将复杂页面拆分为多个小型组件时,需遵循以下原则:
- 单一职责原则:一个组件只负责完成一个特定的功能或展示一块相关的内容,避免组件功能过于庞大和复杂。例如,电商首页中的导航栏只负责导航功能,不应包含商品展示功能。
- 复用性原则:拆分出的组件应具有一定的复用性,能够在应用的多个地方被使用。例如,按钮组件、输入框组件等基础组件可以在多个页面中复用。
- 逻辑性原则:根据业务逻辑和数据关系进行拆分,将相关的逻辑和数据放在同一个组件中。例如,商品列表组件应包含商品数据的展示和相关操作逻辑。
- UI 相关性原则:将视觉上紧密相关的元素放在同一个组件中,保持 UI 结构的完整性。例如,商品卡片组件应包含商品图片、名称、价格等视觉上相关的元素。
复杂页面的拆分示例
以电商首页为例,可将其拆分为以下组件:
- 导航栏组件(Navbar):包含网站 logo、导航菜单、搜索框、购物车图标等。
- 轮播图组件(Carousel):展示首页的轮播图片和相关链接。
- 商品列表组件(ProductList):展示多个商品卡片,负责商品数据的遍历和展示。
- 商品卡片组件(ProductCard):作为 ProductList 的子组件,展示单个商品的图片、名称、价格、添加购物车按钮等。
- 页脚组件(Footer):包含网站版权信息、联系方式、链接等。
组件的嵌套组合
拆分后的组件通过嵌套组合的方式形成完整的页面。父组件可以包含多个子组件,子组件也可以包含自己的子组件,形成组件树结构。例如,电商首页组件(HomePage)的嵌套组合:
// HomePage.js
import Navbar from './Navbar';
import Carousel from './Carousel';
import ProductList from './ProductList';
import Footer from './Footer';
function HomePage() {
return (
<div className="home-page">
<Navbar />
<Carousel />
<ProductList />
<Footer />
</div>
);
}
// ProductList.js
import ProductCard from './ProductCard';
function ProductList() {
const products = [/* 商品数据 */];
return (
<div className="product-list">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
在上述代码中,HomePage 是父组件,包含 Navbar、Carousel、ProductList、Footer 子组件;ProductList 作为父组件,包含多个 ProductCard 子组件,通过这种嵌套组合方式实现了电商首页的完整页面结构。
第 15 天:组件的命名规范和文件组织方式
组件的命名规范
- 组件名必须首字母大写:这是 React 区分组件和普通 HTML 标签的重要约定,React 会将首字母大写的标签识别为组件。例如
Button、ProductCard是合法的组件名,button、productCard则会被当作普通标签处理。 - 使用 PascalCase 命名法:组件名由多个单词组成时,每个单词的首字母都大写,如
UserProfile、ShoppingCart,使组件名更具可读性。 - 组件名应清晰表意:组件名应准确反映组件的功能或用途,让人一眼就能了解组件的作用。例如
Navbar表示导航栏组件,LoginForm表示登录表单组件。
组件的文件组织方式
- 一个组件对应一个文件:通常情况下,每个组件单独放在一个文件中,方便管理和维护。例如
Button.js文件中只包含 Button 组件。 - 文件名与组件名一致:文件名采用与组件名相同的 PascalCase 命名法,使文件和组件之间的对应关系清晰明了。例如 Button 组件放在
Button.js文件中,ProductList 组件放在ProductList.js文件中。 - 按功能模块组织文件夹:将相关的组件放在同一个文件夹中,形成功能模块。例如,电商应用中可以创建
components/navbar、components/product、components/footer等文件夹,分别存放导航栏相关组件、商品相关组件、页脚相关组件。
index.js 在组件导入中的便捷作用
在组件文件夹中创建 index.js 文件,可以简化组件的导入路径。index.js 文件通常用于导出文件夹中的主要组件,这样在导入组件时,只需指定文件夹路径,而无需指定具体的组件文件路径。例如:
组件文件夹结构:
components/
Button/
Button.js
index.js
Button.js 文件:
function Button() {
return <button>Click Me</button>;
}
export default Button;
index.js 文件:
export { default } from './Button';
导入组件时:
// 无需写成 import Button from './components/Button/Button';
import Button from './components/Button';
通过这种方式,当组件文件路径发生变化时,只需修改 index.js 文件中的导出路径,而无需修改所有导入该组件的地方,提高了代码的可维护性。
React Props 与 State 学习知识点(第 18 - 25 天)
第 18 - 19 天:深入学习 Props
父组件向子组件传递 Props
Props 是组件间传递数据的主要方式,由父组件向子组件单向传递。传递方式为在子组件标签上添加属性,属性值可以是任意 JavaScript 数据类型(字符串、数字、对象、数组、函数等)。
- 传递基础类型:
// 父组件
function Parent() {
const name = "React";
const version = 18;
return (
<Child
title="Props 示例"
libName={name}
currentVersion={version}
/>
);
}
// 子组件
function Child(props) {
return (
<div>
<h3>{props.title}</h3>
<p>库名:{props.libName}</p>
<p>版本:{props.currentVersion}</p>
</div>
);
}
- 传递复杂类型(对象/数组):
// 父组件
function Parent() {
const user = { name: "Alice", age: 25 };
const skills = ["React", "JavaScript", "CSS"];
return <Child userInfo={user} skillList={skills} />;
}
// 子组件
function Child(props) {
return (
<div>
<p>姓名:{props.userInfo.name}</p>
<p>技能:{props.skillList.join(", ")}</p>
</div>
);
}
- 传递函数(实现子组件向父组件通信):
// 父组件
function Parent() {
const handleClick = (message) => {
console.log("子组件传递的消息:", message);
};
return <Child onButtonClick={handleClick} />;
}
// 子组件
function Child(props) {
return (
<button onClick={() => props.onButtonClick("Hello Parent")}>
点击向父组件发送消息
</button>
);
}
子组件接收 Props 的方式
- 直接使用
props参数:子组件函数的第一个参数即为 props 对象,通过.语法访问属性。 - 解构赋值:简化代码,直接提取需要的属性(推荐)。
// 解构单个属性
function Child({ title }) {
return <h3>{title}</h3>;
}
// 解构多个属性
function Child({ userInfo, skillList }) {
return (
<div>
<p>姓名:{userInfo.name}</p>
<p>技能:{skillList.join(", ")}</p>
</div>
);
}
Props 是只读的特性
React 规定 Props 是只读的(read-only),子组件绝对不能修改接收的 props。如果需要修改数据,应通过父组件更新后重新传递,否则会导致不可预测的 bug。
// 错误示例:子组件试图修改 props
function Child(props) {
const handleClick = () => {
props.count = props.count + 1; // 报错!Props 不可修改
};
return <button onClick={handleClick}>错误修改</button>;
}
第 20 - 21 天:Props 的默认值与类型检查
Props 默认值(defaultProps)
当父组件未传递某个 props 时,可通过 defaultProps 设置默认值,确保组件在缺少 props 时仍能正常工作。
- 函数组件设置默认值:
function ProductCard({ name, price }) {
return (
<div>
<p>商品名:{name}</p>
<p>价格:{price} 元</p>
</div>
);
}
// 设置默认值
ProductCard.defaultProps = {
name: "未知商品",
price: 0
};
// 使用时未传递 props,将显示默认值
<ProductCard /> // 商品名:未知商品,价格:0 元
- 类组件设置默认值:
class ProductCard extends React.Component {
render() {
const { name, price } = this.props;
return (
<div>
<p>商品名:{name}</p>
<p>价格:{price} 元</p>
</div>
);
}
}
// 类组件通过静态属性设置默认值
ProductCard.defaultProps = {
name: "未知商品",
price: 0
};
Props 类型检查(PropTypes)
使用 prop-types 库可以对 props 的类型进行校验,在开发环境中捕获类型错误,提高代码健壮性。
- 安装依赖:
npm install prop-types --save
- 函数组件类型检查:
import PropTypes from 'prop-types';
function User({ name, age, isStudent }) {
return (
<div>
<p>姓名:{name}</p>
<p>年龄:{age}</p>
<p>是否学生:{isStudent ? "是" : "否"}</p>
</div>
);
}
// 类型检查配置
User.propTypes = {
name: PropTypes.string.isRequired, // 字符串类型,必须传递
age: PropTypes.number, // 数字类型
isStudent: PropTypes.bool // 布尔类型
};
- 常用类型检查规则:
Component.propTypes = {
// 基础类型
str: PropTypes.string,
num: PropTypes.number,
bool: PropTypes.bool,
func: PropTypes.func,
arr: PropTypes.array,
obj: PropTypes.object,
// 特定结构的对象
user: PropTypes.shape({
name: PropTypes.string,
age: PropTypes.number
}),
// 枚举类型(只能是指定值)
status: PropTypes.oneOf(['active', 'inactive', 'deleted']),
// 多种类型之一
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
// 必须传递
requiredProp: PropTypes.any.isRequired
};
- 类组件类型检查:与函数组件写法一致,在类外部通过
类名.propTypes定义。
第 22 - 23 天:函数组件中 State 的使用(useState)
useState 基本用法
useState 是 React 提供的 Hook,用于在函数组件中管理状态(State)。状态是组件内部维护的数据,变化时会触发组件重新渲染。
- 定义状态:调用
useState(初始值),返回一个数组[当前状态, 更新函数]。
import { useState } from 'react';
function Counter() {
// 定义计数状态,初始值为 0
const [count, setCount] = useState(0);
return (
<div>
<p>计数:{count}</p>
<button onClick={() => setCount(count + 1)}>加 1</button>
</div>
);
}
- 初始值的特殊性:初始值只在组件首次渲染时生效,后续渲染会忽略。如果初始值需要通过计算得到,可传递函数(惰性初始化):
// 惰性初始化:函数返回值作为初始值,只执行一次
const [count, setCount] = useState(() => {
return localStorage.getItem('count') || 0; // 从本地存储读取初始值
});
状态更新的方式
- 直接传递新值:适用于简单类型(数字、字符串、布尔值)。
setCount(10); // 直接设置新值
setName("Bob");
- 基于前一个状态计算新值:当新状态依赖于旧状态时,应传递更新函数(确保获取最新状态)。
// 错误示例:可能获取到旧状态
<button onClick={() => setCount(count + 1)}>加 1</button>
// 正确示例:通过函数获取前一个状态
<button onClick={() => setCount(prevCount => prevCount + 1)}>加 1</button>
- 更新对象/数组状态:状态为对象或数组时,必须返回新的引用(不能直接修改原状态),否则 React 可能无法检测到变化。
// 管理对象状态
const [user, setUser] = useState({ name: "Alice", age: 25 });
// 正确更新:复制旧对象,修改属性(使用展开运算符)
setUser(prev => ({ ...prev, age: prev.age + 1 }));
// 管理数组状态
const [todos, setTodos] = useState([]);
// 正确添加元素:返回新数组
setTodos(prev => [...prev, "新任务"]);
// 正确删除元素:过滤出需要保留的元素(返回新数组)
setTodos(prev => prev.filter(item => item !== "要删除的任务"));
状态更新触发重渲染
当调用更新函数(如 setCount)时,React 会将新状态存入队列,然后重新渲染组件,使用新状态计算 UI。这是组件响应用户交互的核心机制。
function Toggle() {
const [isVisible, setIsVisible] = useState(false);
return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>
{isVisible ? "隐藏" : "显示"}
</button>
{isVisible && <p>我是可以显示/隐藏的内容</p>}
</div>
);
}
// 点击按钮时,isVisible 状态切换,组件重新渲染,内容显示或隐藏
第 24 - 25 天:类组件中 State 的使用
类组件中定义状态(this.state)
类组件通过 this.state 定义初始状态,必须在 constructor 中初始化,且值为对象。
class Counter extends React.Component {
constructor(props) {
super(props); // 必须调用父类构造函数
// 初始化状态
this.state = {
count: 0,
message: "Hello"
};
}
render() {
return <p>计数:{this.state.count}</p>;
}
}
更新状态(this.setState())
类组件通过 this.setState() 方法更新状态,不能直接修改 this.state(如 this.state.count = 1 是错误的)。
- 基本用法:传递对象或函数,React 会合并新状态到
this.state。
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
handleIncrement = () => {
// 传递对象更新状态
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>计数:{this.state.count}</p>
<button onClick={this.handleIncrement}>加 1</button>
</div>
);
}
}
- this.setState() 是异步操作:React 会批量处理状态更新,确保性能优化。因此,不能在
setState后立即获取最新状态。
// 错误示例:无法获取最新状态
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 输出旧值,因为 setState 是异步的
- 依赖前一个状态的更新:当新状态依赖旧状态时,必须传递函数(与 useState 类似),函数接收前一个状态和 props,返回新状态。
// 正确:通过函数获取前一个状态
this.setState((prevState, props) => {
return { count: prevState.count + 1 };
});
- 更新对象状态:
setState会合并对象(只更新指定属性,其他属性保留):
this.state = {
user: { name: "Alice", age: 25 },
theme: "light"
};
// 只更新 user 的 age,其他状态不变
this.setState({
user: { ...this.state.user, age: 26 } // 注意:需要手动复制旧对象
});
类组件与函数组件状态管理对比
| 特性 | 函数组件(useState) | 类组件(this.state) |
|---|---|---|
| 定义方式 | const [state, setState] = useState(0) | this.state = { count: 0 }(构造函数) |
| 更新方式 | setState(newValue) 或函数式更新 | this.setState({ count: 1 }) 或函数式更新 |
| 状态合并 | 不会自动合并,需手动处理对象/数组 | 自动合并顶层属性(深层对象需手动合并) |
this 依赖 | 无(无需处理 this 绑定) | 需注意函数的 this 绑定(如箭头函数) |
| 代码简洁性 | 更简洁,减少模板代码 | 相对繁琐,需遵循类语法规范 |
以上内容详细覆盖了 Props 和 State 的核心知识点,包括传递方式、特性、状态管理方法等。建议结合每日实践任务动手练习,尤其是状态更新的正确方式(避免直接修改)和 Props 类型检查的实际应用,这对编写健壮的 React 代码非常重要。如果有具体场景的疑问,可以随时补充说明!
React Router 学习知识点(第 26 - 31 天)
第 26 天:React Router 基础与基本路由配置
安装 React Router
React Router 是 React 生态中用于处理路由的库,最常用的是适用于 Web 应用的 react-router-dom。安装命令:
npm install react-router-dom
核心组件及作用
React Router 的核心功能通过以下组件实现:
-
BrowserRouter:路由的根组件,用于包裹整个应用,维护路由历史记录(基于 HTML5 History API)。
通常在入口文件(如index.js)中使用,包裹App组件:import { BrowserRouter } from 'react-router-dom'; ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById('root') ); -
Route:定义路由规则,通过
path属性指定 URL 路径,component属性指定对应路径渲染的组件。
示例:import { Route } from 'react-router-dom'; import Home from './pages/Home'; import List from './pages/List'; function App() { return ( <div> {/* 当 URL 为 '/' 时,渲染 Home 组件 */} <Route path="/" component={Home} /> {/* 当 URL 为 '/list' 时,渲染 List 组件 */} <Route path="/list" component={List} /> </div> ); } -
Link:用于创建路由链接,类似 HTML 中的
<a>标签,但不会导致页面刷新(单页应用特性)。
通过to属性指定跳转路径:import { Link } from 'react-router-dom'; function Navbar() { return ( <nav> <Link to="/">首页</Link> <Link to="/list">列表页</Link> </nav> ); }
基本路由配置示例
实现首页、列表页的路由跳转:
// App.js
import { Route, Link } from 'react-router-dom';
import Home from './pages/Home';
import List from './pages/List';
function App() {
return (
<div>
{/* 导航链接 */}
<nav>
<Link to="/">首页</Link> |
<Link to="/list">列表页</Link>
</nav>
{/* 路由规则 */}
<Route path="/" component={Home} />
<Route path="/list" component={List} />
</div>
);
}
第 27 天:Route 精确匹配与 Switch 组件
Route 的 path 和 component 属性
path:定义路由匹配的 URL 路径(字符串),如/、/list。component:指定路径匹配时渲染的组件,组件会接收路由相关的 props(如history、location、match)。
精确匹配(exact 属性)
默认情况下,Route 的 path 是“包含匹配”(只要 URL 包含 path 就会渲染)。例如,path="/" 会匹配所有 URL(因为所有 URL 都以 / 开头),导致多个组件同时渲染。
使用 exact 属性可开启“精确匹配”,只有 URL 完全等于 path 时才渲染:
// 仅当 URL 严格为 '/' 时,才渲染 Home 组件
<Route exact path="/" component={Home} />
// URL 为 '/list' 时渲染 List 组件(不受 exact 影响,因为没有子路径)
<Route path="/list" component={List} />
Switch 组件的作用
Switch 组件用于包裹多个 Route,只渲染第一个匹配的路由,解决路由匹配的优先级问题(避免多个路由同时渲染)。
示例:
import { Route, Switch } from 'react-router-dom';
function App() {
return (
<Switch>
{/* 精确匹配 '/',优先级最高 */}
<Route exact path="/" component={Home} />
{/* 匹配 '/list' */}
<Route path="/list" component={List} />
{/* 若以上路由都不匹配,渲染 404 组件(放最后) */}
<Route component={NotFound} />
</Switch>
);
}
注意:
Switch内的Route匹配顺序从前往后,应将更具体的路径(如/list)放在前面,模糊路径(如/)放在后面。
第 28 - 29 天:动态路由参数
定义动态路由参数
在 Route 的 path 中,通过 :paramName 定义动态参数(如 ID、名称等),用于匹配变化的 URL 片段。
示例:定义商品详情页路由,接收 id 参数:
// 动态参数 id 表示商品唯一标识
<Route path="/product/:id" component={ProductDetail} />
此时,URL 如 /product/123、/product/456 都会匹配该路由,123、456 即为动态参数 id 的值。
通过 useParams 获取动态参数
在函数组件中,使用 useParams Hook 从动态路由中提取参数。步骤:
- 从
react-router-dom导入useParams。 - 在组件中调用
useParams(),返回包含参数的对象。
示例(商品详情页):
import { useParams } from 'react-router-dom';
function ProductDetail() {
// 获取动态参数 id
const { id } = useParams();
return (
<div>
<h2>商品详情</h2>
<p>商品 ID:{id}</p>
{/* 根据 id 请求对应商品数据并渲染 */}
</div>
);
}
实现详情页跳转
在列表页中,通过 Link 跳转至详情页,并传递动态参数:
// 商品列表页
function ProductList() {
const products = [
{ id: 1, name: "商品 A" },
{ id: 2, name: "商品 B" }
];
return (
<ul>
{products.map(product => (
<li key={product.id}>
{/* 跳转至详情页,传递 id 参数 */}
<Link to={`/product/${product.id}`}>
{product.name}
</Link>
</li>
))}
</ul>
);
}
第 30 天:嵌套路由
嵌套路由的概念
嵌套路由用于在父组件中嵌入子路由,实现页面局部刷新(父组件不变,子组件切换)。例如:首页(父组件)中包含推荐模块、热门模块(子组件),通过子路由切换显示。
配置嵌套路由的步骤
- 父组件中定义子路由:在父组件的 JSX 中,使用
Route定义子路由,path为“父路径/子路径”。 - 父组件中预留子路由渲染位置:子组件会渲染在父组件中
Route所在的位置。
示例:首页(Home)中嵌套推荐(Recommend)和热门(Hot)子模块:
// 父组件:Home.js
import { Route, Link, Switch } from 'react-router-dom';
import Recommend from './Recommend';
import Hot from './Hot';
function Home() {
return (
<div>
<h2>首页</h2>
{/* 子路由导航 */}
<div>
<Link to="/home/recommend">推荐</Link> |
<Link to="/home/hot">热门</Link>
</div>
{/* 子路由渲染位置 */}
<Switch>
<Route path="/home/recommend" component={Recommend} />
<Route path="/home/hot" component={Hot} />
</Switch>
</div>
);
}
// 根路由配置(App.js)
<Route path="/home" component={Home} />
此时,访问 /home/recommend 会在 Home 组件中渲染 Recommend 组件,访问 /home/hot 会渲染 Hot 组件。
第 31 天:编程式导航与路由守卫
编程式导航(useHistory Hook)
useHistory Hook 用于在函数组件中通过代码实现路由跳转(非 Link 标签跳转),返回的 history 对象包含以下常用方法:
push(path):跳转到指定路径(新增历史记录,可通过浏览器回退返回)。replace(path):替换当前路径(不新增历史记录,无法回退到上一页)。goBack():回退到上一页。
示例:
import { useHistory } from 'react-router-dom';
function Login() {
const history = useHistory();
const handleLogin = () => {
// 模拟登录成功
localStorage.setItem('token', 'xxx');
// 跳转到首页(编程式导航)
history.push('/home');
// 若需要替换当前页(如登录页不希望回退),可使用:
// history.replace('/home');
};
return (
<div>
<button onClick={handleLogin}>登录</button>
<button onClick={() => history.goBack()}>返回</button>
</div>
);
}
路由守卫的基本概念
路由守卫用于控制路由的访问权限(如未登录用户不能访问个人中心),在路由跳转前进行验证,符合条件才允许访问,否则跳转至登录页等。
用高阶组件实现简单路由守卫
高阶组件(HOC)是一个函数,接收组件作为参数,返回一个新组件。通过 HOC 实现登录验证守卫:
import { Redirect, Route } from 'react-router-dom';
// 定义路由守卫高阶组件
function withAuth(Component) {
return function AuthComponent(props) {
// 检查是否登录(从本地存储获取 token)
const isLogin = !!localStorage.getItem('token');
if (isLogin) {
// 已登录,渲染目标组件
return <Component {...props} />;
} else {
// 未登录,重定向到登录页
return <Redirect to="/login" />;
}
};
}
// 使用守卫保护个人中心路由
<Route
path="/profile"
component={withAuth(Profile)} // 只有登录后才能访问 Profile 组件
/>
说明:
Redirect组件用于路由重定向,to属性指定跳转路径。
以上内容覆盖了 React Router 的核心用法,从基础路由配置到动态参数、嵌套路由和权限控制。建议结合实际场景练习(如电商网站的商品列表→详情页跳转、需要登录的个人中心),加深对路由逻辑的理解。如果有具体场景的疑问,可以进一步探讨!
React 状态管理学习知识点(第 32 - 39 天)
第 32 - 33 天:Context API 基本使用
什么是 Context API?
Context API 是 React 内置的状态管理方案,用于解决跨组件数据传递(避免 props 层层传递的“props drilling”问题)。适用于中小型应用的简单状态共享。
核心步骤:创建 → 提供 → 消费
1. 创建上下文(createContext)
使用 createContext 函数创建一个上下文对象,可指定默认值(当组件未被 Provider 包裹时使用)。
// 创建上下文(ThemeContext.js)
import { createContext } from 'react';
// 默认值为 "light"
const ThemeContext = createContext("light");
export default ThemeContext;
2. 提供状态(Provider 组件)
通过 Context 的 Provider 组件包裹子组件树,使用 value 属性提供共享状态,所有子组件(无论层级)都能访问该状态。
// 父组件(App.js)
import ThemeContext from './ThemeContext';
import Child from './Child';
function App() {
const theme = "dark"; // 共享的状态
return (
// 提供主题状态,所有子组件可访问
<ThemeContext.Provider value={theme}>
<div>
<Child />
</div>
</ThemeContext.Provider>
);
}
3. 消费状态(两种方式)
方式 1:Consumer 组件
通过 Context.Consumer 组件的回调函数获取状态,适用于类组件或函数组件。
// 子组件(Child.js)
import ThemeContext from './ThemeContext';
function Child() {
return (
<ThemeContext.Consumer>
{/* 回调函数的参数即为 Provider 提供的 value */}
{(theme) => <p>当前主题:{theme}</p>}
</ThemeContext.Consumer>
);
}
方式 2:useContext Hook(推荐)
函数组件中使用 useContext Hook 直接获取状态,代码更简洁。
// 子组件(Child.js)
import { useContext } from 'react';
import ThemeContext from './ThemeContext';
function Child() {
// 直接获取上下文状态
const theme = useContext(ThemeContext);
return <p>当前主题:{theme}</p>;
}
跨组件共享示例
多层级组件共享用户信息:
// UserContext.js
import { createContext } from 'react';
export const UserContext = createContext(null);
// 顶层组件
function App() {
const user = { name: "Alice", age: 25 };
return (
<UserContext.Provider value={user}>
<Parent />
</UserContext.Provider>
);
}
// 中间组件(无需传递 props)
function Parent() {
return <Child />;
}
// 深层子组件(直接获取用户信息)
function Child() {
const user = useContext(UserContext);
return <p>用户名:{user.name}</p>;
}
第 34 - 35 天:Context API 状态更新与适用场景
Context 状态更新
Context 本身不提供状态更新机制,需结合 useState 或 useReducer 实现。核心思路:在 Provider 中定义修改状态的方法,并通过 value 传递给子组件,子组件调用方法更新状态。
示例:实现主题切换功能
// ThemeContext.js
import { createContext, useState } from 'react';
const ThemeContext = createContext();
// 封装 Provider 组件,集中管理状态和更新方法
export function ThemeProvider({ children }) {
// 定义状态和更新方法
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
setTheme(prev => prev === "light" ? "dark" : "light");
};
// 提供状态和方法(包装成对象)
const value = {
theme,
toggleTheme
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
export default ThemeContext;
子组件使用更新方法:
// 子组件
import { useContext } from 'react';
import ThemeContext from './ThemeContext';
function ThemeToggle() {
// 获取状态和更新方法
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<div>
<p>当前主题:{theme}</p>
<button onClick={toggleTheme}>切换主题</button>
</div>
);
}
// 顶层组件使用 ThemeProvider 包裹
function App() {
return (
<ThemeProvider>
<ThemeToggle />
</ThemeProvider>
);
}
适用场景与局限性
适用场景
- 中小型应用的简单状态共享(如主题、用户登录状态)。
- 状态变化频率低的场景。
- 跨少量层级组件的数据传递。
局限性
- 性能问题:当 Context 的
value变化时,所有消费该 Context 的组件都会强制重渲染,即使不需要该状态的组件也可能被牵连。 - 复杂状态管理困难:无法像 Redux 那样中间件(如日志、异步处理),不适合管理包含大量状态或复杂状态逻辑的应用。
- 调试不便:缺乏状态追踪工具,难以定位状态更新问题。
第 36 天:Redux 核心概念
Redux 是独立于 React 的状态管理库,基于“单一数据源”和“单向数据流”设计,适用于复杂应用的状态管理。
核心概念
-
Store:
- 定义:存储整个应用状态的唯一容器(一个应用只有一个 Store)。
- 作用:提供方法获取状态(
getState())、分发动作(dispatch(action))、监听状态变化(subscribe(listener))。
-
Action:
- 定义:描述“发生了什么”的普通 JavaScript 对象,必须包含
type字段(字符串常量,描述动作类型)。 - 示例:
// 计数器加 1 的动作 { type: 'INCREMENT' } // 带额外数据的动作(如添加商品) { type: 'ADD_TODO', payload: '学习 Redux' } - 注意:Action 本身不修改状态,只描述变化。
- 定义:描述“发生了什么”的普通 JavaScript 对象,必须包含
-
Reducer:
- 定义:根据 Action 计算新状态的纯函数(
(state, action) => newState)。 - 规则:
- 不能修改原状态(必须返回新对象/数组)。
- 无副作用(不调用 API、不修改外部变量)。
- 相同输入必须返回相同输出。
- 示例(计数器 Reducer):
function counterReducer(state = 0, action) { switch (action.type) { case 'INCREMENT': return state + 1; // 返回新状态 case 'DECREMENT': return state - 1; default: return state; // 默认返回原状态 } }
- 定义:根据 Action 计算新状态的纯函数(
-
单向数据流:
Redux 状态更新遵循严格的单向流程:1. 组件触发 Action(如用户点击按钮)→ 2. 通过 dispatch 分发 Action → 3. Reducer 根据 Action 计算新状态 → 4. Store 更新状态 → 5. 组件感知状态变化并重新渲染
第 37 - 38 天:Redux 使用流程
步骤 1:安装 Redux
npm install redux
步骤 2:创建 Reducer
定义处理状态更新的纯函数,初始化状态并根据 Action 类型返回新状态。
// reducers/counterReducer.js
// 初始状态
const initialState = { count: 0 };
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 }; // 复制原状态,修改 count
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'SET_COUNT':
return { ...state, count: action.payload }; // 使用 action 携带的数据
default:
return state;
}
}
export default counterReducer;
步骤 3:定义 Action Type 和 Action Creator
- Action Type:用常量定义动作类型,避免拼写错误。
- Action Creator:返回 Action 的函数,简化 Action 创建。
// actions/counterActions.js
// Action Type 常量
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const SET_COUNT = 'SET_COUNT';
// Action Creator
export function increment() {
return { type: INCREMENT };
}
export function decrement() {
return { type: DECREMENT };
}
export function setCount(value) {
return {
type: SET_COUNT,
payload: value // 携带额外数据
};
}
步骤 4:创建 Store
使用 createStore 函数创建 Store,传入 Reducer。
// store/index.js
import { createStore } from 'redux';
import counterReducer from '../reducers/counterReducer';
// 创建 Store
const store = createStore(counterReducer);
export default store;
步骤 5:使用 Store(获取/更新状态)
store.getState():获取当前状态。store.dispatch(action):分发 Action 触发状态更新。store.subscribe(listener):监听状态变化(通常在 React 中由 react-redux 自动处理)。
示例:
import store from './store';
import { increment, setCount } from './actions/counterActions';
// 初始状态
console.log(store.getState()); // { count: 0 }
// 分发 Action 更新状态
store.dispatch(increment());
console.log(store.getState()); // { count: 1 }
store.dispatch(setCount(10));
console.log(store.getState()); // { count: 10 }
// 监听状态变化
store.subscribe(() => {
console.log('状态更新:', store.getState());
});
第 39 天:react-redux 与 redux-thunk
react-redux:连接 React 与 Redux
react-redux 是官方库,简化 Redux 与 React 组件的连接,核心组件和 Hook 如下:
1. Provider 组件
将 Store 传递给整个 React 应用,使所有组件可访问 Store。
// index.js
import { Provider } from 'react-redux';
import store from './store';
import App from './App';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
2. 组件连接方式
方式 1:useSelector 与 useDispatch(推荐,函数组件)
useSelector(selector):从 Store 中提取状态(类似mapStateToProps)。useDispatch():获取dispatch方法,用于分发 Action。
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './actions/counterActions';
function Counter() {
// 提取 count 状态
const count = useSelector(state => state.count);
// 获取 dispatch 方法
const dispatch = useDispatch();
return (
<div>
<p>计数:{count}</p>
<button onClick={() => dispatch(increment())}>加 1</button>
<button onClick={() => dispatch(decrement())}>减 1</button>
</div>
);
}
方式 2:connect 高阶组件(支持类组件)
connect(mapStateToProps, mapDispatchToProps)(Component):将状态和 dispatch 映射到组件 props。
import { connect } from 'react-redux';
import { increment } from './actions/counterActions';
function Counter({ count, increment }) {
return (
<div>
<p>计数:{count}</p>
<button onClick={increment}>加 1</button>
</div>
);
}
// 映射状态到 props
const mapStateToProps = (state) => ({
count: state.count
});
// 映射 dispatch 到 props
const mapDispatchToProps = {
increment // 等价于 () => dispatch(increment())
};
// 连接组件
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
redux-thunk:处理异步 Action
Redux 默认只支持同步 Action,redux-thunk 中间件允许 Action Creator 返回函数(而非对象),用于处理异步操作(如 API 请求)。
步骤 1:安装并配置
npm install redux-thunk
配置中间件:
// store/index.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import counterReducer from '../reducers/counterReducer';
// 应用 thunk 中间件
const store = createStore(counterReducer, applyMiddleware(thunk));
export default store;
步骤 2:创建异步 Action Creator
返回的函数接收 dispatch 和 getState 作为参数,完成异步操作后分发同步 Action。
// actions/dataActions.js
// 异步获取数据的 Action Creator
export function fetchData() {
// 返回函数(由 thunk 处理)
return async (dispatch) => {
try {
// 1. 分发“开始请求”的 Action
dispatch({ type: 'FETCH_START' });
// 2. 异步请求(如 API 调用)
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// 3. 请求成功,分发“获取数据”的 Action
dispatch({ type: 'FETCH_SUCCESS', payload: data });
} catch (error) {
// 4. 请求失败,分发“请求失败”的 Action
dispatch({ type: 'FETCH_ERROR', payload: error.message });
}
};
}
步骤 3:组件中使用异步 Action
import { useDispatch, useSelector } from 'react-redux';
import { fetchData } from './actions/dataActions';
function DataComponent() {
const { data, loading, error } = useSelector(state => state);
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(fetchData())}>获取数据</button>
{loading && <p>加载中...</p>}
{error && <p>错误:{error}</p>}
{data && <pre>{JSON.stringify(data)}</pre>}
</div>
);
}
总结
- Context API:适合简单状态共享,实现简单但性能有限。
- Redux:适合复杂应用,状态管理规范,可扩展性强(结合中间件)。
- react-redux:简化 Redux 与 React 的集成,推荐使用
useSelector和useDispatch。 - redux-thunk:解决 Redux 异步处理问题,允许 Action Creator 返回函数。
根据应用复杂度选择合适的状态管理方案,小型应用优先考虑 Context API + useReducer,大型应用推荐 Redux 生态。
824

被折叠的 条评论
为什么被折叠?



