文章目录
- 一、项目起航:项目初始化与配置
- 二、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 <ModalforceRender={true}onCancel={onCancel}onOk={onOk}okText={"确认"}cancelText={"取消"}confirmLoading={editLoading}title={"编辑任务"}open={!!editingTaskId}><Form {...layout} initialValues={editingTask} form={form}><Form.Itemlabel={"任务名"}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 <CardonClick={() => 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>);
};
测试一下删除任务,功能正常
部分引用笔记还在草稿阶段,敬请期待。。。