CodeWithMosh - React 18 for Beginners
- 用React和TypeScript构建前端应用
- 构建可重复使用的函数组件
- 用原版CSS、CSS模块和CSS-in-JS来样式你的组件
- 管理组件状态
- [带有React Hook Forms的构建表单](#带有React Hook Forms的构建表单)
- [使用 Zod 实现表单验证](#使用 Zod 实现表单验证)
- 把你的React应用连接到后台
用React和TypeScript构建前端应用
使用vite创建
构建可重复使用的函数组件
好用的扩展(VS code中使用)


自动生成

可重用的LIke组件构建


用原版CSS、CSS模块和CSS-in-JS来样式你的组件








图标库


管理组件状态
理解state hook
- 异步更新状态
- 状态实际上存储在组件外部
- 只能在组件的顶层使用hooks
选择状态结构
- 避免选择冗余状态变量,例如全名或者可以从现有变量计算出的任何内容
- 将多个相关的变量分组在同一个对象内
- 使用对象时,避免深层嵌套结构(很难更新),尽量使用扁平结构
Pure Function纯粹函数
在计算机科学中给定相同的输入,总是会得到相同的输出
React根据这个理念来设计函数,给定相同的输入比如Props,应该返回相同的jsx,因此如果组件的输入没有变化,应该跳过重新渲染的步骤
严格模式
默认开启严格模式,React会对每个组件渲染两次,可以找到不纯的组件
只在开发模式下会起作用,构建生产应用程序的时候不包括严格模式检查,组件只会渲染一次
状态变量值的改变
像props一样,将状态对象视为不可变或者只读的
在React中不应该更新现有的状态对象,而是应该给React一个全新的状态对象
修改时可以使用JS中的扩展选算符(...),会将原来的所有属性复制过来,再重新赋值修改
对于数组

在组件之间共享状态



练习-更新状态
bash
import { useState } from "react"
function App() {
const [game, setGame] = useState({
id: 1,
player: {
name: 'John'
}
})
const handleClick = () => {
setGame({
...game,
player: {
...game.player,
name: 'Mike'
}
})
}
return (
<div>
<div>姓名:{game.player.name}</div>
<button onClick={handleClick}>修改姓名</button>
</div>
)
}
export default App
bash
import { useState } from "react"
function App() {
const [pizza, setPizza] = useState({
name: 'Spicy Pepperoni',
toppings: ['Mushroom']
})
const handleClick = () => {
setPizza({
...pizza,
toppings: [...pizza.toppings, 'Cheese']
})
}
return (
<div>
<div>配料:{pizza.toppings}</div>
<button onClick={handleClick}>添加配料</button>
</div>
)
}
export default App
bash
import { useState } from "react"
import { IoTimeSharp } from "react-icons/io5"
function App() {
const [cart, setCart] = useState({
discount: .1,
items: [
{ id: 1, title: 'Product 1', quantity: 1 },
{ id: 2, title: 'Product 2', quantity: 1 }
]
})
const handleClick = () => {
setCart({
...cart,
items: cart.items.map(item => item.id === 1 ? { ...item, quantity: item.quantity + 1 } : item)
})
}
return (
<div>
<div>库存:{cart.items[0].quantity}</div>
<button onClick={handleClick}>添加商品id=1的库存</button>
</div>
)
}
export default App


带有React Hook Forms的构建表单

bash
import Form from "./components/Form"
function App() {
return (
<div>
<Form></Form>
</div>
)
}
export default App
使用BootStrap库构建表单,使用useRef获取表单中输入字段的值
useRef获取表单中输入字段的值
bash
import React, { FormEvent, useRef } from 'react'
const Form = () => {
const nameRef = useRef<HTMLInputElement>(null)
const ageReft = useRef<HTMLInputElement>(null)
const person = {name:'',age:0}
const handleSubmit = (event:FormEvent)=>{
event.preventDefault()
if(nameRef.current !== null)
person.name = nameRef.current.value
if(ageReft.current !== null)
person.age = parseInt(ageReft.current.value)
console.log(person)
}
return (
<form onSubmit={handleSubmit}>
{/* div.mb-3 : 边距底部3的缩写,bootstrap中的工具类之一,使用它在元素下方添加一些边距*/}
{/* div.mb-3>label.form-label+input.form-control */}
<div className="mb-3">
<label htmlFor="name" className="form-label">Name</label>
<input ref={nameRef} id="name" type="text" className="form-control" />
</div>
<div className="mb-3">
<label htmlFor="age" className="form-label">Age</label>
<input ref={ageReft} id="age" type="number" className="form-control" />
</div>
<button className="btn btn-primary" type='submit'>Submit</button>
</form>
)
}
export default Form
useState实现受控组件
使用useState管理表单状态
受控组件 = 状态(state) + value 属性 + onChange 事件
bash
import React, { FormEvent, useRef, useState } from 'react'
const Form = () => {
const [person,setPerson] = useState({name:'',age:''})
const handleSubmit = (event:FormEvent)=>{
event.preventDefault()
console.log(person)
}
return (
<form onSubmit={handleSubmit}>
{/* div.mb-3 : 边距底部3的缩写,bootstrap中的工具类之一,使用它在元素下方添加一些边距*/}
{/* div.mb-3>label.form-label+input.form-control */}
<div className="mb-3">
<label htmlFor="name" className="form-label">Name</label>
<input value={person.name} onChange={(e)=>{setPerson({...person,name:e.target.value})}} id="name" type="text" className="form-control" />
</div>
<div className="mb-3">
<label htmlFor="age" className="form-label">Age</label>
<input value={person.age} onChange={(e)=>{setPerson({...person,age:e.target.value})}} id="age" type="number" className="form-control" />
</div>
<button className="btn btn-primary" type='submit'>Submit</button>
</form>
)
}
export default Form
使用react-hook-form管理表单状态-useForm

bash
import { FieldValues, useForm } from "react-hook-form";
const Form = () => {
const { register, handleSubmit } = useForm();
const onSubmit = handleSubmit((data: FieldValues) => console.log(data));
return (
<form onSubmit={onSubmit}>
{/* div.mb-3 : 边距底部3的缩写,bootstrap中的工具类之一,使用它在元素下方添加一些边距*/}
{/* div.mb-3>label.form-label+input.form-control */}
<div className="mb-3">
<label htmlFor="name" className="form-label">
Name
</label>
<input
{...register("name")}
id="name"
type="text"
className="form-control"
/>
</div>
<div className="mb-3">
<label htmlFor="age" className="form-label">
Age
</label>
<input
{...register("age")}
id="age"
type="number"
className="form-control"
/>
</div>
<button className="btn btn-primary" type="submit">
Submit
</button>
</form>
);
};
export default Form;

使用表单校验规则required和minlength
bash
import { FieldValues, useForm } from "react-hook-form";
interface FormData {
name: string;
age: number;
}
const Form = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>();
const onSubmit = handleSubmit((data: FieldValues) => console.log(data));
return (
<form onSubmit={onSubmit}>
{/* div.mb-3 : 边距底部3的缩写,bootstrap中的工具类之一,使用它在元素下方添加一些边距*/}
{/* div.mb-3>label.form-label+input.form-control */}
<div className="mb-3">
<label htmlFor="name" className="form-label">
Name
</label>
<input
{...register("name", {
required: true,
minLength: 3,
})}
id="name"
type="text"
className="form-control"
/>
{errors.name?.type === "required" && (
<p className="text-danger">请输入内容</p>
)}
{errors.name?.type === "minLength" && (
<p className="text-danger">内容长度不少于3</p>
)}
</div>
<div className="mb-3">
<label htmlFor="age" className="form-label">
Age
</label>
<input
{...register("age")}
id="age"
type="number"
className="form-control"
/>
</div>
<button className="btn btn-primary" type="submit">
Submit
</button>
</form>
);
};
export default Form;


使用 Zod 实现表单验证
Zod核心功能实现
| 依赖库 | 版本号 | 作用 |
|---|---|---|
| zod | 3.20.6 | 核心Schema定义与验证库 |
| @hookform/resolvers | 2.9.11 | 连接React Hook Form与Zod的解析器 |
基础Schema创建:
bash
npm i zod
bash
import z from "zod";
const schema = z.object({
name: z.string().min(3, { message: "名称至少3个字符" }), // 字符串+最小长度验证
age: z.number().min(18, { message: "年龄必须至少18岁" }) // 数字+最小值验证
});
TypeScript类型自动生成:
通过z.infer从Schema提取类型,消除手动类型定义:
bash
type FormData = z.infer<typeof schema>;
// 等价于:{ name: string; age: number }
二、 与React Hook Form集成流程
安装解析器:
bash
npm install @hookform/resolvers@2.9.11
配置表单解析器:
bash
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema) // 关联Zod Schema
});
表单字段注册:
bash
{/* 文本输入框 */}
<input {...register("name")} />
{errors.name && <p>{errors.name.message}</p>}
{/* 数字输入框(需指定类型转换) */}
<input type="number" {...register("age", { valueAsNumber: true })} />
{errors.age && <p>{errors.age.message}</p>}
完整代码
bash
import { FieldValues, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const schema = z.object({
name: z.string().min(3, { message: "名称至少3个字符" }),
age: z.number({}).min(18, { message: "年龄必须至少18岁" }),
});
type formData = z.infer<typeof schema>; // 等价于:{ name: string; age: number }
const Form = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<formData>({ resolver: zodResolver(schema) }); // 关联Zod Schema
const onSubmit = handleSubmit((data: FieldValues) => console.log(data));
return (
<form onSubmit={onSubmit}>
{/* div.mb-3 : 边距底部3的缩写,bootstrap中的工具类之一,使用它在元素下方添加一些边距*/}
{/* div.mb-3>label.form-label+input.form-control */}
<div className="mb-3">
<label htmlFor="name" className="form-label">
Name
</label>
<input
{...register("name")}
id="name"
type="text"
className="form-control"
/>
{errors.name && <p className="text-danger">{errors.name.message}</p>}
</div>
<div className="mb-3">
<label htmlFor="age" className="form-label">
Age
</label>
<input
{...register("age", { valueAsNumber: true })}
id="age"
type="number"
className="form-control"
/>
{errors.age && <p className="text-danger">{errors.age.message}</p>}
</div>
<button className="btn btn-primary" type="submit">
Submit
</button>
</form>
);
};
export default Form;
表单验证优化:基于表单状态的提交按钮禁用逻辑实现

练习
组件存放路径:src/expanse-tracker/components/ExpenseList.tsx
基础结构搭建
bash
interface Expense {
id: number;
description: string;
amount: number;
category: string;
}
interface Props {
expenses: Expense[];
onDelete: (id: number) => void;
}
const ExpenseList = ({ expenses, onDelete }: Props) => {
return (
<table className="table table-bordered">
{/* 表头 */}
<thead>
<tr>
<th>商品名称</th>
<th>数量</th>
<th>类别</th>
<th></th>
</tr>
</thead>
{/* 表格内容 */}
<tbody>
{expenses.map((expense) => (
<tr key={expense.id}>
<td>{expense.description}</td>
<td>{expense.amount}</td>
<td>{expense.category}</td>
<td>
<button
className="btn btn-outline-danger"
onClick={() => onDelete(expense.id)}
>
Delete
</button>
</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<td>总计</td>
<td>
¥
{expenses
.reduce((acc, expense) => acc + expense.amount, 0)
.toFixed(2)}
</td>
<td></td>
<td></td>
</tr>
</tfoot>
</table>
);
};
export default ExpenseList;
bash
import { useState } from "react";
import ExpenseList from "./expanse-tracker/components/ExpenseList";
function App() {
const [expenses, setExpenses] = useState([
{ id: 1, description: "aaa", amount: 10, category: "Utilities" },
{ id: 2, description: "aaa", amount: 10, category: "Utilities" },
{ id: 3, description: "aaa", amount: 10, category: "Utilities" },
{ id: 4, description: "aaa", amount: 10, category: "Utilities" },
]);
if (expenses.length === 0) return null;
return (
<div>
<ExpenseList
expenses={expenses}
onDelete={(id) =>
setExpenses(expenses.filter((expense) => expense.id !== id))
}
></ExpenseList>
</div>
);
}
export default App;

事件处理逻辑
- 为元素添加onChange事件监听
- 通过event.target.value获取选中分类值
- 调用onSelectCategory回调函数传递选中值

ExpenseForm组件
创建包含三个字段的表单,用于记录支出信息
返回一个元素,包含三个输入字段和一个提交按钮

ExpenseFilter组件改造

使用React Hook Form与Zod实现表单验证
分类数据定义
创建categories.ts文件:
如果categories放在App.tsx,在ExpenseForm组件中使用categories就需要从App组件中导入,但App组件也要导入ExpenseForm渲染到页面,此时涉及组件加载顺序,会发生错误

重新导入

ExpenseForm组件依赖导入




实现效果:

表单数据提交处理
实现步骤:
- 创建ExpenseFormProps属性接口
- 使用useForm钩子管理表单状态,解构reset表单重置函数
- 实现表单提交处理逻辑


把你的React应用连接到后台
bash
npm i axios
发送HTTP请求
App.tsx
bash
import { useEffect, useState } from "react";
import axios from "axios";
interface User {
id: number;
name: string;
}
function App() {
const [user, SetUser] = useState<User[]>([]);
useEffect(() => {
axios
.get<User[]>("https://jsonplaceholder.typicode.com/users")
.then((res) => SetUser(res.data));
}, []);
return (
<ul>
{user.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export default App;

直接将async函数传给useEffect会报错,需通过内部函数间接实现。


使用AbortController取消Fetch请求
实现步骤:
- 创建控制器
- 关联请求信号
- 返回清理函数

数据加载时显示Loading指示器
实现步骤:
- 状态变量声明
- 控制Loading状态切换
数据请求前将isLoading设置为true
数据请求后将isLoading设置为false

Bootstrap加载组件使用
bash
{isLoading && <div className="spinner-border"></div>}
finally方法方案:理论上可使用Promise的finally方法统一处理状态重置(无论成功/失败),但在严格模式(Strict Mode) 下存在兼容性问题。
增删改查数据
删除数据:实现用户删除功能
实现步骤:
- 添加删除按钮
- 绑定点击删除事件
- 编写删除处理逻辑
先保留原来的的用户列表
使用filter函数过滤目标删除的用户item(filter返回一个新的数组),处理UI更新逻辑
服务器交互处理,服务器同步删除

添加用户
实现步骤
- 创建添加按钮并绑定添加事件addUser
- 编写添加事件的逻辑
保存元数据,方便出错回滚
新建用户item,并将其渲染到UI界面
服务器交互处理,将新用户item添加到服务器

更新数据
实现步骤
- 添加更新按钮并绑定
- 实现更新函数逻辑

提取HTTP请求逻辑到独立服务

api-Services.ts
bash
import axios ,{CanceledError} from "axios";
export default axios.create({
baseURL:'https://jsonplaceholder.typicode.com'
})
export {CanceledError}
UserService.ts
bash
import apiClient from "./api-client";
export interface User {
id: number;
name: string;
}
class UserService{
getAllUsers(){
// 创建控制器
const controller = new AbortController();
const request = apiClient.get<User[]>("/users",{ signal: controller.signal });
return {
request,
cancel: ()=>controller.abort()
}
}
delUser(id:number){
return apiClient.delete("/users/" + id)
}
createUser(user:User){
return apiClient.post("/users", user)
}
updateUser(user:User){
return apiClient.patch("/users/" + user.id, user)
}
}
export default new UserService();


改造成通用模板
http-service.ts
bash
import apiClient from "./api-client";
interface Entity{
id:number
}
class HttpService{
endpoint:string
constructor(endpoint:string){
this.endpoint = endpoint
}
getAll<T>(){
// 创建控制器
const controller = new AbortController();
const request = apiClient.get<T[]>(this.endpoint,{ signal: controller.signal });
return {
request,
cancel: ()=>controller.abort()
}
}
del(id:number){
return apiClient.delete(this.endpoint+"/" + id)
}
create<T>(entity:T){
return apiClient.post(this.endpoint, entity)
}
update<T extends Entity>(entity:T){
return apiClient.patch(this.endpoint+"/" + entity.id, entity)
}
}
const create = (endpoint:string)=>new HttpService(endpoint)
export default create
user-service.ts
bash
import create from "./http-service";
export interface User {
id: number;
name: string;
}
export default create('/users');
App.tsx
bash
import { useEffect, useState } from "react";
import { AxiosError } from "axios";
import { CanceledError } from "./services/api-client";
import UserService, { User } from "./services/user-service";
import userService from "./services/user-service";
function App() {
const [users, setUsers] = useState<User[]>([]);
const [error, setError] = useState("");
// Loading
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const { request, cancel } = UserService.getAll<User>();
// 调用服务器之前设置为true
setIsLoading(true);
const fetchUser = async () => {
try {
const res = await request;
setUsers(res.data);
setIsLoading(false);
} catch (err) {
if (err instanceof CanceledError) return;
// setError((err as AxiosError).message);
setError((err as AxiosError).message);
setIsLoading(false);
}
};
fetchUser();
return cancel;
}, []);
// 删除函数
const delUser = (user: User) => {
const originalUsers = [...users];
setUsers(users.filter((u) => u.id !== user.id));
userService.del(user.id).catch((err) => {
setError(err.message);
setUsers(originalUsers);
});
};
// 添加用户
const addUser = () => {
const originalUsers = [...users];
const newUser = { id: 0, name: "qing" };
setUsers([newUser, ...users]);
userService
.create(newUser)
.then(({ data: savedUser }) => setUsers([savedUser, ...users]))
.catch((err) => {
// 显示错误信息
setError(err.message);
// 状态回滚
setUsers(originalUsers);
});
};
// 更新用户
const updateUser = (user: User) => {
const originalUsers = [...users];
const updatedUser = { ...user, name: user.name + "!" };
setUsers(users.map((u) => (u.id === user.id ? updatedUser : u)));
UserService.update(updatedUser).catch((err) => {
setError(err.message);
setUsers(originalUsers);
});
};
return (
<>
{error && <p className="text-danger">{error}</p>}
{isLoading && <div className="spinner-border"></div>}
{/* 添加用户 */}
<button className="btn btn-primary mb-3" onClick={addUser}>
Add
</button>
<ul className="list-group">
{users.map((user) => (
<li
key={user.id}
className="list-group-item d-flex justify-content-between"
>
{user.name}
<div>
<button
className="btn btn-outline-secondary mx-1"
onClick={() => updateUser(user)}
>
更新
</button>
<button
className="btn btn-outline-danger"
onClick={() => delUser(user)}
>
删除
</button>
</div>
</li>
))}
</ul>
</>
);
}
export default App;
自定义Hook复用
useUsers数据获取逻辑封装

useUsers.ts
bash
import { useEffect, useState } from "react";
import { AxiosError } from "axios";
import { CanceledError } from "../services/api-client";
import UserService, { User } from "../services/user-service";
const useUser = ()=>{
const [users, setUsers] = useState<User[]>([]);
const [error, setError] = useState("");
// Loading
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const { request, cancel } = UserService.getAll<User>();
// 调用服务器之前设置为true
setIsLoading(true);
const fetchUser = async () => {
try {
const res = await request;
setUsers(res.data);
setIsLoading(false);
} catch (err) {
if (err instanceof CanceledError) return;
// setError((err as AxiosError).message);
setError((err as AxiosError).message);
setIsLoading(false);
}
};
fetchUser();
return cancel;
}, []);
return {users,error,isLoading,setUsers,setError}
}
export default useUser

