文章目录
- 一、项目起航:项目初始化与配置
- 二、React 与 Hook 应用:实现项目列表
- 三、TS 应用:JS神助攻 - 强类型
- 四、JWT、用户认证与异步请求
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
- 六、用户体验优化 - 加载中和错误状态处理
- 七、Hook,路由,与 URL 状态管理
- 八、用户选择器与项目编辑功能
- 九、深入React 状态管理与Redux机制
- 十、用 react-query 获取数据,管理缓存
- 十一、看板页面及任务组页面开发
- 1~3
- 4~6
- 7.编辑任务功能
- 8.看板和任务删除功能
学习内容来源: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 |
具体配置、操作和内容会有差异,“坑”也会有所不同。。。
一、项目起航:项目初始化与配置
- 一、项目起航:项目初始化与配置
二、React 与 Hook 应用:实现项目列表
- 二、React 与 Hook 应用:实现项目列表
三、TS 应用:JS神助攻 - 强类型
- 三、 TS 应用:JS神助攻 - 强类型
四、JWT、用户认证与异步请求
- 四、 JWT、用户认证与异步请求(上)
- 四、 JWT、用户认证与异步请求(下)
五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(上)
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(下)
六、用户体验优化 - 加载中和错误状态处理
- 六、用户体验优化 - 加载中和错误状态处理(上)
- 六、用户体验优化 - 加载中和错误状态处理(中)
- 六、用户体验优化 - 加载中和错误状态处理(下)
七、Hook,路由,与 URL 状态管理
- 七、Hook,路由,与 URL 状态管理(上)
- 七、Hook,路由,与 URL 状态管理(中)
- 七、Hook,路由,与 URL 状态管理(下)
八、用户选择器与项目编辑功能
- 八、用户选择器与项目编辑功能(上)
- 八、用户选择器与项目编辑功能(下)
九、深入React 状态管理与Redux机制
- 九、深入React 状态管理与Redux机制(一)
- 九、深入React 状态管理与Redux机制(二)
- 九、深入React 状态管理与Redux机制(三)
- 九、深入React 状态管理与Redux机制(四)
- 九、深入React 状态管理与Redux机制(五)
十、用 react-query 获取数据,管理缓存
- 十、用 react-query 获取数据,管理缓存(上)
- 十、用 react-query 获取数据,管理缓存(下)
十一、看板页面及任务组页面开发
1~3
- 十一、看板页面及任务组页面开发(一)
4~6
- 十一、看板页面及任务组页面开发(二)
7.编辑任务功能
接下来新建编辑任务的组件:
先准备好调用编辑任务接口和获取任务详情的 Hook
,编辑 src\utils\task.ts
:
...
import { useAddConfig, useEditConfig } from "./use-optimistic-options";
export const useEditTask = (queryKey: QueryKey) => {
const client = useHttp();
return useMutation(
(params: Partial<Task>) =>
client(`tasks/${params.id}`, {
method: "PATCH",
data: params,
}),
useEditConfig(queryKey)
);
};
export const useTask = (id?: number) => {
const client = useHttp();
return useQuery<Task>(["task", id], () => client(`tasks/${id}`), {
enabled: Boolean(id),
});
};
编辑 src\screens\ViewBoard\utils.ts
(新增 useTasksModal
):
...
// import { useDebounce } from "utils";
import { useTask } from "utils/task";
...
export const useTasksSearchParams = () => {
const [param] = useUrlQueryParam([
"name",
"typeId",
"processorId",
"tagId",
]);
const projectId = useProjectIdInUrl();
// const debouncedName = useDebounce(param.name)
return useMemo(
() => ({
projectId,
typeId: Number(param.typeId) || undefined,
processorId: Number(param.processorId) || undefined,
tagId: Number(param.tagId) || undefined,
// name: debouncedName,
name: param.name,
}),
// [projectId, param, debouncedName]
[projectId, param]
);
};
...
export const useTasksModal = () => {
const [{ editingTaskId }, setEditingTaskId] = useUrlQueryParam(['editingTaskId'])
const { data: editingTask, isLoading } = useTask(Number(editingTaskId))
const startEdit = useCallback((id: number) => {
setEditingTaskId({editingTaskId: id})
}, [setEditingTaskId])
const close = useCallback(() => {
setEditingTaskId({editingTaskId: ''})
}, [setEditingTaskId])
return {
editingTaskId,
editingTask,
startEdit,
close,
isLoading
}
}
视频中使用
useDebounce
使得完全停止输入后才开始搜索,避免输入过程中频繁搜索造成系统资源浪费,且影响用户体验,博主这样更改后中文输入法无法正常使用。。。后续再解决
新建组件:src\screens\ViewBoard\components\taskModal.tsx
:
import { useForm } from "antd/lib/form/Form"
import { useTasksModal, useTasksQueryKey } from "../utils"
import { useEditTask } from "utils/task"
import { useEffect } from "react"
import { Form, Input, Modal } from "antd"
import { UserSelect } from "components/user-select"
import { TaskTypeSelect } from "components/task-type-select"
const layout = {
labelCol: {span: 8},
wrapperCol: {span: 16}
}
export const TaskModal = () => {
const [form] = useForm()
const { editingTaskId, editingTask, close } = useTasksModal()
const { mutateAsync: editTask, isLoading: editLoading } = useEditTask(useTasksQueryKey())
const onCancel = () => {
close()
form.resetFields()
}
const onOk = async () => {
await editTask({...editingTask, ...form.getFieldsValue()})
close()
}
useEffect(() => {
form.setFieldsValue(editingTask)
}, [form, editingTask])
return <Modal
forceRender={true}
onCancel={onCancel}
onOk={onOk}
okText={"确认"}
cancelText={"取消"}
confirmLoading={editLoading}
title={"编辑任务"}
open={!!editingTaskId}
>
<Form {...layout} initialValues={editingTask} form={form}>
<Form.Item
label={"任务名"}
name={"name"}
rules={[{ required: true, message: "请输入任务名" }]}
>
<Input />
</Form.Item>
<Form.Item label={"经办人"} name={"processorId"}>
<UserSelect defaultOptionName={"经办人"} />
</Form.Item>
<Form.Item label={"类型"} name={"typeId"}>
<TaskTypeSelect />
</Form.Item>
</Form>
</Modal>
}
注意:与
Drawer
一样,在Modal
组件中使用通过useForm()
提取的form
绑定的Form
时,需要添加forceRender
属性,否则在页面打开时绑定不到会有报错,参见:【实战】React 实战项目常见报错 —— Instance created by ‘useForm’ is not connected to any Form element. Forget…
编辑:src\screens\ViewBoard\index.tsx
(引入 TaskModal
):
...
import { TaskModal } from "./components/taskModal";
export const ViewBoard = () => {
...
return (
<ViewContainer>
...
<TaskModal/>
</ViewContainer>
);
};
...
编辑:src\screens\ViewBoard\components\ViewboardCloumn.tsx
(引入 useTasksModal
使得点击 任务卡片 可以打开 TaskModal
进行编辑):
...
import { useTasksModal, useTasksSearchParams } from "../utils";
...
export const ViewboardColumn = ({ viewboard }: { viewboard: Viewboard }) => {
...
const { startEdit } = useTasksModal()
return (
<Container>
...
<TasksContainer>
{tasks?.map((task) => (
<Card onClick={() => startEdit(task.id)} style={{ marginBottom: "0.5rem", cursor: 'pointer' }} key={task.id}>
...
</Card>
))}
...
</TasksContainer>
</Container>
);
};
...
查看功能和效果,点击 任务卡片 后 TaskModal
出现,编辑并确认后即可看到修改后的任务(用了乐观更新,完全无感):
8.看板和任务删除功能
接下来先实现一个小功能,搜索结果中关键字高亮
新建 src\screens\ViewBoard\components\mark.tsx
:
export const Mark = ({name, keyword}: {name: string, keyword: string}) => {
if(!keyword) {
return <>{name}</>
}
const arr = name.split(keyword)
return <>
{
arr.map((str, index) => <span key={index}>
{str}
{
index === arr.length -1 ? null : <span style={{ color: '#257AFD' }}>{keyword}</span>
}
</span>)
}
</>
}
编辑 src\screens\ViewBoard\components\ViewboardCloumn.tsx
(引入 Task
并将 TaskCard
单独提取出来):
...
import { Task } from "types/Task";
import { Mark } from "./mark";
...
const TaskCard = ({task}: {task: Task}) => {
const { startEdit } = useTasksModal();
const { name: keyword } = useTasksSearchParams()
return <Card
onClick={() => startEdit(task.id)}
style={{ marginBottom: "0.5rem", cursor: "pointer" }}
key={task.id}
>
<p>
<Mark keyword={keyword} name={task.name}/>
</p>
<TaskTypeIcon id={task.id} />
</Card>
}
export const ViewboardColumn = ({ viewboard }: { viewboard: Viewboard }) => {
const { data: allTasks } = useTasks(useTasksSearchParams());
const tasks = allTasks?.filter((task) => task.kanbanId === viewboard.id);
return (
<Container>
<h3>{viewboard.name}</h3>
<TasksContainer>
{tasks?.map((task) => <TaskCard task={task}/>)}
<CreateTask kanbanId={viewboard.id} />
</TasksContainer>
</Container>
);
};
...
查看效果:
下面开始开发删除功能
编辑 src\utils\viewboard.ts
(创建并导出 useDeleteViewBoard
):
...
export const useDeleteViewBoard = (queryKey: QueryKey) => {
const client = useHttp();
return useMutation(
(id?: number) =>
client(`kanbans/${id}`, {
method: "DELETE",
}),
useDeleteConfig(queryKey)
);
};
编辑 src\screens\ViewBoard\components\ViewboardCloumn.tsx
:
...
import { Button, Card, Dropdown, MenuProps, Modal, Row } from "antd";
import { useDeleteViewBoard } from "utils/viewboard";
...
export const ViewboardColumn = ({ viewboard }: { viewboard: Viewboard }) => {
const { data: allTasks } = useTasks(useTasksSearchParams());
const tasks = allTasks?.filter((task) => task.kanbanId === viewboard.id);
return (
<Container>
<Row>
<h3>{viewboard.name}</h3>
<More viewboard={viewboard}/>
</Row>
<TasksContainer>
{tasks?.map((task) => <TaskCard task={task}/>)}
<CreateTask kanbanId={viewboard.id} />
</TasksContainer>
</Container>
);
};
const More = ({ viewboard }: { viewboard: Viewboard }) => {
const {mutateAsync: deleteViewBoard} = useDeleteViewBoard(useViewBoardQueryKey())
const startDelete = () => {
Modal.confirm({
okText: '确定',
cancelText: '取消',
title: '确定删除看板吗?',
onOk() {
deleteViewBoard(viewboard.id)
}
})
}
const items: MenuProps["items"] = [
{
key: 1,
label: "删除",
onClick: startDelete,
},
];
return <Dropdown menu={{ items }}>
<Button type="link" onClick={(e) => e.preventDefault()}>
...
</Button>
</Dropdown>
}
...
测试一下删除看板,功能正常
下面是删除任务功能
编辑 src\utils\task.ts
(创建并导出 useDeleteTask
):
...
export const useDeleteTask = (queryKey: QueryKey) => {
const client = useHttp();
return useMutation(
(id?: number) =>
client(`tasks/${id}`, {
method: "DELETE",
}),
useDeleteConfig(queryKey)
);
};
编辑 src\screens\ViewBoard\components\taskModal.tsx
:
...
import { useDeleteTask, useEditTask } from "utils/task";
export const TaskModal = () => {
...
const { mutateAsync: deleteTask } = useDeleteTask(useTasksQueryKey());
...
const startDelete = () => {
close();
Modal.confirm({
okText: '确定',
cancelText: '取消',
title: '确定删除看板吗?',
onOk() {
deleteTask(Number(editingTaskId));
}
})
}
return (
<Modal {...}>
<Form {...}>
...
</Form>
<div style={{ textAlign: 'right' }}>
<Button style={{fontSize: '14px'}} size="small" onClick={startDelete}>删除</Button>
</div>
</Modal>
);
};
测试一下删除任务,功能正常
部分引用笔记还在草稿阶段,敬请期待。。。