【React18+TypeScript】React 18 for Beginners

CodeWithMosh - 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中的扩展选算符(...),会将原来的所有属性复制过来,再重新赋值修改

对于数组

在组件之间共享状态



练习-更新状态

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

依赖库 版本号 作用
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;

事件处理逻辑

  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

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

实现步骤:

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

数据加载时显示Loading指示器

实现步骤:

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

Bootstrap加载组件使用

bash 复制代码
{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

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
相关推荐
Zhi.C.Yue6 小时前
React 状态更新中的双缓冲机制、优先级调度
前端·javascript·react.js
hellsing6 小时前
UniPush 2.0 实战指南:工单提醒与多厂商通道配置
前端·javascript
king王一帅6 小时前
告别 AI 输出的重复解析:正常 markdown 解析渲染也能提速 2-10 倍以上
前端·javascript·ai编程
dudke6 小时前
js的reduce详解
开发语言·javascript·ecmascript
代码or搬砖6 小时前
ES6新增的新特性以及用法
前端·javascript·es6
小番茄夫斯基6 小时前
Monorepo 架构:现代软件开发的代码管理革命
前端·javascript·架构
一只秋刀鱼6 小时前
从 0 到 1 构建 React + TypeScript 车辆租赁后台管理系统
前端·typescript
转转技术团队7 小时前
前端工程化实践:打包工具的选择与思考
前端·javascript·webpack
前端郭德纲7 小时前
React 19.2 已发布,现已上线 npm!
前端·react.js·npm