文章目录
- 一、项目起航:项目初始化与配置
- 二、React 与 Hook 应用:实现项目列表
- 三、TS 应用:JS神助攻 - 强类型
- 四、JWT、用户认证与异步请求
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
- 六、用户体验优化 - 加载中和错误状态处理
- 1.给页面添加 Loading 和 Error 状态,增加页面友好性
- 2.用高级 Hook-useAsync 统一处理 Loading 和 Error 状态
学习内容来源: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 应用:实现项目列表
- 【实战】 二、React 与 Hook 应用:实现项目列表 —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(二)
三、TS 应用:JS神助攻 - 强类型
- 【实战】三、 TS 应用:JS神助攻 - 强类型 —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(三)
四、JWT、用户认证与异步请求
- 【实战】四、 JWT、用户认证与异步请求(上) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(四)
- 【实战】四、 JWT、用户认证与异步请求(下) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(五)
五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
- 【实战】 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(上) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(六)
- 【实战】 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(下) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(七)
六、用户体验优化 - 加载中和错误状态处理
1.给页面添加 Loading 和 Error 状态,增加页面友好性
修改 src\screens\ProjectList\index.tsx
(新增 loading 状态 和 请求错误提示)(部分未修改内容省略):
...
import { Typography } from "antd";
export const ProjectList = () => {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<null | Error>(null);
...
useEffect(() => {
setIsLoading(true)
// React Hook "useHttp" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.
client("projects", { data: cleanObject(lastParam) }).then(setList)
.catch(error => {
setList([])
setError(error)
})
.finally(() => setIsLoading(false));
// React Hook useEffect has a missing dependency: 'client'. Either include it or remove the dependency array.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastParam]);
...
return (
<Container>
<h1>项目列表</h1>
<SearchPanel users={users} param={param} setParam={setParam} />
{error ? <Typography.Text type="danger">{error.message}</Typography.Text> : null}
<List loading={isLoading} users={users} dataSource={list} />
</Container>
);
};
...
修改 src\screens\ProjectList\components\List.tsx
(ListProps
继承 TableProps
, Table
的属性(透传))(部分未修改内容省略):
import { Table, TableProps } from "antd";
...
interface ListProps extends TableProps<Project> {
users: User[];
}
// type PropsType = Omit<ListProps, 'users'>
export const List = ({ users, ...props }: ListProps) => {
return (
<Table
pagination={false}
columns={...}
{ ...props }
></Table>
);
};
为方便后续在组件外再次配置
Table
的属性(透传),直接让ListProps
继承TableProps
,TableProps
单独抽出到props
配置请求最短时间(如下图),即可清楚看到 loading
效果
配置请求失败比例为百分百即可看到错误提示:
2.用高级 Hook-useAsync 统一处理 Loading 和 Error 状态
新建 src\utils\use-async.ts
(统一对 异步状态 和 请求数据 的管理):
import { useState } from "react";
interface State<D> {
error: Error | null;
data: D | null;
stat: 'ready' | 'loading' | 'error' | 'success'
}
const defaultInitialState: State<null> = {
stat: 'ready',
data: null,
error: null
}
export const useAsync = <D>(initialState?: State<D>) => {
const [state, setState] = useState<State<D>>({
...defaultInitialState,
...initialState
})
const setData = (data: D) => setState({
data,
stat: 'success',
error: null
})
const setError = (error: Error) => setState({
error,
stat: 'error',
data: null
})
// run 来触发异步请求
const run = (promise: Promise<D>) => {
if(!promise || !promise.then) {
throw new Error('请传入 Promise 类型数据')
}
setState({...state, stat: 'loading'})
return promise.then(data => {
setData(data)
return data
}).catch(error => {
setError(error)
return error
})
}
return {
isReady: state.stat === 'ready',
isLoading: state.stat === 'loading',
isError: state.stat === 'error',
isSuccess: state.stat === 'success',
run,
setData,
setError,
...state
}
}
修改 src\screens\ProjectList\components\List.tsx
(将 Project
导出,以便后续引用)(部分未修改内容省略):
...
export interface Project {...}
...
修改 src\screens\ProjectList\index.tsx
(部分未修改内容省略):
- 删去之前
loading
和error
相关内容; - 删去
client
异步请求then
及后续操作; - 使用
useAsync
统一处理 异步状态 和 请求数据;
...
import { List, Project } from "./components/List";
...
import { useAsync } from "utils/use-async";
export const ProjectList = () => {
const [users, setUsers] = useState([]);
const [param, setParam] = useState({
name: "",
personId: "",
});
// 对 param 进行防抖处理
const lastParam = useDebounce(param);
const client = useHttp();
const { run, isLoading, error, data: list } = useAsync<Project[]>();
useEffect(() => {
run(client("projects", { data: cleanObject(lastParam) }))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastParam]);
useMount(() => client("users").then(setUsers));
return (
<Container>
...
<List loading={isLoading} users={users} dataSource={list || []} />
</Container>
);
};
...
新建 src\utils\project.ts
(单独处理 Project 数据的异步请求):
import { cleanObject } from "utils";
import { useHttp } from "./http";
import { useAsync } from "./use-async";
import { useEffect } from "react";
import { Project } from "screens/ProjectList/components/List";
export const useProjects = (param?: Partial<Project>) => {
const client = useHttp();
const { run, ...result } = useAsync<Project[]>();
useEffect(() => {
run(client("projects", { data: cleanObject(param || {}) }))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [param]);
return result
}
新建 src\utils\use-users.ts
(单独处理 User 数据的异步请求):
import { cleanObject } from "utils";
import { useHttp } from "./http";
import { useAsync } from "./use-async";
import { useEffect } from "react";
import { User } from "screens/ProjectList/components/SearchPanel";
export const useUsers = (param?: Partial<User>) => {
const client = useHttp();
const { run, ...result } = useAsync<User[]>();
useEffect(() => {
run(client("users", { data: cleanObject(param || {}) }))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [param]);
return result
}
再次修改 src\screens\ProjectList\index.tsx
(部分未修改内容省略):
Project
和User
数据获取分别单独抽离
import { SearchPanel } from "./components/SearchPanel";
import { List } from "./components/List";
import { useState } from "react";
import { useDebounce } from "utils";
import styled from "@emotion/styled";
import { Typography } from "antd";
import { useProjects } from "utils/project";
import { useUsers } from "utils/use-users";
export const ProjectList = () => {
const [param, setParam] = useState({
name: "",
personId: "",
});
// 对 param 进行防抖处理后接入请求
const { isLoading, error, data: list } = useProjects(useDebounce(param));
const { data: users } = useUsers();
return (
<Container>
<h1>项目列表</h1>
<SearchPanel users={users || []} param={param} setParam={setParam} />
{error ? (
<Typography.Text type="danger">{error.message}</Typography.Text>
) : null}
<List loading={isLoading} users={users || []} dataSource={list || []} />
</Container>
);
};
...
测试功能:一切正常!
部分引用笔记还在草稿阶段,敬请期待。。。