文章目录
相对原教程,我在学习开始时(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 |
具体配置、操作和内容会有差异,“坑”也会有所不同。。。
一、项目起航:项目初始化与配置
二、React 与 Hook 应用:实现项目列表
三、TS 应用:JS神助攻 - 强类型
四、JWT、用户认证与异步请求
五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
六、用户体验优化 - 加载中和错误状态处理
七、Hook,路由,与 URL 状态管理
八、用户选择器与项目编辑功能
九、深入React 状态管理与Redux机制
十、用 react-query 获取数据,管理缓存
刷完副本,返回主分支
1.用url参数管理项目模态框状态
为了用 url
参数管理项目模态框状态,在 src\screens\ProjectList\utils.ts
中新增一个 Custom Hook
export const useProjectModal = () => {
const [{
projectCreate}, setProjectCreate] = useUrlQueryParam(['projectCreate'])
const open = () => setProjectCreate({
projectCreate: true})
const close = () => setProjectCreate({
projectCreate: false}) // 若在url显示 false,可以改为 undefined 隐藏
return {
projectModalOpen: projectCreate === 'true',
open,
close
}
}
接下来去掉之前 组合组件的使用,在对应位置直接使用组件并调用 useProjectModal
中的状态和方法
编辑 src\authenticated-app.tsx
:
...
export const AuthenticatedApp = () => {
- const [isOpen, setIsOpen] = useState(false);
useDocumentTitle("项目列表", false);
return (
<Container>
- <PageHeader
- projectButton={
- <ButtonNoPadding type="link" onClick={() => setIsOpen(true)}>
- 创建项目
- </ButtonNoPadding>
- }
- />
+ <PageHeader/>
<Main>
<Router>
<Routes>
- <Route
- path="/projects"
- element={
- <ProjectList
- projectButton={
- <ButtonNoPadding
- type="link"
- onClick={() => setIsOpen(true)}
- >
- 创建项目
- </ButtonNoPadding>
- }
- />
- }
- />
+ <Route path="/projects" element={<ProjectList/>}/>
...
</Routes>
</Router>
</Main>
- <ProjectModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
+ <ProjectModal/>
</Container>
);
};
- const PageHeader = (props: { projectButton: JSX.Element }) => {
+ const PageHeader = () => {
...
return (
<Header between={true}>
<HeaderLeft gap={true}>
...
- <ProjectPopover {...props} />
+ <ProjectPopover/>
<span>用户</span>
</HeaderLeft>
...
</Header>
);
};
...
编辑 src\screens\ProjectList\index.tsx
:
...
- import { useProjectsSearchParams } from "./utils";
+ import { useProjectModal, useProjectsSearchParams } from "./utils";
...
+ import { ButtonNoPadding } from "components/lib";
- export const ProjectList = ({ projectButton }: { projectButton: JSX.Element }) => {
+ export const ProjectList = () => {
+ const {open} = useProjectModal()
...
return (
<Container>
<Row justify="space-between">
<h1>项目列表</h1>
- {projectButton}
+ <ButtonNoPadding type="link" onClick={open}>创建项目</ButtonNoPadding>
</Row>
<SearchPanel users={users || []} param={param} setParam={setParam} />
{error ? (
<Typography.Text type="danger">{error.message}</Typography.Text>
) : null}
<List
- projectButton={projectButton}
...
/>
</Container>
);
};
...
编辑 src\screens\ProjectList\components\List.tsx
:
...
+ import { useProjectModal } from "../utils";
...
interface ListProps extends TableProps<Project> {
users: User[];
refresh?: () => void;
- projectButton: JSX.Element;
}
// type PropsType = Omit<ListProps, 'users'>
export const List = ({ users, ...props }: ListProps) => {
+ const {open} = useProjectModal()
...
return (
<Table
pagination={false}
columns={[
...
{
render: (text, project) => {
const items: MenuProps["items"] = [
{
key: "edit",
label: "编辑",
+ onClick: open
},
];
return (
- <Dropdown dropdownRender={() => props.projectButton}>
+ <Dropdown menu={
{items}}>
<ButtonNoPadding
type="link"
onClick={(e) => e.preventDefault()}
>
...
</ButtonNoPadding>
</Dropdown>
);
},
},
]}
{...props}
></Table>
);
};
编辑 src\screens\ProjectList\components\ProjectModal.tsx
:
...
+ import { useProjectModal } from "../utils";
- export const ProjectModal = ({isOpen, onClose}: {isOpen: boolean; onClose: () => void;}) => {
+ export const ProjectModal = () => {
+ const {projectModalOpen, close} = useProjectModal()
return (
- <Drawer onClose={onClose} open={isOpen} width="100%">
+ <Drawer onClose={close} open={projectModalOpen} width="100%">
<h1>Project Modal</h1>
- <Button onClick={onClose}>关闭</Button>
+ <Button onClick={close}>关闭</Button>
</Drawer>
);
};
编辑 src\screens\ProjectList\components\ProjectPopover.tsx
:
...
+ import { ButtonNoPadding } from "components/lib";
+ import { useProjectModal } from "../utils";
- export const ProjectPopover = ({projectButton}: {projectButton: JSX.Element;}) => {
+ export const ProjectPopover = () => {
+ const {open} = useProjectModal()
...
const content = (
<ContentContainer>
...
- {projectButton}
+ <ButtonNoPadding type="link" onClick={open}>创建项目</ButtonNoPadding>
</ContentContainer>
);
...
};
...
更改完后查看页面效果,报错:
useLocation() may be used only in the context of a <Router> component.
查看代码发现 src\authenticated-app.tsx
中 PageHeader
和 ProjectModal
没有包在 Router
里面,而使用 useUrlQueryParam
的相关功能需要放在 Router
里面,修改一下:
...
export const AuthenticatedApp = () => {
...
return (
<Container>
+ <Router>
<PageHeader/>
<Main>
- <Router>
<Routes>
<Route path="/projects" element={<ProjectList/>}/>
<Route path="/projects/:projectId/*" element={<ProjectDetail />} />
<Route index element={<Navigate to="projects" />} />
</Routes>
- </Router>
</Main>
<ProjectModal/>
+ </Router>
</Container>
);
};
...
再次查看页面,功能正常!
2.用 react-query 来处理 服务端缓存
3.类型守卫,用useQuery缓存工程列表
部分视频在线查看地址:https://space.bilibili.com/2814498/video
react-query
在之前 安装另一个版本的 jira-dev-tool
时已经安装过了,安装命令:
npm i react-query # --force
接下来 使用 react-query
改造 Project
相关接口调用
编辑 src\utils\project.ts
...
import {
useQuery } from "react-query";
export const useProjects = (param?: Partial<Project>) => {
const client = useHttp();
return useQuery<Project[]>(['projects', param], () => client("projects", {
data: cleanObject(param || {
}) }))
};
...
useQuery
如果命中缓存,那么onSuccess
对应的方法不会被触发- 之前这部分功能就是模仿
react-query
写的,一些功能替换后可直接使用,一些不能:
- 之前的
rerun
(视频中retry
)功能可以将useQuery
中的第一个参数QueryKey
改为 数组形式[key, ...otherParams]
,数组首项作为传统意义上的key
,后续参数可作为触发隐性 “rerun
” 的“依赖”,当参数有变化时即使命中缓存依旧触发onSuccess
对应的方法- 对于请求会出现报错的情况,虽然
useQuery
中也有相同Error
数据返回,但类型(unknown
而非Error
)并不是我们想要的, 后续使用会有类型报错,两种解决办法:
- 1.显示声明返回值类型:
useQuery<Project[], Error>()
- 2.使用类型守卫,具体见下
编辑 src\components\lib.tsx
新增类型守卫和 ErrorBox
组件:
...
// 类型守卫
const isError = (value: any): value is Error => value?.message
export const ErrorBox = ({
error}: {
error: unknown}) => {
if (isError(error)) {
return <Typography.Text type="danger">{
error.message}</Typography.Text>
}
return null
}
...
is
是一个 类型谓词,用来帮助ts
编译器 收窄 变量的类型
其它有需要使用到 ErrorBox
的地方依次替换:
编辑 src\unauthenticated-app\index.tsx
:
...
import {
ErrorBox } from "components/lib";
export const UnauthenticatedApp = () => {
...
return (
<Container>
...
<ShadowCard>
<Title>{
isRegister ? "请注册" : "请登录"}</Title>
<ErrorBox error={
error}/>
...
</ShadowCard>
</Container>
);
};
...
编辑 src\screens\ProjectList\index.tsx
:
...
- import { ButtonNoPadding } from "components/lib";
+ import { ButtonNoPadding, ErrorBox } from "components/lib";
export const ProjectList = () => {
...
const {
isLoading,
error,
data: list,
- rerun
} = useProjects(useDebounce(param));
const { data: users } = useUsers();
return (
<Container>
...
<SearchPanel users={users || []} param={param} setParam={setParam} />
- {error ? (
- <Typography.Text type="danger">{error.message}</Typography.Text>
- ) : null}
+ <ErrorBox error={error}/>
<List
- refresh={rerun}
loading={isLoading}
users={users || []}
dataSource={list || []}
/>
</Container>
);
};
...
同时去掉使用 rerun
(视频中 retry
)的部分
编辑 src\screens\ProjectList\components\List.tsx
:
...
export const List = ({ users, ...props }: ListProps) => {
...
const starProject = (id: number) => (star: boolean) =>
- mutate({ id, star }).then(props.refresh);
+ mutate({ id, star })
return (
<Table
pagination={false}
columns={[
...
{
render: (text, project) => {
const items: MenuProps["items"] = [
{
key: "edit",
label: "编辑",
onClick: open,
- },
+ }, {
+ key: "delete",
+ label: "删除",
+ // onClick: open,
+ },
];
...
},
},
]}
{...props}
></Table>
);
};
查看页面效果,查询功能正常,报错信息展示正常
部分引用笔记还在草稿阶段,敬请期待。。。