【实战】 六、用户体验优化 - 加载中和错误状态处理(上) —— 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 添加样式


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

1.给页面添加 Loading 和 Error 状态,增加页面友好性

修改 src\screens\ProjectList\index.tsx(新增 loading 状态 和 请求错误提示)(部分未修改内容省略):

...
import {
    
     Typography } from "antd";

export const ProjectList = () => {
    
    
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<null | Error>(null);

  ...

  useEffect(() => {
    
    
    setIsLoading(true)
    // React Hook "useHttp" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.
    client("projects", {
    
     data: cleanObject(lastParam) }).then(setList)
      .catch(error => {
    
    
        setList([])
        setError(error)
      })
      .finally(() => setIsLoading(false));
    // React Hook useEffect has a missing dependency: 'client'. Either include it or remove the dependency array.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [lastParam]);

  ...

  return (
    <Container>
      <h1>项目列表</h1>
      <SearchPanel users={
    
    users} param={
    
    param} setParam={
    
    setParam} />
      {
    
    error ? <Typography.Text type="danger">{
    
    error.message}</Typography.Text> : null}
      <List loading={
    
    isLoading} users={
    
    users} dataSource={
    
    list} />
    </Container>
  );
};

...

修改 src\screens\ProjectList\components\List.tsxListProps 继承 TableProps, Table 的属性(透传))(部分未修改内容省略):

import {
    
     Table, TableProps } from "antd";
...

interface ListProps extends TableProps<Project> {
    
    
  users: User[];
}

// type PropsType = Omit<ListProps, 'users'>
export const List = ({
     
      users, ...props }: ListProps) => {
    
    
  return (
    <Table
      pagination={
    
    false}
      columns={
    
    ...}
      {
    
     ...props }
    ></Table>
  );
};

为方便后续在组件外再次配置 Table 的属性(透传),直接让 ListProps 继承 TableProps, TableProps 单独抽出到 props

配置请求最短时间(如下图),即可清楚看到 loading 效果
在这里插入图片描述
配置请求失败比例为百分百即可看到错误提示:
在这里插入图片描述

2.用高级 Hook-useAsync 统一处理 Loading 和 Error 状态

新建 src\utils\use-async.ts (统一对 异步状态请求数据 的管理):

import {
    
     useState } from "react";

interface State<D> {
    
    
  error: Error | null;
  data: D | null;
  stat: 'ready' | 'loading' | 'error' | 'success'
}

const defaultInitialState: State<null> = {
    
    
  stat: 'ready',
  data: null,
  error: null
}

export const useAsync = <D>(initialState?: State<D>) => {
    
    
  const [state, setState] = useState<State<D>>({
    
    
    ...defaultInitialState,
    ...initialState
  })

  const setData = (data: D) => setState({
    
    
    data,
    stat: 'success',
    error: null
  })

  const setError = (error: Error) => setState({
    
    
    error,
    stat: 'error',
    data: null
  })

  // run 来触发异步请求
  const run = (promise: Promise<D>) => {
    
    
    if(!promise || !promise.then) {
    
    
      throw new Error('请传入 Promise 类型数据')
    }
    setState({
    
    ...state, stat: 'loading'})
    return promise.then(data => {
    
    
      setData(data)
      return data
    }).catch(error => {
    
    
      setError(error)
      return error
    })
  }

  return {
    
    
    isReady: state.stat === 'ready',
    isLoading: state.stat === 'loading',
    isError: state.stat === 'error',
    isSuccess: state.stat === 'success',
    run,
    setData,
    setError,
    ...state
  }
}

修改 src\screens\ProjectList\components\List.tsx (将 Project 导出,以便后续引用)(部分未修改内容省略):

...
export interface Project {
    
    ...}
...

修改 src\screens\ProjectList\index.tsx (部分未修改内容省略):

  • 删去之前 loadingerror 相关内容;
  • 删去 client 异步请求 then 及后续操作;
  • 使用 useAsync 统一处理 异步状态请求数据
...
import {
    
     List, Project } from "./components/List";
...
import {
    
     useAsync } from "utils/use-async";

export const ProjectList = () => {
    
    
  const [users, setUsers] = useState([]);
  const [param, setParam] = useState({
    
    
    name: "",
    personId: "",
  });

  // 对 param 进行防抖处理
  const lastParam = useDebounce(param);
  const client = useHttp();
  const {
    
     run, isLoading, error, data: list } = useAsync<Project[]>();

  useEffect(() => {
    
    
    run(client("projects", {
    
     data: cleanObject(lastParam) }))
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [lastParam]);

  useMount(() => client("users").then(setUsers));

  return (
    <Container>
      ...
      <List loading={
    
    isLoading} users={
    
    users} dataSource={
    
    list || []} />
    </Container>
  );
};
...

新建 src\utils\project.ts (单独处理 Project 数据的异步请求):

import {
    
     cleanObject } from "utils";
import {
    
     useHttp } from "./http";
import {
    
     useAsync } from "./use-async";
import {
    
     useEffect } from "react";
import {
    
     Project } from "screens/ProjectList/components/List";

export const useProjects = (param?: Partial<Project>) => {
    
    
  const client = useHttp();
  const {
    
     run, ...result } = useAsync<Project[]>();

  useEffect(() => {
    
    
    run(client("projects", {
    
     data: cleanObject(param || {
    
    }) }))
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [param]);

  return result
}

新建 src\utils\use-users.ts (单独处理 User 数据的异步请求):

import {
    
     cleanObject } from "utils";
import {
    
     useHttp } from "./http";
import {
    
     useAsync } from "./use-async";
import {
    
     useEffect } from "react";
import {
    
     User } from "screens/ProjectList/components/SearchPanel";

export const useUsers = (param?: Partial<User>) => {
    
    
  const client = useHttp();
  const {
    
     run, ...result } = useAsync<User[]>();

  useEffect(() => {
    
    
    run(client("users", {
    
     data: cleanObject(param || {
    
    }) }))
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [param]);

  return result
}

再次修改 src\screens\ProjectList\index.tsx (部分未修改内容省略):

  • ProjectUser 数据获取分别单独抽离
import {
    
     SearchPanel } from "./components/SearchPanel";
import {
    
     List } from "./components/List";
import {
    
     useState } from "react";
import {
    
     useDebounce } from "utils";
import styled from "@emotion/styled";
import {
    
     Typography } from "antd";
import {
    
     useProjects } from "utils/project";
import {
    
     useUsers } from "utils/use-users";

export const ProjectList = () => {
    
    
  const [param, setParam] = useState({
    
    
    name: "",
    personId: "",
  });

  // 对 param 进行防抖处理后接入请求
  const {
    
     isLoading, error, data: list } = useProjects(useDebounce(param));
  const {
    
     data: users } = useUsers();

  return (
    <Container>
      <h1>项目列表</h1>
      <SearchPanel users={
    
    users || []} param={
    
    param} setParam={
    
    setParam} />
      {
    
    error ? (
        <Typography.Text type="danger">{
    
    error.message}</Typography.Text>
      ) : null}
      <List loading={
    
    isLoading} users={
    
    users || []} dataSource={
    
    list || []} />
    </Container>
  );
};
...

测试功能:一切正常!


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

猜你喜欢

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