实用指南:【React18+TypeScript】React 18 for Beginners

用React和TypeScript构建前端应用

使用vite创建

构建可重复使用的函数组件

好用的扩展(VS code中使用)
在这里插入图片描述
在这里插入图片描述
自动生成
在这里插入图片描述

可重用的LIke组件构建

在这里插入图片描述在这里插入图片描述

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
图标库
在这里插入图片描述
在这里插入图片描述

管理组件状态

理解state hook

  1. 异步更新状态
  2. 状态实际上存储在组件外部
  3. 只能在组件的顶层使用hooks

选择状态结构

  1. 避免选择冗余状态变量,例如全名或者可以从现有变量计算出的任何内容
  2. 将多个相关的变量分组在同一个对象内
  3. 使用对象时,避免深层嵌套结构(很难更新),尽量使用扁平结构

Pure Function纯粹函数

在计算机科学中给定相同的输入,总是会得到相同的输出
React根据这个理念来设计函数,给定相同的输入比如Props,应该返回相同的jsx,因此如果组件的输入没有变化,应该跳过重新渲染的步骤

严格模式

默认开启严格模式,React会对每个组件渲染两次,可以找到不纯的组件
只在开发模式下会起作用,构建生产应用程序的时候不包括严格模式检查,组件只会渲染一次

状态变量值的改变

像props一样,将状态对象视为不可变或者只读的
在React中不应该更新现有的状态对象,而是应该给React一个全新的状态对象
修改时可以使用JS中的扩展选算符(…),会将原来的所有属性复制过来,再重新赋值修改
对于数组
在这里插入图片描述

在组件之间共享状态

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

练习-更新状态

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
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
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的构建表单

在这里插入图片描述

import Form from "./components/Form"
function App() {
return (
<div>
  <Form></Form>
    </div>
      )
      }
      export default App

使用BootStrap库构建表单,使用useRef获取表单中输入字段的值

useRef获取表单中输入字段的值

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 事件

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

在这里插入图片描述

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

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核心功能实现

依赖库版本号作用
zod3.20.6核心Schema定义与验证库
@hookform/resolvers2.9.11连接React Hook Form与Zod的解析器

基础Schema创建:

npm i zod
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提取类型,消除手动类型定义:

type FormData = z.infer<typeof schema>;
  // 等价于:{ name: string; age: number }

二、 与React Hook Form集成流程
安装解析器:

npm install @hookform/resolvers@2.9.11

配置表单解析器:

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
  resolver: zodResolver(schema) // 关联Zod Schema
  });

表单字段注册:

{/* 文本输入框 */}
<input {...register("name")} />
  {errors.name && <p>{errors.name.message}</p>}
    {/* 数字输入框(需指定类型转换) */}
    <input type="number" {...register("age", { valueAsNumber: true })} />
      {errors.age && <p>{errors.age.message}</p>}

完整代码

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

基础结构搭建

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;
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;

在这里插入图片描述
事件处理逻辑

  1. 为元素添加onChange事件监听
  2. 通过event.target.value获取选中分类值
  3. 调用onSelectCategory回调函数传递选中值

在这里插入图片描述

ExpenseForm组件

创建包含三个字段的表单,用于记录支出信息
返回一个元素,包含三个输入字段和一个提交按钮
在这里插入图片描述
在这里插入图片描述ExpenseFilter组件改造
在这里插入图片描述
使用React Hook Form与Zod实现表单验证
分类数据定义
创建categories.ts文件:

如果categories放在App.tsx,在ExpenseForm组件中使用categories就需要从App组件中导入,但App组件也要导入ExpenseForm渲染到页面,此时涉及组件加载顺序,会发生错误

在这里插入图片描述
重新导入
在这里插入图片描述
ExpenseForm组件依赖导入
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
实现效果:
在这里插入图片描述
表单数据提交处理

实现步骤:

  1. 创建ExpenseFormProps属性接口
  2. 使用useForm钩子管理表单状态,解构reset表单重置函数
  3. 实现表单提交处理逻辑

在这里插入图片描述
在这里插入图片描述

把你的React应用连接到后台

jsonplaceholder

npm i axios

发送HTTP请求

App.tsx

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请求

实现步骤:

  1. 创建控制器
  2. 关联请求信号
  3. 返回清理函数

在这里插入图片描述
数据加载时显示Loading指示器

实现步骤:

  1. 状态变量声明
  2. 控制Loading状态切换
    数据请求前将isLoading设置为true
    数据请求后将isLoading设置为false

在这里插入图片描述
Bootstrap加载组件使用

{isLoading && <div className="spinner-border"></div>}

finally方法方案:理论上可使用Promise的finally方法统一处理状态重置(无论成功/失败),但在严格模式(Strict Mode) 下存在兼容性问题。

增删改查数据

删除数据:实现用户删除功能

实现步骤:

  1. 添加删除按钮
  2. 绑定点击删除事件
  3. 编写删除处理逻辑
    先保留原来的的用户列表
    使用filter函数过滤目标删除的用户item(filter返回一个新的数组),处理UI更新逻辑
    服务器交互处理,服务器同步删除

在这里插入图片描述
添加用户

实现步骤

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

在这里插入图片描述
更新数据

实现步骤

  1. 添加更新按钮并绑定
  2. 实现更新函数逻辑

在这里插入图片描述

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

在这里插入图片描述

api-Services.ts

import axios ,{CanceledError} from "axios";
export default axios.create({
baseURL:'https://jsonplaceholder.typicode.com'
})
export {CanceledError}

UserService.ts

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

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

import create from "./http-service";
export interface User {
id: number;
name: string;
}
export default create('/users');

App.tsx

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

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

在这里插入图片描述

posted on 2026-01-13 11:08  ljbguanli  阅读(1)  评论(0)    收藏  举报