文章目录
- 一、项目起航:项目初始化与配置
- 二、React 与 Hook 应用:实现项目列表
- 1.新建文件
- 2.状态提升
- 3.新建utils
- 4.Custom Hook
学习内容来源:React + React Hook + TS 最佳实践-慕课网
相对原教程,我在学习开始时(2023.03)采用的是当前最新版本:
项 | 版本 |
---|---|
react & react-dom | ^18.2.0 |
react-router & react-router-dom | ^6.11.2 |
antd | ^4.24.8 |
@commitlint/cli & @commitlint/config-conventional | ^17.4.4 |
eslint-config-prettier | ^8.6.0 |
husky | ^8.0.3 |
lint-staged | ^13.1.2 |
prettier | 2.8.4 |
json-server | 0.17.2 |
craco-less | ^2.0.0 |
@craco/craco | ^7.1.0 |
qs | ^6.11.0 |
dayjs | ^1.11.7 |
react-helmet | ^6.1.0 |
@types/react-helmet | ^6.1.6 |
react-query | ^6.1.0 |
@welldone-software/why-did-you-render | ^7.0.1 |
@emotion/react & @emotion/styled | ^11.10.6 |
具体配置、操作和内容会有差异,“坑”也会有所不同。。。
一、项目起航:项目初始化与配置
- 【实战】 项目起航:项目初始化与配置 —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(一)
二、React 与 Hook 应用:实现项目列表
1.新建文件
- 新建文件:src\screens\ProjectList\index.jsx
import { SearchPanel } from "./components/SearchPanel"
import { List } from "./components/List"
export const ProjectListScreen = () => {
return <div>
<SearchPanel/>
<List/>
</div>
}
- 新建文件:src\screens\ProjectList\components\List.jsx
export const List = () => {
return <table></table>
}
- 新建文件:src\screens\ProjectList\components\SearchPanel.jsx
import { useEffect, useState } from "react"
export const SearchPanel = () => {
const [param, setParam] = useState({
name: '',
personId: ''
})
const [users, setUsers] = useState([])
const [list, setList] = useState([])
useEffect(() => {
fetch('').then(async res => {
if (res.ok) {
setList(await res.json())
}
})
}, [param])
return <form>
<div>
{/* setParam(Object.assign({}, param, { name: evt.target.value })) */}
<input type="text" value={param.name} onChange={evt => setParam({
...param,
name: evt.target.value
})}/>
<select value={param.personId} onChange={evt => setParam({
...param,
personId: evt.target.value
})}>
<option value="">负责人</option>
{
users.map(user => (<option key={user.id} value={user.id}>{user.name}</option>))
}
</select>
</div>
</form>
}
- 相对原视频,这里为组件命名使用的是:大驼峰法(Upper Camel Case)
- 相对原视频,目录结构和变量名都可以按自己习惯来的哦!
- 编码过程很重要,但文字不好体现。。。
- vscode 在 JS 文件中不会自动补全 HTML标签可参考:【小技巧】vscode 在 JS 文件中补全 HTML标签
2.状态提升
由于 list
和 param
涉及两个不同组件,因此需要将这两个 state
提升到他们共同的父组件中,子组件通过解构 props
使用:
list
由List
消费;list
根据param
获取;param
由SearchPanel
消费;
按照数据库范式思维,project
、users
各自单独一张表、而 list
只是关联查询的中间产物,hard
模式中通过 project
只能得到 users
的主键,即 personId
,需要根据 personId
再去获取 personName
,因此 users
也需要做状态提升
为了 DRY
原则,将接口调用URL中的 http://host:port
提取到 项目全局环境变量 中:
- .env
REACT_APP_API_URL=http://online.com
- .env.development
REACT_APP_API_URL=http://localhost:3001
webpack
环境变量识别规则的理解:
- 执行
npm start
时,webpack
读取.env.development
中的环境变量;- 执行
npm run build
时,webpack
读取.env
中的环境变量;
3.新建utils
常用工具方法统一放到 utils/index.js
中
- 由于在fetch传参过程中,多个可传参数单只传一个,那么空参需要过滤(过滤过程中考虑到
0
是有效参数,因此特殊处理):
export const isFalsy = val => val === 0 ? false : !val
// 在函数里,不可用直接赋值的方式改变传入的引用类型变量
export const cleanObject = obj => {
const res = { ...obj }
Object.keys(res).forEach(key => {
const val = res[key]
if (isFalsy(val)) {
delete res[key]
}
})
return res
}
- Falsy - MDN Web 文档术语表:Web 相关术语的定义 | MDN
- 在url后拼参时,参数较多会显得繁琐,因此引入
qs
npm i qs
- qs - npm
经过前面两步,状态提升并使用 cleanObject
和 qs
处理参数后,源码如下:
src\screens\ProjectList\index.jsx
import { SearchPanel } from "./components/SearchPanel";
import { List } from "./components/List";
import { useEffect, useState } from "react";
import { cleanObject } from "utils";
import * as qs from 'qs'
const apiUrl = process.env.REACT_APP_API_URL;
export const ProjectListScreen = () => {
const [users, setUsers] = useState([]);
const [param, setParam] = useState({
name: "",
personId: "",
});
const [list, setList] = useState([]);
useEffect(() => {
fetch(
// name=${param.name}&personId=${param.personId}
`${apiUrl}/projects?${qs.stringify(cleanObject(param))}`
).then(async (res) => {
if (res.ok) {
setList(await res.json());
}
});
}, [param]);
useEffect(() => {
fetch(`${apiUrl}/users`).then(async (res) => {
if (res.ok) {
setUsers(await res.json());
}
});
}, []);
return (
<div>
<SearchPanel users={users} param={param} setParam={setParam} />
<List users={users} list={list} />
</div>
);
};
src\screens\ProjectList\components\List.jsx
export const List = ({ users, list }) => {
return (
<table>
<thead>
<tr>
<th>名称</th>
<th>负责人</th>
</tr>
</thead>
<tbody>
{list.map((project) => (
<tr key={project.id}>
<td>{project.name}</td>
{/* undefined.name */}
<td>
{users.find((user) => user.id === project.personId)?.name ||
"未知"}
</td>
</tr>
))}
</tbody>
</table>
);
};
src\screens\ProjectList\components\SearchPanel.jsx
export const SearchPanel = ({ users, param, setParam }) => {
return (
<form>
<div>
{/* setParam(Object.assign({}, param, { name: evt.target.value })) */}
<input
type="text"
value={param.name}
onChange={(evt) =>
setParam({
...param,
name: evt.target.value,
})
}
/>
<select
value={param.personId}
onChange={(evt) =>
setParam({
...param,
personId: evt.target.value,
})
}
>
<option value="">负责人</option>
{users.map((user) => (
<option key={user.id} value={user.id}>
{user.name}
</option>
))}
</select>
</div>
</form>
);
};
src\App.tsx
import "./App.css";
import { ProjectListScreen } from "screens/ProjectList";
function App() {
return (
<div className="App">
<ProjectListScreen />
</div>
);
}
export default App;
现在效果:可以通过项目名和人名筛选(全匹配)
4.Custom Hook
Custom Hook 可是代码复用利器
- useMount:生命周期模拟 —— componentDidMount
export const useMount = cbk => useEffect(() => cbk(), [])
正常情况下 useEffect 只执行一次,但是 react@v18 严格模式下 useEffect 默认执行两遍,具体详见:【已解决】react@v18 严格模式下 useEffect 默认执行两遍
- useDebounce:防抖
/**
* @param { 值 } val
* @param { 延时:默认 1000 } delay
* @returns 在某段时间内多次变动后最终拿到的值(delay 延迟的是存储在队列中的上一次变化)
*/
export const useDebounce = (val, delay = 1000) => {
const [tempVal, setTempVal] = useState(val)
useEffect(() => {
// 每次在 val 变化后,设置一个定时器
const timeout = setTimeout(() => setTempVal(val), delay)
// 每次在上一个 useEffect 处理完以后再运行(useEffect 的天然功能即是在运行结束的 return 函数中清除上一个(同一) useEffect)
return () => clearTimeout(timeout)
}, [val, delay])
return tempVal
}
// 日常案例,对比理解
// const debounce = (func, delay) => {
// let timeout;
// return () => {
// if (timeout) {
// clearTimeout(timeout);
// }
// timeout = setTimeout(function () {
// func()
// }, delay)
// }
// }
// const log = debounce(() => console.log('call'), 5000)
// log()
// log()
// log()
// ...5s
// 执行!
// debounce 原理讲解:
// 0s ---------> 1s ---------> 2s --------> ...
// 这三个函数是同步操作,它们一定是在 0~1s 这个时间段内瞬间完成的;
// log()#1 // timeout#1
// log()#2 // 发现 timeout#1!取消之,然后设置timeout#2
// log()#3 // 发现 timeout#2! 取消之,然后设置timeout#3
// // 所以,log()#3 结束后,就只有最后一个 —— timeout#3 保留
拓展学习:【笔记】深度理解并 js 手写不同场景下的防抖函数
- 使用了
Custom Hook
后的src\screens\ProjectList\index.js
(lastParam
定义在紧挨param
后)
...
// 对 param 进行防抖处理
const lastParam = useDebounce(param)
const [list, setList] = useState([]);
useEffect(() => {
fetch(
// name=${param.name}&personId=${param.personId}
`${apiUrl}/projects?${qs.stringify(cleanObject(lastParam))}`
).then(async (res) => {
if (res.ok) {
setList(await res.json());
}
});
}, [lastParam]);
useMount(() => {
fetch(`${apiUrl}/users`).then(async (res) => {
if (res.ok) {
setUsers(await res.json());
}
});
});
...
这样便可 1s
内再次输入不会触发对 projects
的 fetch
请求
拓展学习:
- 【笔记】Custom Hook
部分引用笔记还在草稿阶段,敬请期待。。。