react practice:mosh gamehub
观前提示:
- 默认读者有基础的 js, ts, html 基础, 其中 html 和 js 在MDN Web Docs上看入门教程即可,ts 可以看mosh 的视频或者 google 一下 typescript tutorial 即可;
- 本人水平有限,错漏和不足之处敬请谅解
- 本笔记对应的视频: CodeWithMosh - React 18 for Beginners对应初级部分;code with mosh - React: Intermediate Topics对应进阶部分;相关代码可以在 github 找到。
gamehub: react + ts + ...
初级部分
前端部分
一切的开始:初识 React
vite: 一个比 create-react-app 更轻量级的创建应用工具
npm create vite@latest
然后选择 react, ts
之后 npm install, npm run dev
ps: 笔者在后续发现,如果 npm 的镜像源默认改成了 cnpm, (有时候)会找不到 dev 而报错,疑似和 vite 有冲突
react,启动!

react 使用 JSX, JSX 被编译成 pure js(with react)
编写 react 的时候需要遵守 PascalCasting, 即组件名为 AppName 而不是 app_name 或者 appName
export 和 export default 的区别:export default 是唯一的,使得导入的时候不受命名空间冲突影响,可以起别名
vite 自动有热启动
.tsx 为后缀的文件可以使用 JSX 语法(tsx: ts+ jsx),.ts 为后缀的文件不行
JSX 暴打原始人(暴论):回想起实战 1 里面,需要定义 pug 文件,往 pug 里面传参数(提前想好传哪些),在 pug 里面写乱七八糟的语法,相对不方便复用......
how react works?
react 维护一个 virtual dom(document object model)树,代码会更新这棵树,然后 react 将这棵树和浏览器的真实 DOM 树比较,并更新浏览器上的 DOM
what is react?
react 是“库 lib”式的单个工具,而不是“框架 framework”一组完善的建立网站的工具
react 专注于建立组件式可交互 UI
但 react 不 解决 router, http, form validation, internationalization... 等等问题
这些在下面的内容之中(笑
bootstrap: 现代感 UI, css 库
在文档内搜索后复制示例代码即可
返回多个块
差的方法:使用<div></div>包裹,语法上没有问题,但是在 react 的 DOM 上多加了一个组件 div
好的方法:使用<fragment></fragment>包裹
简写,使用<></>包裹
component 快捷键: React ES7+插件,rafce
JSX: 里面只能有 html 元素/React 组件/包裹的表达式
"for loop in JSX"? 不能这么干,但可以通过 map 方法实现
function ListGroup() {
const items = ["SJTU", "PKU", "THU", "FDU"];
return (
<>
<h1>List Title</h1>
<ul className="list-group">
{items.map((item) => (
<li className="list-group-item">{item}</li>
))}
</ul>
</>
);
}
export default ListGroup;
“if else in JSX”?也不行, 但可以用表达式代替
function ListGroup() {
let items = ["SJTU", "PKU", "THU", "FDU"];
items = [];
// const Message = items.length === 0 ? <p>No items found</p> : null;
// 另一种方法
const Message = items.length === 0 && <p>No items found</p>; // true && something === something
return (
<>
<h1>List Title</h1>
{Message}
<ul className="list-group">
{items.map((item) => (
<li className="list-group-item" key={item}>
{item}
</li>
))}
</ul>
</>
);
}
export default ListGroup;
在使用 map 方法时,需要给 item 一个唯一的 key,以便于 React 追踪
map 可以有第二个可选参数 index, 代表迭代时的序号
处理单击事件:每一个 React 元素都有内置的 onClick 方法,接受一个可选的 event
处理函数命名规范 handleClick
让 react 意识到组件有内部状态,可能随时间变化,变化时需要更新(重新渲染):useState
又叫 stateHook, 通过 Hook,我们可以不用直接接触 DOM,而专注于组件和它的状态
把上面的知识组合起来的应用:动态添加类,鼠标激活效果实现
ps: 动态添加类是解释性语言,如 js,python 的专属; 这样添加 active 效果实际上也算是自定义 active 实现的方法
import { useState } from "react";
function ListGroup() {
let items = ["SJTU", "PKU", "THU", "FDU"];
const [selectIndex, setSelectIndex] = useState(-1);
const Message = items.length === 0 ? <p>No items found</p> : null;
return (
<>
<h1>List Title</h1>
{Message}
<ul className="list-group">
{items.map((item, index) => (
<li
className={
selectIndex === index
? "list-group-item active"
: "list-group-item"
}
key={item}
onClick={() => setSelectIndex(index)}
>
{item}
</li>
))}
</ul>
</>
);
}
export default ListGroup;
让组件可重用:props
刚好可以写成 ts 的 interface
props is immutable: 传入的参数可以认为是某种”复制",不应该直接在子组件之中修改 Props,(直接改了也不会反映到父组件上),如果要根据子组件改父组件状态,把父组件的 setState 作为回调函数传下来
如果父组件想从子组件得到信息?例如,选择?
React 并没有提供组件书“自底向上”传递信息的方法,这样的“将顶层逻辑下放到可重用的子组件”也破坏了子组件的重用性;实现方法应该放在父组件,在传递的 props 里面多加一个函数参数,让子组件得到信息之后调用这个父组件定义的函数,还是自顶向下传递。
示例:
App.tsx
import { useState } from "react";
import ListGroup from "./components/ListGroup";
import "./App.css";
function App() {
let schools = ["PKU", "FDU", "THU", "ZJU", "SJTU"];
let cities = ["Beijing", "Shanghai", "Hangzhou", "Nanjing", "Wuhan"];
const onSelectItem = (item: string) => {
console.log(item);
};
return (
<div>
<ListGroup title="Schools" items={schools} onSelectItem={onSelectItem} />
<ListGroup title="Cities" items={cities} onSelectItem={onSelectItem} />
</div>
);
}
export default App;
ListGroup.tsx
import { useState } from "react";
interface ListGroupProps {
title: string;
items: string[];
onSelectItem: (item: string) => void;
}
function ListGroup({ items, title, onSelectItem }: ListGroupProps) {
const [selectIndex, setSelectIndex] = useState(-1);
const Message = items.length === 0 ? <p>No items found</p> : null;
return (
<>
<h1>{title}</h1>
{Message}
<ul className="list-group">
{items.map((item, index) => (
<li
className={
selectIndex === index
? "list-group-item active"
: "list-group-item"
}
key={item}
onClick={() => {
setSelectIndex(index);
onSelectItem(item);
}}
>
{item}
</li>
))}
</ul>
</>
);
}
export default ListGroup;
传递文本和 html?
直接 props:略丑
使用 children:ReactNode 的 interface
例:
App.tsx
import Alert from "./components/Alert";
function App() {
return (
<Alert>
<p>Note:</p>This is a Alert
</Alert>
);
}
export default App;
Alert.tsx
import { ReactNode } from "react";
interface AlertProps {
children: ReactNode;
}
const Alert = ({ children }: AlertProps) => {
return <div className="alert alert-danger">{children}</div>;
};
export default Alert;
react-developer-tool
组件重用示例 2
Button.tsx
interface ButtonProps {
children: string;
color?:
| "primary"
| "secondary"
| "success"
| "warning"
| "danger"
| "info"
| "light"
| "dark";
onClick?: () => void;
}
const Button = ({
children,
color = "primary",
onClick = () => {
console.log("Clicked");
},
}: ButtonProps) => {
return (
<div className={"btn btn-" + color} onClick={onClick}>
{children}
</div>
);
};
export default Button;
App.tsx
import Button from "./components/Button";
function App() {
return (
<>
<Button children="a" />
<Button children="b" />
<Button children="c" />
</>
);
}
export default App;
Styling: 更进一步的 CSS
plain css: not recommended
css module:用于解决多个 css 同名类的冲突覆盖问题。 .css 文件变成 .module.css 文件,里面定义的 css 类变成整个 module 对象的属性,可以通过 import {style} from "./sth.module.css" 之后 style["some-css-class"]使用
如果需要一个模块的多个 css 类,使用 [style['classA'], style['classB']].join(' ')语法
CSS in JS: styled-component
Seperation of Concerns 原则:把复杂度隐藏在接口之下,一个模块的复杂度不应该暴露在外面
inline CSS: not recommended also
有用的热门 CSS 库(个人只会调库,笑)
- bootstrap
- Material UI
- tailwind
- chakra UI
- ......
添加 icon: react-icon 库, font awesome icon
Managing Component State
NOTE:
- React 更新 State 是异步的 async
- state 实际存储在组件之外
- only use hook at the top level of function: Hook 不能被放在 条件,循环等等之中
BEST PRACTICE:
- 避免冗余 state
- 使用对象将相关的 state 套起来是好的,但不要深度嵌套
ps: 后补:第二点可能比较抽象,详细说就是,如果我的组件取决于一个 state game, 然后 game 可能有多个需要维护的 state 例如 price,id, ... 与其把 price, id,...分别使用 setState,不如只设一个 State,就是 game, 也就是说 useState 可以接受一个复制的对象作为 state 而不局限于基本类型。
而“不要深度嵌套”是因为,这样包含了多个属性的 state 每次更新 都要 用
...复制一次(React 检测作为对象的 State 变化并不会深入下去比较对象内部的每一个值,所以传入新的 State 的时候需要传入一个新的对象),例如useState([...game, newPrice: 20]), 而 game 如果是对象套对象,那就要再多一层.........深起来既浪费,又不好维护
KEEPING COMPONENT PURE:
ReactDOM 的 strictMode 会检测不纯的组件,并执行两次
在 state 是一个对象的时候,React 并不会实时检测这个对象的变化,视为 immutable
所以如果要将一个 drink 对象的 price 属性改变
const Incorrect = () => {
drink.price = 6;
setDrink(drink); // false: React不会比对drink对象的属性有改变,当传递的还是原来的drink对象的时候,ReactDOM就不会重新渲染
};
const Correct = () => {
let newDrink = {
...drink, // 复制原先drink对象的属性
price: 6,
};
setDrink(newDrink); //这样drink对象本身改变了,ReactDOM重新渲染
};
ATTENTION:
如果对象里面嵌套对象,由于...复制对象属性的时候对对象元素默认是浅复制,需要在嵌套的对象里面也使用...来避免修改原来对象的值
对于 state 是数组的情况同理
const [arr, setArr] = useState([0, 1]);
const handleClick = () => {
// 不使用arr.push()方法
let newEle = 2;
// Add
setArr([...arr, newEle]);
// Remove
serArr([arr.filter((ele) => ele !== 0)]);
// Update
setArr([arr.map((ele) => (ele === 0 ? 1 : ele))]);
};
sharing state between component: 把需要在组件中共享的 state 在组件层次架构里面向上提升一级
练习,可折叠文字
生成随机字符串 lorem + 一个数, 比如 lorem200,生成长为 200 个字符的随机字符串
Forms 表单
React Hook Forms && Zod
基础表单
表单的基础和 bootstrap 里面的表单, div.md-3>lable.form-label+input.form-control + [TAB]的简写格式
(.是类className,+是并列, >是包含(子组件),*是多个)
在 React 中,你可以通过将
<button>元素放入<form>元素内部,并设置type="submit",来将按钮的点击事件与表单的提交事件绑定。当点击这个按钮时,表单的onSubmit事件将被触发。你需要在
<form>元素上 添加一个onSubmit事件处理器来处理表单提交
React useRef
useRef 产生一个 React 元素(DOM Node)的引用
直接看具体代码
import "bootstrap/dist/css/bootstrap.css";
import "../index.css";
import { useRef } from "react";
const Form = () => {
const nameRef = useRef<HTMLInputElement>(null);
return (
<>
<form
onSubmit={(event) => {
event.preventDefault();
console.log(nameRef.current?.value);
}}
>
<div className="mb-3">
<label htmlFor="name" className="form-label">
Name
</label>
<input ref={nameRef} id="name" type="text" className="form-control" />
<div className="mb-3">
<label htmlFor="age" className="form-lable">
Age
</label>
<input id="age" type="number" className="form-control" />
</div>
</div>
<button className="btn btn-primary" type="submit">
Submit
</button>
</form>
</>
);
};
export default Form;
但上述代码有一个问题
在 React 中,
useRef创建的引用(reference)的变化不会触发组件的重新渲染。这是因为useRef返回的对象在组件的所有渲染中保持不变,所以 React 不会知道引用的值已经改变。
所以要是需要重新渲染,还得是 useState,引出了下面的技术
controlled components
主要解决使用 useState 和 onChange 方法来控制 state 时候,不同组件同时修改一个 state 的异步问题
解决方法,在组件加一个 value={obj.value},每次调用前先同步,把组件状态的管理转交给 React
const [state, setState]=useState(...);
...
<Component value={state}
onChange={handleStateChange} />
更简单:使用 react-hook-form 库
import { FieldValues, useForm } from "react-hook-form";
const Form = () => {
const { register, handleSubmit } = useForm();
const onSubmit = (data: FieldValues) => console.log(data);
return (
<>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-3">
<label htmlFor="name" className="form-label">
Name
</label>
<input
{...register("name")}
id="name"
type="text"
className="form-control"
/>
<div className="mb-3">
<label htmlFor="age" className="form-lable">
Age
</label>
<input
{...register("age")}
id="age"
type="number"
className="form-control"
/>
</div>
</div>
<button className="btn btn-primary" type="submit">
Submit
</button>
</form>
</>
);
};
export default Form;
Validation
react-hook-form 的 register 可以有第二个参数,一个对象
import "bootstrap/dist/css/bootstrap.css";
import "../index.css";
import { useRef } from "react";
import { FieldValues, useForm } from "react-hook-form";
const Form = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
console.log(errors);
const onSubmit = (data: FieldValues) => console.log(data);
return (
<>
<form onSubmit={handleSubmit(onSubmit)}>
<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>You must give a name.</p>}
{errors.name?.type === "minLength" && (
<p>The length of name must be longer than 3 characters.</p>
)}
<div className="mb-3">
<label htmlFor="age" className="form-lable">
Age
</label>
<input
{...register("age")}
id="age"
type="number"
className="form-control"
/>
</div>
</div>
<button className="btn btn-primary" type="submit">
Submit
</button>
</form>
</>
);
};
export default Form;
基于模式的验证:Zod + @hookhome/resolvers 看代码
需要注意的是,这并不是实时的,需要 submit 才会有 error,如果同时启用了下面的 invalid 不 submit 和这个, 这个相当于没设置
import "bootstrap/dist/css/bootstrap.css";
import "../index.css";
import { FieldValues, useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
const schema = z.object({
name: z.string().min(3, { message: "Name must be at least 3 characters" }),
age: z.number({ invalid_type_error: "Age field is required" }).min(18),
});
type FormData = z.infer<typeof schema>;
const Form = () => {
const {
register,
handleSubmit,
formState: { errors, isValid },
} = useForm<FormData>({
resolver: zodResolver(schema),
});
console.log(errors);
const onSubmit = (data: FieldValues) => console.log(data);
return (
<>
<form onSubmit={handleSubmit(onSubmit)}>
<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>{errors.name.message}</p>}
<div className="mb-3">
<label htmlFor="age" className="form-label">
Age
</label>
<input
{...(register("age"), { valueAsNumber: true })}
id="age"
type="number"
className="form-control"
/>
</div>
{errors.age && <p>{errors.age.message}</p>}
</div>
<button className="btn btn-primary" type="submit">
Submit
</button>
</form>
</>
);
};
export default Form;
用淘宝源,SJTUG 没有; 装@hookhome/resolvers 的时候记得打""( npm --registry https://registry.npm.taobao.org install "@hookform/resolvers"),避免和手动指定源的 “/” 混一起
禁用 submit,
const {
register,
handleSubmit,
formState: { errors, isValid },
} = useForm();
<button disabled={!isValid} className="btn btn-primary" type="submit">
给 useState 加类型
useState\<Type\>
导出类型 export type
练习:记账本
非常好的练习,和 TodoList 一个实现道理,最好自己试着做一做
个人实现(后面回来看感觉很多地方没写好):
//FormDataType.tsx
import "bootstrap/dist/css/bootstrap.css";
import { z } from "zod";
type Category = "All Categories" | "Groceries" | "Utilities" | "Entertainment";
const schema = z.object({
description: z.string().min(3),
amount: z
.number({ invalid_type_error: "Amount field is required" })
.positive(),
category: z.enum([
"All Categories",
"Groceries",
"Utilities",
"Entertainment",
]),
});
type ExpenseFormData = z.infer<typeof schema>;
export { schema };
export type { ExpenseFormData, Category };
// index.tsx
import ExpenseForm from "./ExpenseForm";
import { ExpenseFormData } from "./FormDataType";
import { useState } from "react";
import FilterTable from "./FilterTable";
const ExpenseTracker = () => {
const [dataArr, setDataArr] = useState<ExpenseFormData[]>([]);
const pushCallback = (newData: ExpenseFormData) => {
setDataArr([...dataArr, newData]);
};
const deleteCallback = (index: number) => {
setDataArr(dataArr.filter((_, i) => i !== index));
};
return (
<>
<ExpenseForm dataArr={dataArr} pushCallback={pushCallback} />
<FilterTable dataArr={dataArr} callback={deleteCallback} />
</>
);
};
export default ExpenseTracker;
// FilterTable.tsx
import "bootstrap/dist/css/bootstrap.css";
import { ExpenseFormData, Category } from "./FormDataType";
import { useState } from "react";
interface FilterTableProps {
dataArr: ExpenseFormData[];
callback: (index: number) => void;
}
const FilterTable = ({ dataArr, callback }: FilterTableProps) => {
const [filterSelection, setFilter] = useState<Category>("All Categories");
const handleFilterChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setFilter(event.target.value as Category);
};
return (
<>
<select
value={filterSelection}
onChange={handleFilterChange}
className="form-select"
aria-label="Category"
>
<option selected>All Categories</option>
<option value="Groceries">Groceries</option>
<option value="Utilities">Utilities</option>
<option value="Entertainment">Entertainment</option>
</select>
<table className="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Description</th>
<th scope="col">Amount</th>
<th scope="col">Categories</th>
</tr>
</thead>
<tbody>
{dataArr.map((data: ExpenseFormData, index) => {
return (
(filterSelection === "All Categories" ||
data.category === filterSelection) && (
<tr>
<th scope={"row"}>{index + 1}</th>
<td>{data.description}</td>
<td>{data.amount}</td>
<td>{data.category}</td>
<td>
<button
onClick={() => callback(index)}
className="btn btn-warning"
>
Delete
</button>
</td>
</tr>
)
);
})}
<tr>
<th scope={"row"}>Total</th>
<td>
{dataArr.reduce((total, cur) => {
if (
filterSelection === "All Categories" ||
cur.category === filterSelection
) {
return total + cur.amount;
}
return total;
}, 0)}
</td>
</tr>
</tbody>
</table>
</>
);
};
export default FilterTable;
// ExpenseForm.tsx
import "bootstrap/dist/css/bootstrap.css";
import { FieldValues, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { schema } from "./FormDataType";
import { ExpenseFormData } from "./FormDataType";
interface ExpenseFormProps {
dataArr: ExpenseFormData[];
pushCallback: (newData: ExpenseFormData) => void;
}
const ExpenseForm = ({ dataArr, pushCallback }: ExpenseFormProps) => {
const {
register,
handleSubmit,
formState: { errors, isValid },
} = useForm<ExpenseFormData>({ resolver: zodResolver(schema) });
const onSubmit = (data: FieldValues) => {
if (isValid) {
pushCallback(data as ExpenseFormData);
console.log(dataArr);
}
};
return (
<>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-3">
<label htmlFor="Description" className="form-label">
Description
</label>
<input
{...register("description")}
id="Description"
type="text"
className="form-control"
/>
</div>
{errors.description && <p>{errors.description.message}</p>}
<div className="mb-3">
<label htmlFor="Amount" className="form-label">
Amount
</label>
<input
{...register("amount", { valueAsNumber: true })}
id="Amount"
type="number"
className="form-control"
/>
</div>
{errors.amount && <p>{errors.amount.message}</p>}
<select
{...register("category")}
className="form-select"
aria-label="Category"
>
<option selected>All Categories</option>
<option value="Groceries">Groceries</option>
<option value="Utilities">Utilities</option>
<option value="Entertainment">Entertainment</option>
</select>
{errors.category && <p>{errors.category.message}</p>}
<button type="submit" className="btn btn-primary">
Submit
</button>
</form>
</>
);
};
export default ExpenseForm;
看视频,上面的一些缺点:
- type 还是有一些耦合,category 应该独立出来成为单独的模块
- 可以继续调 css, 美观度不足(e.g 使用 mb-3 mb-5 class 加边框,改 validation text 颜色等等)
后端部分
- Express
- Django
- Ruby on Rail
- Spring
- ASP.NET
- ......
链接 React 和后端
useEffect 和 Effect Hook
为什么需要 useEffect:
React 引入
useEffect是为了在函数组件中处理副作用。在 React 中,副作用包括数据获取、订阅或手动修改 DOM 等操作。这些操作都会在组件渲染之后执行,可能会影响组件之外的事物。
例如一个窗口组件,需要检测它是否获取到焦点,如果我手动处理,就必须在窗口函数之中记录和维护焦点信息,而这和 React 的设计逻辑——一个只关注渲染的组件相违背
而使用了 useEffect 之后,焦点相关的代码实质上被独立出 ReactDOM, 而窗口组件重新成为纯函数,在 DOM 渲染完毕之后,React 检测 useEffect 生效与否,最后执行副作用,保证了副作用的执行顺序,使得整个 App 最后依然是纯函数
例:(这个例子并不实际,因为浏览器默认有聚焦行为覆盖了这个代码,不需要这样手动加 ,并且这样也只是在第一次点击的时候有聚焦)
import "bootstrap/dist/css/bootstrap.css";
import { useRef, useEffect } from "react";
const FocusInput = () => {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
ref.current?.focus();
}, []);
return (
<div className="md-3">
<input ref={ref} type="text" className="form-control" />
</div>
);
};
export default FocusInput;
useEffect 接受两个参数,第一个是回调函数,第二个是控制何时执行的对象——只有当该对象改变时,useEffect 才会执行回调函数
如果只执行一次,传入一个空数组
如果第二个参数什么都不传递,那么每次组件重新渲染的时候,react 都会调用回调函数,如果回调函数之中又有 setState,就成功的无限循环了(
useEffect Clean up: useEffect 的第一个参数(回调函数 f)可以有非空返回值,返回另一个 函数 g 时,当屏幕上 unmount 该组件时,该函数(g)会被调用,一般用于清理
Fetching Data
JSON Place Holder
Sending HTTP Request:
- 原生 Fetch 方法
- Axios 库 [recommended]
Promise: A promise is an object that holds the eventual result or failure of an asynchronous operation
promise 有一个 then() 方法,用于执行回调函数
Handling Errors
promise 有一个 catch()方法,捕获错误,执行回调
示例代码:
import axios from "axios";
import { useEffect, useState } from "react";
interface User {
id: number;
name: string;
}
function App() {
const [users, setUsers] = useState<User[]>([]);
const [err, setError] = useState("");
useEffect(() => {
axios
.get("https://jsonplaceholder.typicode.com/users")
.then((res) => setUsers(res.data))
.catch((err) => setError(err.message));
}, []);
return (
<>
{err && <p className="text-danger"> {err}</p>}
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</>
);
}
export default App;
还有一种方法是使用 async 和 await,但略繁琐
ps:实际上 then 和 catch 好像是后面才提出来的语法糖(
useEffect(() => {
const fetchUsers = async () => {
try {
const res = await axios.get("https://jsonplaceholder.typicode.com/users");
setUsers(res.data);
} catch (err) {
setError((err as AxiosError).message);
}
};
fetchUsers();
}, []);
当用户离开此页怎么办?不再需要 Fetch data,使用先前提到的 useEffect Clean up
使用 Controller
AbortController class,浏览器内置,允许取消或中 止异步操作
const controller = new AbortController();
useEffect(() => {
axios
.get("https://jsonplaceholder.typicode.com/users", {signal: controller.signal})
.then((res) => setUsers(res.data))
.catch((err) => {
if (err instanceof CanceledError) return;
setError(err.message);
});
return () => controller.abort();
}, []);
Loading 效果:测试,在 network 选项卡里面加 throtting 模拟慢速情况
在两个组件之间加点空白
<ComponentA />{" "}<ComponentB />
更优的处理,使用 ListGroup 类和 flex 类来调整空间布局
CRUD
删除 Delete:delete
乐观更新/悲观更新:先更新 UI 响应(预计 Server 更新会成功)还是先更新 Server 数据(预计 Server 更新会失败)
一般为了用户体验会选择乐观更新
const onDelete = (id: number) => {
const originalUsers = [...users];
setUsers(users.filter((user) => user.id !== id));
// 先setUsers再让server delete,乐观更新
axios
.delete(`https://jsonplaceholder.typicode.com/xusers/${id}`)
.catch((err) => {
setError(err.message);
setUsers(originalUsers); // 错误时要回到原始状态
});
};
创建 Create: post
axios.post("https://jsonplaceholder.typicode.com/users", newUser).then((res)=>setUsers[res.data, ...users]).catch((err)=>{
setError(err.message);
setUsers(originalUsers);
})
更新 Update: put/patch
const onUpdate = (User: User) => {
const originalUsers = [...users];
const newUser = { ...User, name: User.name + "!" };
setUsers(users.map((user) => (user.id === User.id ? newUser : user)));
axios
.patch("https://jsonplaceholder.typicode.com/users/" + User.id, newUser)
.catch((err) => {
setError(err.message);
setUsers(originalUsers);
});
};
A reusable API client: 思考怎么重构解耦
将 axios 和 baseURL 放到 service 文件夹里面去
进一步,再把整个 HTTPRequest 的逻辑放到 userService 里面去
再进一步,使用泛型把 userService 的逻辑放到 HTTPService 里面去
再进一步,一堆 Hook 可以放到一个大的自定义 Hook 里面去
一个 Hook 没什么神奇的,就是一个导出{obj, setObj, ...}的函数组件,只要它的内部还是 useState 之类,就不需要探知到 React 的底层 DOM
Build A Project!
流程实录
如果想做的最好对着 mosh 的视频做,这里只是杂乱的一点做的笔记
chakra UI: 官网找 vite 下载版本,然后 ChakraProvider 包裹 App
ps: 如果 chakra UI 引入的组件报错 "Expression produces a union type that is too complex to represent.", 别慌,能正常运行,这是 ts 版本不匹配导致的, 参见此处, 将 ts 版本降为工作区版本即可
前端部分:
1fr: 一个分数单位
在 CSS Grid 中,
fr是一个单位,代表 "fractional unit"(分数单位)。它用于分配可用空间。如果你设置
grid-template-columns: repeat(5, 1fr);,这意味着你想要创建五列,每列的宽度都是可用空间的一个等份。例如,如果可用空间是 500px,那么每列的宽度就是 100px。
针对移动端和大屏幕设计 UI, F12 模拟移动端
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import "./App.css";
import { Grid, GridItem, Show } from "@chakra-ui/react";
function App() {
const templateAreas = {
base: `"nav" "main"`, // 移动端小屏幕没有aside侧边栏
lg: `"nav nav" "aside main"`, // > 1000px
};
return (
<div className="App">
<Grid templateAreas={templateAreas}>
<GridItem area="nav" bg="coral">
Nav
</GridItem>
<Show above="lg">
<GridItem area="aside" bg="gold">
Aside
</GridItem>
</Show>
<GridItem area="main" bg="dodgerblue">
Main
</GridItem>
</Grid>
</div>
);
}
export default App;
theme:colormode
使用 colorMode 来实现暗色模式和亮色模式的切换(切完 F12-> App-> storage,清除 local storage 的颜色 mode 验证)
使用 useColorMode 和 Switch 来做开关
使用 Hstack 和 justify-content 来调整位置
FetchGames:
查官方 api 文档,axios 建一个 api-client (create 方法, baseURL + key), 然后再用 get 方法 fetch 数据
GameCard && GameGrid: 使用 SimpleGrid && Card 组件
移动端适配:不硬编码 column,传入一个对象{sm: 1, md: 2, lg: 3, xl: 5}
添加图标:使用第三方图标库 https://chakra-ui.com/docs/components/icon/usage#using-a-third-party-icon-library
先从 API 拿到 slug(全小写名字),再写一个 map 函数来对应图标
const IconMap: { [key: string]: IconType } = {
pc: FaWindows,
playstation: FaPlaystation,
xbox: FaXbox,
nintendo: SiNintendo,
mac: FaApple,
ios: MdPhoneIphone,
linux: FaLinux,
};
注意=和...在传递 props 时候的区别
props={} ,传递的是一个对象; ...传递的是对象里面所有的属性
<PlatformIconList
{...(game.platforms.map((item) => item.platform) as Platform[])}
/> // 传递的是表达式对象里面的数组展开后的结果
<PlatformIconList
platforms={(game.platforms.map((item) => item.platform) as Platform[])}
/> // 传递的是一个数组用{}包裹后的对象
// 用那种取决于子组件接受的是一个对象还是一个数组,一般是在子组件定义一个接口,然后接受一个对象,并用解构接收
interface PlatformIconListProps {
platforms: Platform[];
}
const PlatformIconList = ({ platforms }: PlatformIconListProps) => {
// 类似这样,这样在传递的时候就是传一个数组用{}包裹后的对象,即=语法
添加评分
Badge(徽章)类
添加加载时骨架
refactor:将重复的样式 提出成为一个 GameCardContainer 组件
fetch the genres: 类似 useGames 的 custom hook,取来数据之后再放到 Aside 位置上即可
侧边栏加 Icon: 调 padding(内间距)和 spacing(外间距) , 利用 Grid 类的 templateColumn 200px 1fr 来达到自适应
完善根据 genre 选择呈现列表
需要注意的是如何修改 useEffect,如何上提控制状态和下放控制逻辑和 axios 之中方法所带的 AxiosRequestConfig 参数的使用(在请求体之中用来携带数据)
这里的逻辑是每次切换 selectedGenre 都重新向 API(模拟的后端)请求一次数据,而不是用先前请求的数据做 filter,filter 的逻辑放给后端做了
const useGames = (selectedGenre: Genre | null) => {
return useData<Game>("/games", { params: { genres: selectedGenre?.id } }, [
selectedGenre,
]);
};
useData 的处理方法
import { useEffect, useState } from "react";
import ApiClient from "./Api-Client";
import { AxiosRequestConfig, CanceledError } from "axios";
interface dataResponse<T> {
count: number;
results: T[];
}
const useData = <T,>(
endpoint: string,
config?: AxiosRequestConfig,
dep?: any[]
) => {
const [data, setData] = useState<T[]>([]);
const [err, setError] = useState<string>("");
const [loading, setLoading] = useState(true);
const controller = new AbortController();
const FetchGames = useEffect(
() => {
setLoading(true); // 每次重新取数据重置loading
ApiClient.get<dataResponse<T>>(endpoint, {
signal: controller.signal,
...config,
})
.then((res) => {
setData(res.data.results);
setLoading(false);
})
.catch((err) => {
if (err instanceof CanceledError) return;
setError(err.message);
setLoading(false);
});
},
dep ? [...dep] : []
);
return { data, err, loading };
};
export default useData;
highlight selectedGenre: fontweight 属性即可
fontWeight={genre.id === selectedGenre?.id ? "bold" : "normal"}
filterByPlatform
Menu 类 下拉列表, 如果有错误,宁愿什么都不返回(取消这个组件)而不是将错误扔在用户脸上的设计原则
其他逻辑同 genre
重构:使用 Query Object Model,传入的各个 select 的 props 打包成 Query
下一步: 加排序 Sort,多种排序顺序还是一个 Menu
如果一直向 api 发送请求(体现在 UI 上是一直闪烁), 可能是不恰当地传递了解构又重构的对象(即使是临时的对象),这样之后,看起来对象内部的值没有变化,但是对象不断地更新,然后 React 一直重复渲染
处理没有图片的情况:Image 的 src 不能使用相对路径,需要通过 import 导入
加上动态的 Heading
处理 Button 的文本和图片重叠问题:改它的 whiteSpace 属性为"normal", 再设置 textAlign=left左对齐
objectfit="cover"保持图片长宽比
shipping static data,当然,作为之前选择的回旋镖,也可以选择将 Genre,Platform 这种不怎么改变的数据存储在本地一个 data 文件夹之中,而不是每次都到 API 里面的查询
可以使用官方文档上的方法自定义颜色主题
部署
先本地部署
npm run build
会生成 dist 文件夹,里面有编译完的 js 代码,所有需要的外部包和 asset......
在 Vercel(https://vercel.com/)上部署
使用 github 账号注册,登录,下载 CLI
npm install -g vercel
之后
vercel
接受默认值即可部署
ps: 这个好像在使用 cnpm 的时候会有问题,我没有部署成功,两者交互会有问题
What's Next
-
Routing,
-
State Management,
-
Fetching Data With React Query
-
Authentication
-
Error Handling
-
Performance Optimazation
-
......