【实战】 八、用户选择器与项目编辑功能(上) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(十四)

文章目录

    • 一、项目起航:项目初始化与配置
    • [二、React 与 Hook 应用:实现项目列表](#二、React 与 Hook 应用:实现项目列表)
    • [三、TS 应用:JS神助攻 - 强类型](#三、TS 应用:JS神助攻 - 强类型)
    • 四、JWT、用户认证与异步请求
    • [五、CSS 其实很简单 - 用 CSS-in-JS 添加样式](#五、CSS 其实很简单 - 用 CSS-in-JS 添加样式)
    • [六、用户体验优化 - 加载中和错误状态处理](#六、用户体验优化 - 加载中和错误状态处理)
    • [七、Hook,路由,与 URL 状态管理](#七、Hook,路由,与 URL 状态管理)
    • 八、用户选择器与项目编辑功能
      • [1.实现id-select.tsx解决id类型 难题](#1.实现id-select.tsx解决id类型 难题)
      • 2.抽象user-select组件选择用户
      • [3.自定义 Star 组件做项目收藏标记](#3.自定义 Star 组件做项目收藏标记)

学习内容来源: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 应用:实现项目列表

三、TS 应用:JS神助攻 - 强类型

四、JWT、用户认证与异步请求


五、CSS 其实很简单 - 用 CSS-in-JS 添加样式


六、用户体验优化 - 加载中和错误状态处理



七、Hook,路由,与 URL 状态管理



八、用户选择器与项目编辑功能

1.实现id-select.tsx解决id类型 难题

上一节最后的 bug 可以通过自定义组件来优化掉

新建 src\types\index.ts 存放常用类型:

js 复制代码
export type SN = string | number

新建组件 src\components\id-select.tsx

js 复制代码
import { Select } from "antd"
import { SN } from "types"

type SelectProps = React.ComponentProps<typeof Select>

// 类型不是简单的后来者居上,而是寻求"最大公约数"的方式
interface IdSelectProps extends Omit<SelectProps, 'value' | 'onChange' | 'options'>{
  value: SN | null | undefined,
  onChange: (value?: number) => void,
  defaultOptionName?: string,
  options?: {name: string, id: number}[]
}

/**
 * value 可以传入多种类型的值
 * onChange 只会回调 number | undefined 类型
 * 当isNaN(Number(value)) 为 true 的时候,代表选择默认类型
 * 当选择默认类型时,onChange 会回调 undefined
 * @param props 
 */
export const IdSelect = (props: IdSelectProps) => {
  const { value, onChange, defaultOptionName, options, ...restProps } = props
  return <Select
    value={toNumber(value)}
    onChange={value => onChange(toNumber(value) || undefined)}
    { ...restProps }
  >
    {
      defaultOptionName ? <Select.Option value={0}>{defaultOptionName}</Select.Option> : null
    }
    {
      options?.map(option => <Select.Option key={option.id} value={option.id}>{option.name}</Select.Option>)
    }
  </Select>
}

const toNumber = (value: unknown) => isNaN(Number(value)) ? 0 : Number(value)

2.抽象user-select组件选择用户

修改 src\screens\ProjectList\components\List.tsx(将 Project 中的 idpersonId 类型统一改为 number):

js 复制代码
...
export interface Project {
  id: number;
  ...
  personId: number;
  ...
}
...

修改 src\screens\ProjectList\components\SearchPanel.tsx(将 User 中的 id 改为 number 类型,使用Utility Types处理 Project 类型 生成 param 的可选子类型):

js 复制代码
...
import { Project } from "./List";

export interface User {
  id: number;
  ...
}
interface SearchPanelProps {
  ...
  param: Partial<Pick<Project, 'name' | 'personId'>>
  ...
}
...
  • Partial:将每个子类型转换为可选类型
  • Pick:经过 泛型约束 生成一个新类型

由于从 URL 中得到的数据都是 string 类型,因此需要特殊处理,接下来将这部分单独抽离出来

新建 src\screens\ProjectList\utils.ts

js 复制代码
import { useUrlQueryParam } from "utils/url";

export const useProjectsSearchParams = () => {
  const [param, setParam] = useUrlQueryParam(["name", "personId"]);
  return [
    {...param, personId: Number(param.personId) || undefined},
    setParam
  ] as const
}

src\screens\ProjectList\index.tsx 中调用它:

js 复制代码
...
import { useProjectsSearchParams } from "./utils";

export const ProjectList = () => {
  useDocumentTitle('项目列表')

  const [param, setParam] = useProjectsSearchParams()
  ...
};
...

接下来重头戏来了

新建 src\components\user-select.tsx

js 复制代码
import { useUsers } from "utils/use-users";
import { IdSelect } from "./id-select";

export const UserSelect = (props: React.ComponentProps<typeof IdSelect>) => {
  const {data: users} = useUsers()
  return <IdSelect options={users || []} {...props}/>
};

src\screens\ProjectList\components\SearchPanel.tsx 中调用 UserSelect 组件:

js 复制代码
...
import { UserSelect } from "components/user-select";

...
export const SearchPanel = ({ users, param, setParam }: SearchPanelProps) => {
  return (
    <Form {...}>
      ...
      <Form.Item>
        <UserSelect
          defaultOptionName="负责人"
          value={param.personId}
          onChange={(value) => setParam({ ...param, personId: value, })}
        />
      </Form.Item>
    </Form>
  );
};

查看页面效果,又发生了熟悉的事情。。。无限循环

打开 wdyr 的开关,查找原因,发现之前的 useUrlQueryParam 中的 param 使用 useMemo 后不再创建新对象,但是经过 useProjectsSearchParams 处理,每次返回的又是新对象,那还是老办法,用 useMemo 解决

修改 src\screens\ProjectList\utils.ts

js 复制代码
import { useMemo } from "react";
...
// 项目列表搜索的参数
export const useProjectsSearchParams = () => {
  ...
  return [
    useMemo(() =>({...param, personId: Number(param.personId) || undefined}), [param]),
    setParam
  ] as const
}

查看页面,问题解决

还有个特别小的问题,一般情况下容易忽略:

  • 当切换到某个具体负责人时,刷新页面(带参链接首次加载)时,userSelect 组件在 users 数据请求回来之前由于找不到匹配项,会短暂显示 personId

接下来解决一下

修改 src\components\id-select.tsx(请求到 users 数据之前值为 0,即显示默认选项负责人):

js 复制代码
...
export const IdSelect = (props: IdSelectProps) => {
  ...
  return (
    <Select
      value={options?.length ? toNumber(value) : 0}
      {...}
    >...</Select>
  );
};
...

查看页面效果,完美!

3.自定义 Star 组件做项目收藏标记

为每个项目新增一个收藏标记

新建组件 Star src\components\star.tsx

js 复制代码
import { Rate } from "antd";

interface StarProps extends React.ComponentProps<typeof Rate> {
  checked: boolean,
  onCheckedChange?: (checked: boolean) => void
}

export const Star = ({checked, onCheckedChange, ...restProps}: StarProps) => {
  return <Rate
    count={1}
    value={checked ? 1 : 0}
    onChange={num => onCheckedChange?.(!!num)}
    {...restProps}
  />
}

新增 编辑和新增 的 Custom Hook src\utils\project.ts

js 复制代码
...
export const useEditProject = () => {
  const client = useHttp();
  const { run, ...asyncResult } = useAsync<Project[]>();
  const mutate = (params: Partial<Project>) => {
    return run(client(`projects/${params.id}`, {
      data: params,
      method: 'PATCH'
    }))
  }

  return {
    mutate,
    ...asyncResult
  };
};

export const useAddProject = () => {
  const client = useHttp();
  const { run, ...asyncResult } = useAsync<Project[]>();
  const mutate = (params: Partial<Project>) => {
    return run(client(`projects/${params.id}`, {
      data: params,
      method: 'POST'
    }))
  }

  return {
    mutate,
    ...asyncResult
  };
};

这部分在构思时需要考虑到,Hook 只能在 函数组件内的最外层使用,不能在外面再嵌套其他非组件的普通函数,因此需要提前暴露出一个函数来接收参数并处理相关逻辑(闭包的应用),否则会出现下面的报错:

  • React Hook "useEditProject" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.

编辑 src\screens\ProjectList\components\List.tsx(使用 Star 组件):

js 复制代码
...
import { Star } from "components/star";
import { useEditProject } from "utils/project";
...
// type PropsType = Omit<ListProps, 'users'>
export const List = ({ users, ...props }: ListProps) => {
  const { mutate } = useEditProject()
  // 函数式编程 柯里化
  const starProject = (id: number) => (star: boolean) => mutate({id, star})
  return (
    <Table
      pagination={false}
      columns={[
        {
          title: <Star checked={true} disabled={true}/>,
          render: (val, record) =>
            <Star
              checked={record.star}
              // stared => starProject(record.id)(stared)
              onCheckedChange={starProject(record.id)}
            />
        },
        ...
      ]}
      {...props}
    ></Table>
  );
};
  • 在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
  • 柯里化(currying)又称部分求值。一个柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

查看页面,点击标记一个,但是没有反应,控制台 Network 中有网络请求,刷新页面再看,数据已经更新了,这个问题后续解决


部分引用笔记还在草稿阶段,敬请期待。。。

相关推荐
JiangJiang24 分钟前
🚀 Vue人看React useRef:它不只是替代 ref
javascript·react.js·面试
1024小神28 分钟前
在GitHub action中使用添加项目中配置文件的值为环境变量
前端·javascript
齐尹秦37 分钟前
CSS 列表样式学习笔记
前端
Mnxj41 分钟前
渐变边框设计
前端
用户76787977373243 分钟前
由Umi升级到Next方案
前端·next.js
快乐的小前端1 小时前
TypeScript基础一
前端
北凉温华1 小时前
UniApp项目中的多服务环境配置与跨域代理实现
前端
源柒1 小时前
Vue3与Vite构建高性能记账应用 - LedgerX架构解析
前端
Danny_FD1 小时前
常用 Git 命令详解
前端·github
stanny1 小时前
MCP(上)——function call 是什么
前端·mcp