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


学习内容来源: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 存放常用类型:

export type SN = string | number

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

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):

...
export interface Project {
    
    
  id: number;
  ...
  personId: number;
  ...
}
...

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

...
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

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 中调用它:

...
import {
    
     useProjectsSearchParams } from "./utils";

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

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

接下来重头戏来了

新建 src\components\user-select.tsx

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 组件:

...
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

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,即显示默认选项负责人):

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

查看页面效果,完美!

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

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

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

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

...
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 组件):

...
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 中有网络请求,刷新页面再看,数据已经更新了,这个问题后续解决


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

猜你喜欢

转载自blog.csdn.net/qq_32682301/article/details/131885909