[Practice] 6. User Experience Optimization - Loading and Error State Handling (Part 1) —— React17+React Hook+TS4 best practice, imitating Jira enterprise-level project (8)


Source of learning content: React + React Hook + TS Best Practice - MOOC


Compared with the original tutorial, I used the latest version at the beginning of my study (2023.03):

item Version
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

The specific configuration, operation and content will be different, and the "pit" will also be different. . .


1. Project launch: project initialization and configuration

2. React and Hook application: implement the project list

3. TS Application: JS God Assist - Strong Type

4. JWT, user authentication and asynchronous request


5. CSS is actually very simple - add styles with CSS-in-JS


6. User experience optimization - loading and error state handling

1. Add Loading and Error states to the page to increase page friendliness

Modification src\screens\ProjectList\index.tsx(add loading status and request error prompt) (some unmodified content omitted):

...
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>
  );
};

...

Modify src\screens\ProjectList\components\List.tsx( ListPropsinheritance TableProps, Tableattribute (transparent)) (some unmodified content is omitted):

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>
  );
};

In order to facilitate the subsequent reconfiguration of the properties (transparent transmission) outside the component Table, let directly ListPropsinherit TablePropsand TablePropsextract them separatelyprops

Configure the minimum request time (as shown in the figure below), and you can clearly see loadingthe effect
insert image description here
. Configure the failure ratio of the request to be 100%, and you can see the error message:
insert image description here

2. Use advanced Hook-useAsync to uniformly handle Loading and Error states

New src\utils\use-async.ts(unified management of asynchronous state and request data ):

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

Modification src\screens\ProjectList\components\List.tsx(will be Projectexported for subsequent reference) (some unmodified content omitted):

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

Modifications src\screens\ProjectList\index.tsx(some unmodified content omitted):

  • Delete before loadingand errorrelated content;
  • Delete clientasynchronous request thenand subsequent operations;
  • Use useAsyncUnified processing of asynchronous state and request data ;
...
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>
  );
};
...

New src\utils\project.ts(asynchronous request for Project data is processed separately):

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
}

New src\utils\use-users.ts(asynchronous request for User data is processed separately):

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
}

Revised again src\screens\ProjectList\index.tsx(some unmodified content omitted):

  • Projectand Userdata acquisition are separated separately
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>
  );
};
...

Test function: everything works fine!


Some reference notes are still in draft stage, so stay tuned. . .

Guess you like

Origin blog.csdn.net/qq_32682301/article/details/131529812