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