[Actual Combat] React and Hook Application: Implementation Project List - React17+React Hook+TS4 best practice, imitating Jira enterprise-level project (2)


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

1. Create a new file

  • New file: src\screens\ProjectList\index.jsx
import {
    
     SearchPanel } from "./components/SearchPanel"
import {
    
     List } from "./components/List"

export const ProjectListScreen = () => {
    
    
  return <div>
    <SearchPanel/>
    <List/>
  </div>
}
  • New file: src\screens\ProjectList\components\List.jsx
export const List = () => {
    
    
  return <table></table>
}
  • New file: src\screens\ProjectList\components\SearchPanel.jsx
import {
    
     useEffect, useState } from "react"

export const SearchPanel = () => {
    
    
  const [param, setParam] = useState({
    
    
    name: '',
    personId: ''
  })
  const [users, setUsers] = useState([])
  const [list, setList] = useState([])

  useEffect(() => {
    
    
    fetch('').then(async res => {
    
    
      if (res.ok) {
    
    
        setList(await res.json())
      }
    })
  }, [param])

  return <form>
    <div>
      {
    
    /* setParam(Object.assign({}, param, { name: evt.target.value })) */}
      <input type="text" value={
    
    param.name} onChange={
    
    evt => setParam({
    
    
        ...param,
        name: evt.target.value
      })}/>
      <select value={
    
    param.personId} onChange={
    
    evt => setParam({
    
    
        ...param,
        personId: evt.target.value
      })}>
        <option value="">负责人</option>
        {
    
    
          users.map(user => (<option key={
    
    user.id} value={
    
    user.id}>{
    
    user.name}</option>))
        }
      </select>
    </div>
  </form>
}
  • Compared with the original video, here is the name used for components: Upper Camel Case
  • Compared with the original video, the directory structure and variable names can be used according to your own habits!
  • The coding process is very important, but the text is not easy to reflect. . .
  • Vscode will not automatically complete HTML tags in JS files, please refer to: [Tips] Vscode completes HTML tags in JS files

2. Status improvement

Since listand paraminvolve two different components, these two need to be statepromoted to their common parent component, and the child component propsis used by deconstruction:

  • listListconsumed by
  • listparamAcquired according to ;
  • paramSearchPanelconsumed by

According to the database paradigm thinking, project, usersare each a separate table, and listare only intermediate products of associated queries. hardIn the mode, projectonly the usersprimary key of canpersonId be obtained through .personIdpersonNameusers


In DRYprinciple , http://host:portextract the in the interface call URL to the project global environment variable:

  • .env
REACT_APP_API_URL=http://online.com
  • .env.development
REACT_APP_API_URL=http://localhost:3001

webpackUnderstanding of environment variable identification rules:

  • npm startWhen executing , webpackread the environment variables .env.developmentin ;
  • npm run buildWhen executing , webpackread the environment variables .envin ;

3. New utils

Commonly used tools and methods are put utils/index.jsin

  • Since only one of multiple passable parameters is passed in the process of fetching parameters, empty parameters need to be filtered (during the filtering process, it is considered that 0is valid parameter, so it is treated specially):
export const isFalsy = val => val === 0 ? false : !val

// 在函数里,不可用直接赋值的方式改变传入的引用类型变量
export const cleanObject = obj => {
    
    
  const res = {
    
     ...obj }
  Object.keys(res).forEach(key => {
    
    
    const val = res[key]
    if (isFalsy(val)) {
    
    
      delete res[key]
    }
  })
  return res
}
  • When spelling parameters after the url, it will be cumbersome if there are many parameters, so introduceqs
npm i qs

After the previous two steps, the state is improved and the parameters are processed with cleanObjectand , the source code is as follows:qs

  • src\screens\ProjectList\index.jsx
import {
    
     SearchPanel } from "./components/SearchPanel";
import {
    
     List } from "./components/List";
import {
    
     useEffect, useState } from "react";
import {
    
     cleanObject } from "utils";
import * as qs from 'qs'

const apiUrl = process.env.REACT_APP_API_URL;

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

  useEffect(() => {
    
    
    fetch(
      // name=${param.name}&personId=${param.personId}
      `${
      
      apiUrl}/projects?${
      
      qs.stringify(cleanObject(param))}`
    ).then(async (res) => {
    
    
      if (res.ok) {
    
    
        setList(await res.json());
      }
    });
  }, [param]);

  useEffect(() => {
    
    
    fetch(`${
      
      apiUrl}/users`).then(async (res) => {
    
    
      if (res.ok) {
    
    
        setUsers(await res.json());
      }
    });
  }, []);

  return (
    <div>
      <SearchPanel users={
    
    users} param={
    
    param} setParam={
    
    setParam} />
      <List users={
    
    users} list={
    
    list} />
    </div>
  );
};
  • src\screens\ProjectList\components\List.jsx
export const List = ({
     
      users, list }) => {
    
    
  return (
    <table>
      <thead>
        <tr>
          <th>名称</th>
          <th>负责人</th>
        </tr>
      </thead>
      <tbody>
        {
    
    list.map((project) => (
          <tr key={
    
    project.id}>
            <td>{
    
    project.name}</td>
            {
    
    /* undefined.name */}
            <td>
              {
    
    users.find((user) => user.id === project.personId)?.name ||
                "未知"}
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};
  • src\screens\ProjectList\components\SearchPanel.jsx
export const SearchPanel = ({
     
      users, param, setParam }) => {
    
    
  return (
    <form>
      <div>
        {
    
    /* setParam(Object.assign({}, param, { name: evt.target.value })) */}
        <input
          type="text"
          value={
    
    param.name}
          onChange={
    
    (evt) =>
            setParam({
    
    
              ...param,
              name: evt.target.value,
            })
          }
        />
        <select
          value={
    
    param.personId}
          onChange={
    
    (evt) =>
            setParam({
    
    
              ...param,
              personId: evt.target.value,
            })
          }
        >
          <option value="">负责人</option>
          {
    
    users.map((user) => (
            <option key={
    
    user.id} value={
    
    user.id}>
              {
    
    user.name}
            </option>
          ))}
        </select>
      </div>
    </form>
  );
};
  • src\App.tsx
import "./App.css";
import {
    
     ProjectListScreen } from "screens/ProjectList";

function App() {
    
    
  return (
    <div className="App">
      <ProjectListScreen />
    </div>
  );
}

export default App;

Current effect: can be filtered by project name and person name (full match)
renderings

4.Custom Hook

Custom Hook is a tool for code reuse

  • useMount: life cycle simulation - componentDidMount
export const useMount = cbk => useEffect(() => cbk(), [])

Under normal circumstances, useEffect is only executed once, but useEffect is executed twice by default in react@v18 strict mode. For details, see: [Solved] react@v18 useEffect is executed twice by default in strict mode

  • useDebounce: debounce
/**
 * @param { 值 } val 
 * @param { 延时:默认 1000 } delay 
 * @returns 在某段时间内多次变动后最终拿到的值(delay 延迟的是存储在队列中的上一次变化)
 */
export const useDebounce = (val, delay = 1000) => {
    
    
  const [tempVal, setTempVal] = useState(val)

  useEffect(() => {
    
    
    // 每次在 val 变化后,设置一个定时器
    const timeout = setTimeout(() => setTempVal(val), delay)
    // 每次在上一个 useEffect 处理完以后再运行(useEffect 的天然功能即是在运行结束的 return 函数中清除上一个(同一) useEffect)
    return () => clearTimeout(timeout)
  }, [val, delay])

  return tempVal
}

// 日常案例,对比理解

// const debounce = (func, delay) => {
    
    
//   let timeout;
//   return () => {
    
    
//     if (timeout) {
    
    
//       clearTimeout(timeout);
//     }
//     timeout = setTimeout(function () {
    
    
//       func()
//     }, delay)
//   }
// }

// const log = debounce(() => console.log('call'), 5000)
// log()
// log()
// log()
//   ...5s
// 执行!

// debounce 原理讲解:
// 0s ---------> 1s ---------> 2s --------> ...
//     这三个函数是同步操作,它们一定是在 0~1s 这个时间段内瞬间完成的;
//     log()#1 // timeout#1
//     log()#2 // 发现 timeout#1!取消之,然后设置timeout#2
//     log()#3 // 发现 timeout#2! 取消之,然后设置timeout#3
//             // 所以,log()#3 结束后,就只有最后一个 —— timeout#3 保留

Extended learning: [Notes] In-depth understanding and js handwriting of anti-shake functions in different scenarios

  • used Custom Hookafter src\screens\ProjectList\index.js( lastParamdefined immediately paramafter )
  ...
  // 对 param 进行防抖处理
  const lastParam = useDebounce(param)
  const [list, setList] = useState([]);

  useEffect(() => {
    
    
    fetch(
      // name=${param.name}&personId=${param.personId}
      `${
      
      apiUrl}/projects?${
      
      qs.stringify(cleanObject(lastParam))}`
    ).then(async (res) => {
    
    
      if (res.ok) {
    
    
        setList(await res.json());
      }
    });
  }, [lastParam]);

  useMount(() => {
    
    
    fetch(`${
      
      apiUrl}/users`).then(async (res) => {
    
    
      if (res.ok) {
    
    
        setUsers(await res.json());
      }
    });
  });
  ...

This way, retyping 1swithin will not trigger a requestprojects forfetch

Extended learning:


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

Guess you like

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