加载中...
  • react+ts仿jira笔记 loading

    base

    json-server

    1
    2
    3
    4
    5
    6
    7
    npm i json-server
    json-server -w db.json

    `${api}/projects?name=${param.name}&personId=${param.personId}`
    如果name为空, 应该把name属性去掉
    可以使用 qs =>
    ${api}/projects?${qs.stringify(cleanObject(param))}

    env

    1
    2
    3
    react项目引用.env文件的内容
    变量名需要以REACT_APP_开头
    process.env.REACT_APP_XX

    Custom Hook

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import { useEffect } from 'react'

    export const useMount = (callback) => {
    useEffect(() => {
    callback()
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [])
    }

    import { useEffect, useState } from 'react'

    export const useDebounce = (value, delay) => {
    const [debouncedValue, setDebouncedValue] = useState(value)
    useEffect(() => {
    const timeout = setTimeout(() => setDebouncedValue(value), delay)
    // 在下一次执行 useEffect 前被调用
    return () => clearTimeout(timeout)
    }, [value, delay])
    return debouncedValue
    }

    集成 ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    const onChange = (evt: React.ChangeEvent<HTMLSelectElement>) => {
    setParam({ ...param, personId: evt.target.value })
    }

    export const cleanObject = (obj: Record<string, unknown>): Record<string, unknown> => {
    const result = { ...obj }
    Object.entries(result).forEach(([key, value]) => {
    if (!value && value !== 0) {
    delete result[key]
    }
    })
    return result
    }

    export const useDebounce = <T>(value: T, delay: number = 200): T => {
    const [debouncedValue, setDebouncedValue] = useState(value)
    useEffect(() => {
    const timeout = setTimeout(() => setDebouncedValue(value), delay)
    return () => clearTimeout(timeout)
    }, [value, delay])
    return debouncedValue
    }

    useArray

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    export const useArray = <T>(initialArray: T[]) => {
    const [value, setValue] = useState(initialArray)

    return {
    value,
    setValue,
    add: (item: T) => setValue([...value, item]),
    clear: () => setValue([]),
    removeIndex: (index: number) => {
    const copy = [...value]
    copy.splice(index, 1)
    setValue(copy)
    }
    }
    }

    Login 组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    import axios from 'axios'

    const api = process.env.REACT_APP_API_URL

    const Login = () => {
    const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault()
    const username = (event.currentTarget.elements[0] as HTMLInputElement).value
    const password = (event.currentTarget.elements[1] as HTMLInputElement).value
    axios.post(`${api}/login`, { username, password }).then((resp) => {
    console.log('resp: ', resp)
    })
    }

    return (
    <form onSubmit={onSubmit}>
    <div>
    <label htmlFor='username'>用户名</label>
    <input type='text' id='username' />
    </div>
    <div>
    <label htmlFor='password'>密码</label>
    <input type='password' id='password' />
    </div>
    <button type='submit'>登录</button>
    </form>
    )
    }

    export default Login

    json-server 中间件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    "json": "json-server --watch __json-server-mock__/db.json --middlewares __json-server-mock__/middlewares.js --port 3001"

    middlewares.js
    module.exports = (req, res, next) => {
    if (req.method === 'POST' && req.path === '/login') {
    if (req.body.username === 'admin' && req.body.password === 'admin') {
    res.status(200).json({
    user: {
    token: '123'
    }
    })
    } else {
    res.status(400).json({
    message: 'Bad Request'
    })
    }
    next()
    }
    }

    auth-provider

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // 在真实环境中, 如果使用firebase这种第三方auth服务的话, 这里就不需要自己写了

    import axios from 'axios'

    const localStorageKey = '__auth_provider_token__'

    const apiUrl = process.env.REACT_APP_API_URL

    export const getToken = () => window.localStorage.getItem(localStorageKey)

    export const handleUserResponse = ({ user }: { user: User }) => {
    window.localStorage.setItem(localStorageKey, user.token || '')
    return user
    }

    export const login = (data: { username: string; password: string }) => {
    axios.post(`${apiUrl}/login`, data).then((res) => {
    if (res.status === 200) {
    handleUserResponse(res.data)
    }
    })
    }
    export const register = (data: { username: string; password: string }) => {
    axios.post(`${apiUrl}/register`, data).then((res) => {
    if (res.status === 200) {
    handleUserResponse(res.data)
    }
    })
    }

    export const logout = async () => window.localStorage.removeItem(localStorageKey)

    auth-context

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67

    import { createContext, ReactNode, useContext, useState } from 'react'
    import { User } from 'types/User'
    import * as auth from '../auth-provider'

    interface AuthForm {
    username: string
    password: string
    }

    export const AuthContext = createContext<
    | {
    user: User | null
    login: (form: AuthForm) => Promise<void>
    register: (form: AuthForm) => Promise<void>
    logout: () => Promise<void>
    }
    | undefined
    >(undefined)

    export const AuthProvider = ({ children }: { children: ReactNode }) => {
    const [user, setUser] = useState<User | null>(null)

    const login = async (form: AuthForm) => {
    return auth.login(form).then((user) => {
    setUser(user)
    })
    }

    const register = async (form: AuthForm) => {
    return auth.register(form).then((user) => {
    setUser(user)
    })
    }

    const logout = async () => {
    return auth.logout().then(() => {
    setUser(null)
    })
    }

    const value = {
    user,
    login,
    register,
    logout,
    }

    return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
    }

    export const useAuth = () => {
    const context = useContext(AuthContext)
    if (!context) {
    throw new Error('useAuth必须在AuthProvider中使用')
    }
    return context
    }

    index.tsx
    import { ReactNode } from 'react'
    import { AuthProvider } from './auth-context'

    export const AppProviders = ({ children }: { children: ReactNode }) => {
    return <AuthProvider>{children}</AuthProvider>
    }

    抽象 fetch

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    服务端返回401, 500, fetch不会抛出异常, 只有在网络连接失败抛出异常, 所以需要判断Prmise.reject()情况
    axios可以捕获401, 500

    import { logout } from 'auth-provider'
    import qs from 'qs'

    const apiUrl = process.env.REACT_APP_API_URL
    interface Config extends RequestInit {
    data: object
    token?: string
    }

    export const http = async (endpoint: string, { data, token, headers, ...customConfig }: Config) => {
    const config = {
    method: 'GET',
    headers: {
    Authorization: token ? `Bearer ${token}` : '',
    'Content-Type': data ? 'application/json' : '',
    },
    ...customConfig,
    }

    if (config.method.toUpperCase() === 'GET') {
    endpoint += `?${qs.stringify(data)}`
    } else {
    config.body = JSON.stringify(data || {})
    }

    return window.fetch(`${apiUrl}/${endpoint}`, config).then(async (response) => {
    if (response.status === 401) {
    await logout()
    window.location.reload()
    return Promise.reject({ message: '请重新登录' })
    }
    const data = await response.json()
    if (response.ok) {
    return data
    } else {
    return Promise.reject(data)
    }
    })
    }

    export const useHttp = () => {
    const { user } = useAuth()
    return (...[endpoint, config]: Parameters<typeof http>) => http(endpoint, { ...config, token: user?.token })
    }

    css 样式 css in js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    import { useAuth } from 'context/auth-context'
    import { Button } from 'antd'
    import { Screens } from 'components/screens'
    import styled from '@emotion/styled'

    const Container = styled.div`
    background: red;
    `

    const ButtnStyle = styled(Button)`
    background: darkblue;

    &:hover {
    background: pink;
    }
    `

    export const Authed = () => {
    const { logout } = useAuth()

    return (
    <Container>
    <ButtnStyle onClick={logout} type={'dashed'}>
    退出登录
    </ButtnStyle>

    <Screens />
    </Container>
    )
    }

    App.css

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    html {
    font-size: 62.5%
    }

    html,
    body,
    #root,
    .App {
    min-height: 100vh;
    }

    自定义 css 组件, 传参

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <HeaderLeft gap={2}>
    <h2>Logo</h2>
    <h2>项目</h2>
    <h2>用户</h2>
    </HeaderLeft>

    import styled from '@emotion/styled'

    type RowProps = {
    gap?: number | string
    between?: boolean
    marginBottom?: number
    }

    export const Row = styled.div<RowProps>`
    display: flex;
    align-items: center;
    ${({ between }) => between && 'justify-content: space-between;'};
    margin-bottom: ${({ marginBottom }) => (marginBottom ? marginBottom + 'rem' : 0)};

    > * {
    margin-top: 0 !important;
    margin-bottom: 0 !important;
    margin-right: ${({ gap }) => (typeof gap === 'number' ? gap + 'rem' : gap)};
    }
    `

    const HeaderLeft = styled(Row)``

    pargma 注释

    1
    /** @jsx jsx */ 是 JSX 的 pragma 注释。这是告诉 React 在转换 JSX 代码时要使用哪个函数来代替 React.createElement。在这种情况下,使用的是 jsx 函数,它是 @emotion/core 库中的一个函数,它允许我们在 JSX 中使用 Emotion CSS-in-JS 语法。通过在文件顶部添加此注释,可以确保编译器正确地使用指定的函数。

    react-query

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    QueryClientProvider 是 react-query 库中的一个组件,它可以将 QueryClient 实例传递给 React 组件树,并允许这些组件在树中的任何地方使用 useQuery 和 useMutation 钩子。

    具体而言,QueryClientProvider 组件接受一个 QueryClient 实例作为 client 属性,并将其嵌套在 React 组件树中。这使得 QueryClient 实例可以在树中的任何地方使用。通常,我们将 QueryClientProvider 放在应用的最高层,以便整个应用都可以访问 QueryClient 实例。

    使用 QueryClientProvider,我们可以在 React 组件中使用 useQuery 和 useMutation 钩子,这些钩子使用了 QueryClient 实例来执行数据查询和操作。这些钩子使得数据查询和操作的状态管理变得简单而可预测,同时提供了许多高级功能,如缓存和自动重试等。

    import { QueryClientProvider, QueryClient } from 'react-query'

    export const AppProviders = ({ children }: { children: ReactNode }) => {
    return (
    <QueryClientProvider client={new QueryClient()}>
    <AuthProvider>{children}</AuthProvider>
    </QueryClientProvider>
    )
    }

    Loading 和 Error

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    import { useState, useEffect } from 'react'
    import { List } from './list'
    import { SearchPanel } from './search-panel'
    import { cleanObject, useDebounce, useMount } from 'utils'
    import { useHttp } from 'utils/http'
    import styled from '@emotion/styled'
    import { Typography } from 'antd'

    export const ProjectListScreen = () => {
    const [param, setParam] = useState({
    name: '',
    personId: '',
    })
    const [list, setList] = useState([])
    const [users, setUsers] = useState([])
    const [isLoading, setIsLoading] = useState(true)
    const [error, setError] = useState<null | Error>(null)
    const client = useHttp()
    const debouncedParam = useDebounce(param, 200)
    useEffect(() => {
    client('projects', { data: cleanObject(debouncedParam) })
    .then(setList)
    .catch(setError)
    .finally(() => setIsLoading(false))
    // eslint-disable-next-line
    }, [debouncedParam])
    useMount(() => {
    client('users').then(setUsers)
    })
    return (
    <Container>
    <h1>项目列表</h1>
    <SearchPanel users={users} param={param} setParam={setParam} />
    {error ? <Typography.Text type="danger">{error.message}</Typography.Text> : null}
    <List loading={isLoading} dataSource={list} users={users} />
    </Container>
    )
    }

    const Container = styled.div`
    padding: 3.2rem;
    `

    用 useAsync 统一处理 Loading 和 Error

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    import { useState } from 'react'

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

    const defaultInitialState: State<null> = {
    stat: 'idle',
    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({
    data: null,
    stat: 'error',
    error,
    })
    }

    const run = async (promise: Promise<D>) => {
    if (!promise || !promise.then) {
    throw new Error('请传入 Promise 类型数据')
    }
    setState({ ...state, stat: 'loading' })
    return promise.then(setData).catch(setError)
    }

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

    Error boundaries 捕获边界错误

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    自react16起, 任何未被错误边界捕获的错误将会导致整个React组件树被卸载

    import React from 'react'

    // children
    // fallbackRender

    type FallbackRender = (props: { error: Error | null }) => React.ReactElement

    export class ErrorBoundary extends React.Component<
    // 可以将children省略
    React.PropsWithChildren<{ fallbackRender: FallbackRender }>,
    { error: Error | null }
    > {
    state = { error: null }
    // 会在组件渲染过程中,子组件抛出异常后被调用
    static getDerivedStateFromError(error: Error) {
    return { error }
    }
    render() {
    const { error } = this.state
    const { fallbackRender, children } = this.props
    if (error) {
    return fallbackRender({ error })
    }
    return children
    }
    }

    修改浏览器标题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    react-helmet

    export const useDocumentTitle = (title: string, keepOnUnmount = true) => {
    // const oldTitle = document.title
    // 页面加载时, oldTitle === 旧title
    // 加载后, oldTitle === 新titel

    const oldTitle = useRef(document.title).current
    useEffect(() => {
    document.title = title
    }, [title])
    useEffect(() => {
    return () => {
    if (!keepOnUnmount) {
    document.title = oldTitle
    }
    }
    }, [keepOnUnmount, oldTitle])
    }

    react-hook 与 闭包

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    function test() {
    let num = 0
    const effect = () => {
    num += 1
    const message = `num is ${num}`
    return function unmount() {
    console.log(message)
    }
    }
    }

    const add = test()
    message1
    const unmount = add()
    message2
    add()
    message3
    add()
    message4
    add()
    message1
    unmount() // 1

    1. 添加依赖项
    2. 使用useRef
    useRef返回一个可变的ref对象, 其.current属性被初始化为传入的参数. 返回ref对象在组件的整个生命周期内保持不变, 值发生改变, 不会重新渲染组件
    useRef 创建的是一个普通 Javascript 对象,而且会在每次渲染时返回同一个 ref 对象,当我们变化它的 current 属性的时候,对象的引用都是同一个,所以定时器中能够读到最新的值。

    路由

    主页路由配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <Main>
    <Routes>
    <Route path={'/projects'} element={<ProjectListScreen />} />
    <Route path={'/projects/:projectId/*'} element={<ProjectScreen />} />
    <Route path={'*'} element={<Navigate to={'/projects'} replace={true} />} />
    </Routes>
    </Main>

    replace属性, 介入浏览器浏览历史
    export const resetRoute = () => (window.location.href = window.location.origin)

    实现 useUrlQueryParam 管理 URL 状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    export const useUrlQueryParams = <K extends string>(keys: K[]) => {
    const [searchParams, setSearchParams] = useSearchParams()
    return [
    useMemo(() => {
    return keys.reduce((prev, key) => {
    return { ...prev, [key]: searchParams.get(key) || '' }
    }, {} as { [key in K]: string })
    // eslint-disable-next-line
    }, [searchParams]),
    (params: Partial<{ [key in K]: unknown }>) => {
    const o = cleanObject({ ...Object.fromEntries(searchParams), ...params }) as URLSearchParamsInit
    return setSearchParams(o)
    },
    ] as const
    }

    解决无限渲染的问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    使用why-did-you-render 库

    wdyr.ts
    import React from 'react'

    const whyDidYouRender = require('@welldone-software/why-did-you-render')

    whyDidYouRender(React, {
    trackALlPureComponents: false,
    })


    ProjectListScreen.whyDidYouRender = true


    使用useMemo解决

    const [obj, setObj] = useState(object)
    当object是对象的state时, 放入依赖中, 不会无限循环

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    import { Select } from 'antd'
    import { Raw } from 'types'

    type SelectProps = React.ComponentProps<typeof Select>

    interface IdSelectProps extends Omit<SelectProps, 'value' | 'onChange' | 'options'> {
    value: Raw | null | undefined
    onChange: (value?: number) => void
    defaultOptionName?: string
    options?: { name: string; id: number }[]
    }

    /**
    * value 可以传入多种类型的值
    * onChange只会回调 number | undefined 类型
    * 当 isNaN(Number(value)) 为 true 时,代表选择默认类型
    * 当选择默认类型时,onChange会回调 undefined
    * 当选择非默认类型时,onChange会回调选中类型的 id
    */

    export const IdSelect = (props: IdSelectProps) => {
    const { value, onChange, defaultOptionName, options, ...restProps } = props
    return (
    <Select
    value={options?.length ? toNumber(value) : 0}
    onChange={(value) => onChange(toNumber(value) || undefined)}
    {...restProps}>
    {defaultOptionName ? <Select.Option value={0}>{defaultOptionName}</Select.Option> : null}
    {options?.map((option) => (
    <Select.Option value={option.id} key={option.id}>
    {option.name}
    </Select.Option>
    ))}
    </Select>
    )
    }

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

    编辑后刷新, useState 的懒初始化与保存函数状态

    1
    2
    3
    4
    5
    6
    7
    除了传入一个初始状态值外,useState 还支持传入一个函数。这个函数会在组件的初始渲染中执行,并返回一个初始状态值。这种方式可以用来延迟计算初始状态值,通过使用函数作为参数,可以延迟计算初始状态值,从而避免性能问题。需要注意的是,使用函数作为参数只会在组件的初始渲染中执行一次。后续调用 useState 的时候,不会再次执行这个函数。

    useState保存函数的方法
    柯里化
    const [state, setState] = useState(() => () => {})
    使用useRef

    乐观更新

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    乐观更新(Optimistic Update)是指在向后端提交数据请求时,预先在前端更新 UI,然后等待后端返回结果再根据结果更新 UI。

    乐观更新的好处是能够提高用户体验,因为用户不需要等待后端响应就可以看到数据更新的效果。同时,也可以减轻服务器压力,因为可以将一部分计算和数据更新的工作交给客户端完成。

    不过,乐观更新也存在一些风险,因为有可能后端返回的结果与预期不符,例如由于网络故障或者服务器异常导致提交失败。这种情况下,前端的 UI 就会出现不一致的状态,从而影响用户体验。为了解决这个问题,可以采用一些额外的措施,例如:

    添加回滚机制:在出现提交失败的情况下,需要撤销之前更新的 UI 状态,并显示一个错误提示,让用户知道提交失败了;
    添加重试机制:如果提交失败,可以给用户提供一个重新提交的机会,让用户可以再次尝试提交;
    添加补偿机制:如果提交失败,可以采用其他方式(例如长轮询、WebSocket 等)继续尝试提交,直到提交成功为止。
    总之,乐观更新是一种可以提高用户体验和服务器性能的技术,但需要在实现中注意一些风险和难点。

    useCallback

    1
    2
    如果useCallback依赖了useState的数据, 造成无限渲染
    可以将setState改成函数的形式setState((preState) => {})

    接口数据请求成功时, 原页面已被销毁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    // 返回组件的挂载状态
    export const useMountedRef = () => {
    const mountedRef = useRef(false)
    useEffect(() => {
    mountedRef.current = true
    return () => {
    mountedRef.current = false
    }
    })
    }

    const mountedRef = useMountedRef()
    const run = async (promise: Promise<D>, runConfig?: { retry: () => Promise<D> }) => {
    if (!promise || !promise.then) {
    throw new Error('请传入 Promise 类型数据')
    }
    setRetry(() => () => {
    if (runConfig?.retry) {
    run(runConfig?.retry(), runConfig)
    }
    })
    setState({ ...state, stat: 'loading' })
    return promise
    .then((data) => {
    // 阻值在已卸载组件赋值
    if (mountedRef.current) {
    setData(data)
    return data
    }
    })
    .catch((err) => {
    setError(err)
    return err
    })
    }

    组件组合

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <PageHeader
    projectButton={
    <ButtonNoPadding onClick={() => setProjectModalOpen(true)} type="link">
    创建项目
    </ButtonNoPadding>
    }
    />

    component composition 组件组合
    传入JSX.Element 使业务逻辑解耦, 减少了props数量, 无需context
    将逻辑提升到组件树的更高层次来处理, 会使得这些组件变得更复杂

    useUndo

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    import { useCallback, useState } from 'react'

    export const useUndo = <T>(initialPresent: T) => {
    // 合并state
    const [state, setState] = useState<{
    past: T[]
    present: T
    future: T[]
    }>({
    past: [],
    present: initialPresent,
    future: [],
    })
    const canUndo = state.past.length !== 0
    const canRedo = state.future.length !== 0

    const undo = useCallback(() => {
    setState((currentState) => {
    const { past, present, future } = currentState
    if (past.length === 0) return currentState
    const previous = past[past.length - 1]
    const newPast = past.slice(0, past.length - 1)

    return {
    past: newPast,
    present: previous,
    future: [present, ...future],
    }
    })
    }, [])

    const redo = useCallback(() => {
    setState((currentState) => {
    const { past, present, future } = currentState
    if (future.length === 0) return currentState
    const next = future[0]
    const newFuture = future.slice(1)
    return {
    past: [...past, present],
    present: next,
    future: newFuture,
    }
    })
    }, [])

    const set = useCallback((newPresent: T) => {
    setState((currentState) => {
    const { present } = currentState
    if (newPresent === present) return currentState
    return {
    past: [...currentState.past, present],
    present: newPresent,
    future: [],
    }
    })
    }, [])

    const reset = useCallback((newPresent: T) => {
    setState({
    past: [],
    present: newPresent,
    future: [],
    })
    }, [])

    return [state, { set, reset, undo, redo, canUndo, canRedo }] as const
    }

    useReducer

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    import { useCallback, useReducer } from 'react'

    const UNDO = 'UNDO'
    const REDO = 'REDO'
    const SET = 'SET'
    const RESET = 'RESET'

    type State<T> = {
    past: T[]
    present: T
    future: T[]
    }

    type Action<T> = {
    newPresent?: T
    type: typeof UNDO | typeof REDO | typeof SET | typeof RESET
    }

    const undoReducer = <T>(state: State<T>, action: Action<T>) => {
    const { past, present, future } = state
    const { type, newPresent } = action

    switch (type) {
    case UNDO: {
    if (past.length === 0) return state
    const previous = past[past.length - 1]
    const newPast = past.slice(0, past.length - 1)
    return {
    past: newPast,
    present: previous,
    future: [present, ...future],
    }
    }
    case REDO: {
    if (future.length === 0) return state
    const next = future[0]
    const newFuture = future.slice(1)
    return {
    past: [...past, present],
    present: next,
    future: newFuture,
    }
    }
    case SET: {
    if (newPresent === present) return state
    return {
    past: [...past, present],
    present: newPresent,
    future: [],
    }
    }
    case RESET: {
    return {
    past: [],
    present: newPresent,
    future: [],
    }
    }
    default: {
    return state
    }
    }
    }

    export const useUndo = <T>(initialPresent: T) => {
    const [state, dispatch] = useReducer(undoReducer, {
    past: [],
    present: initialPresent,
    future: [],
    } as State<T>)

    const canUndo = state.past.length !== 0
    const canRedo = state.future.length !== 0

    const undo = useCallback(() => dispatch({ type: UNDO }), [])

    const redo = useCallback(() => dispatch({ type: REDO }), [])

    const set = useCallback((newPresent: T) => dispatch({ newPresent, type: SET }), [])

    const reset = useCallback((newPresent: T) => dispatch({ newPresent, type: RESET }), [])

    return [state, { set, reset, undo, redo, canUndo, canRedo }] as const
    }

    const useSafeDispatch = <T>(dispatch: (...args: T[]) => void) => {
    const mountedRef = useMountedRef()
    return (...args: T[]) => (mountedRef.current ? dispatch(...args) : void 0)
    }

    redux

    redux-thunk

    1
    2
    3
    异步函数被认为是副作用, 不可预测

    当我们返回的是函数时,store会帮我们调用这个返回的函数,并且把dispatch暴露出来供我们使用。对于redux-thunk的整个流程来说,它是等异步任务执行完成之后,我们再去调用dispatch,然后去store去调用reduces。

    redux-toolkit

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    pnpm i react-redux @reduxjs/toolkit

    import { configureStore } from '@reduxjs/toolkit'
    import { projectListSlice } from 'components/project-list/project-list.slice'

    export const rootReducer = {
    kanbanList: projectListSlice.reducer,
    }

    export const store = configureStore({
    reducer: rootReducer,
    })

    export type AppDispatch = typeof store.dispatch
    export type RootState = ReturnType<typeof store.getState>
    export const selectProjectModalOpen = (state: RootState) => state.kanbanList.projectModalOpen
    // 使用
    const projectModalOpen = useSelector(selectProjectModalOpen)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    project-list.slice.ts

    import { createSlice } from '@reduxjs/toolkit'
    interface State {
    projectModalOpen: boolean
    }
    const initialState: State = {
    projectModalOpen: false,
    }
    export const projectListSlice = createSlice({
    name: 'projectList',
    initialState,
    reducers: {
    openProjectModal: (state, action) => {
    // toolkit 内置了immer
    state.projectModalOpen = true
    },
    closeProjectModal: (state, action) => {
    state.projectModalOpen = false
    },
    },
    })
    export const projectListActions = projectListSlice.actions

    redux-thunk 管理登录状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    import { User } from 'types/User'
    import { createSlice } from '@reduxjs/toolkit'
    import * as auth from 'auth-provider'
    import { AppDispatch, RootState } from 'store'
    import { AuthForm, bootstrapUser } from 'context/auth-context'

    interface State {
    user: User | null
    }

    const initialState: State = {
    user: null
    }

    export const authSlice = createSlice({
    name: 'auth',
    initialState,
    reducers: {
    setUser(state, action) {
    state.user = action.payload
    }
    }
    })

    export const { setUser } = authSlice.actions

    export const login = (form: AuthForm) => (dispatch: AppDispatch) => {
    return auth.login(form).then((user) => {
    dispatch(setUser(user))
    return user
    })
    }

    export const register = (form: AuthForm) => (dispatch: AppDispatch) => {
    return auth.register(form).then((user) => {
    dispatch(setUser(user))
    return user
    })
    }

    export const logout = () => (dispatch: AppDispatch) => {
    return auth.logout().then(() => {
    dispatch(setUser(null))
    })
    }

    export const bootstrap = () => (dispatch: AppDispatch) => {
    return bootstrapUser().then((user) => {
    dispatch(setUser(user))
    return user
    })
    }

    export const selectUser = (state: RootState) => state.auth.user

    export default authSlice.reducer

    用 url 参数管理项目模态框状态

    1
    2
    3
    4
    5
    6
    7
    export const useProjectModal = () => {
    const [{ projectCreate }, setProjectCreate] = useUrlQueryParams(['projectCreate'])
    const open = () => setProjectCreate({ projectCreate: true })
    const close = () => setProjectCreate({ projectCreate: false })
    return [projectCreate === 'true', open, close] as const
    }

    用 react-query 来处理服务端缓存

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    类似的库 swr

    export const AppProviders = ({ children }: { children: ReactNode }) => {
    const queryClient = new QueryClient()
    return (
    <QueryClientProvider client={queryClient}>
    <AuthProvider>{children}</AuthProvider>
    </QueryClientProvider>
    )
    }

    const { status, data, error } = useQuery(‘user’, async () => {fetch(...)})
    useQuery([‘user’, param], ...) // 当值变化时, 会重新触发

    queryClient.getQueryData(’user’)
    queryClient.invalidateQueries(‘user’)


    eg:
    export const useProjects = () => {
    const [param] = useUrlQueryParams(['name', 'personId'])
    const client = useHttp()
    const debouncedParam = useDebounce(param, 200)
    return useQuery<Project[], Error>(['project', param], () => client('projects', { data: cleanObject(debouncedParam) }))
    }

    export const useEditProject = () => {
    const client = useHttp()
    const queryClient = useQueryClient()
    return useMutation(
    (params: Partial<Project>) => {
    return client(`projects/${params.id}`, { data: params, method: 'PATCH' })
    },
    {
    onSuccess: () => {
    queryClient.invalidateQueries('projects')
    }
    }
    )
    }

    export const useProject = (id?: number) => {
    const client = useHttp()
    return useQuery<Project, Error>(['project', { id }], () => client(`projects/${id}`), {
    // enabled: 是否发送请求
    enabled: Boolean(id)
    })
    }

    project-modal.tsx

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    import { Button, Drawer, Form, Input, Spin } from 'antd'
    import { useProjectModal } from './util'
    import { IdSelect } from '../id-select/index'
    import { useAddProject, useEditProject } from 'utils/project'
    import { useForm } from 'antd/es/form/Form'
    import { useEffect } from 'react'

    export const ProjectModal = () => {
    const { projectModalOpen, close, editingProject, isLoading } = useProjectModal()
    const useMutateProject = editingProject ? useEditProject : useAddProject
    const { mutateAsync, isLoading: mutateLoading } = useMutateProject()
    const [form] = useForm()
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const onFinish = (values: any) => {
    mutateAsync({ ...editingProject, ...values }).then(() => {
    form.resetFields()
    close()
    })
    }

    const title = editingProject ? '编辑项目' : '创建项目'

    useEffect(() => {
    form.setFieldsValue(editingProject)
    }, [editingProject, form])

    return (
    <Drawer forceRender onClose={close} open={projectModalOpen} width="100%">
    {isLoading ? (
    <Spin size="large" />
    ) : (
    <>
    <h1>{title}</h1>
    <Form form={form} layout="vertical" style={{ width: '40rem' }} onFinish={onFinish}>
    <Form.Item label="名称" name="name" rules={[{ required: true, message: '请输入项目名!' }]}>
    <Input placeholder="请输入项目名" />
    </Form.Item>
    <Form.Item label="部门" name="organization" rules={[{ required: true, message: '请输入部门名!' }]}>
    <Input.TextArea rows={3} placeholder="请输入部门" />
    </Form.Item>
    <Form.Item label="负责人" name="personId">
    <IdSelect defaultOptionName="负责人" />
    </Form.Item>
    <Form.Item>
    <Button loading={mutateLoading} type="primary" htmlType="submit">
    提交
    </Button>
    </Form.Item>
    </Form>
    </>
    )}
    </Drawer>
    )
    }

    使用 react-query 实现乐观更新(optimistic updates)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    export const useEditProject = () => {
    const client = useHttp()
    const queryClient = useQueryClient()
    const [searchParams] = useProjectsSearchParams()
    const queryKey = ['projects', searchParams]

    return useMutation(
    (params: Partial<Project>) => {
    return client(`projects/${params.id}`, { data: params, method: 'PATCH' })
    },
    {
    onSuccess: () => {
    return queryClient.invalidateQueries('projects')
    },
    async onMutate(target) {
    const previousItem = queryClient.getQueryData<Project[]>(queryKey)
    // @ts-ignore
    queryClient.setQueryData(queryKey, (old?: Project[]) => {
    return old?.map((project) => (project.id === target.id ? { ...project, ...target } : project))
    })
    return { previousItem }
    },
    onError(error, newItem, context) {
    queryClient.setQueryData(queryKey, context?.previousItem)
    }
    }
    )
    }

    抽离为通用 hook

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    import { QueryKey, useQueryClient } from 'react-query'

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    export const useConfig = (queryKey: QueryKey, callback: (target: any, old: any[]) => any[]) => {
    const queryClient = useQueryClient()
    return {
    onSuccess: () => {
    return queryClient.invalidateQueries(queryKey)
    },
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    async onMutate(target: any) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const previousItems = queryClient.getQueryData<any[]>(queryKey)
    // @ts-ignore
    queryClient.setQueryData(queryKey, (old: any[]) => {
    return callback(target, old)
    // return old?.map((project) => (project.id === target.id ? { ...project, ...target } : project))
    })
    return { previousItems }
    },
    onError(error, newItem, context) {
    queryClient.setQueryData(queryKey, context?.previousItems)
    }
    }
    }

    export const useDeleteConfig = (queryKey: QueryKey) =>
    useConfig(queryKey, (target, old) => {
    return old?.filter((item) => item.id !== target.id)
    })

    export const useEditConfig = (queryKey: QueryKey) =>
    useConfig(queryKey, (target, old) => {
    return old?.map((item) => (item.id === target.id ? { ...item, ...target } : item))
    })

    export const useAddConfig = (queryKey: QueryKey) =>
    useConfig(queryKey, (target, old) => {
    return [...old, target]
    })

    跨组件状态管理总结

    1
    2
    3
    小场景: 状态提升/组合组件
    缓存状态: react-query/swr
    客户端状态: url/redux/context

    拖拽排序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    react-beautiful-dnd

    import React from 'react'
    import {
    Draggable,
    DraggableProps,
    Droppable,
    DroppableProps,
    DroppableProvided,
    DroppableProvidedProps
    } from 'react-beautiful-dnd'

    type DropProps = Omit<DroppableProps, 'children'> & {
    children: React.ReactNode
    }

    export const Drop = ({ children, ...props }: DropProps) => {
    return (
    <Droppable {...props}>
    {(provided) => {
    if (React.isValidElement(children)) {
    // 为children添加额外的属性
    // 等价于 <Children {...provided.droppableProps} ref={provided.innerRef} provided={provided} />
    const childProps = {
    ...provided.droppableProps,
    ref: provided.innerRef,
    provided
    }
    return React.cloneElement(children, childProps)
    }
    return <div />
    }}
    </Droppable>
    )
    }

    type DropChildProps = Partial<
    {
    provided: DroppableProvided
    } & DroppableProvidedProps
    > &
    React.HTMLAttributes<HTMLDivElement>

    // 用户自定义组件传入ref
    export const DropChild = React.forwardRef<HTMLDivElement, DropChildProps>(({ children, ...props }, ref) => {
    return (
    <div ref={ref} {...props}>
    {children}
    {props.provided?.placeholder}
    </div>
    )
    })
    DropChild.displayName = 'DropChild'

    type DragProps = Omit<DraggableProps, 'children'> & {
    children: React.ReactNode
    }
    export const Drag = ({ children, ...props }: DragProps) => {
    return (
    <Draggable {...props}>
    {(provided) => {
    if (React.isValidElement(children)) {
    const childProps = {
    ...provided.draggableProps,
    ...provided.dragHandleProps,
    ref: provided.innerRef
    }
    return React.cloneElement(children, childProps)
    }
    return <div />
    }}
    </Draggable>
    )
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    return (
    <DragDropContext
    onDragEnd={() => {
    return
    }}>
    <div>
    <h1>{currentProject?.name}看板</h1>
    <Drop type="COLUMN" direction="horizontal" droppableId="kanban">
    <ColumnsContainer>
    {kanbans?.map((kanban: Kanban, index: number) => (
    <Drag key={kanban.id} draggableId={'kanban' + kanban.id} index={index}>
    <KanbanColumn key={kanban.id} kanban={kanban}></KanbanColumn>
    </Drag>
    ))}
    </ColumnsContainer>
    </Drop>
    </div>
    </DragDropContext>
    )

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

    export const KanbanColumn = React.forwardRef<HTMLDivElement, { kanban: Kanban }>(
    ({ kanban, ...props }: { kanban: Kanban }, ref) => {
    const { data: tasks } = useTasksType()
    return (
    <Container ref={ref} {...props}>
    <h3>{kanban.name}</h3>
    <Drop type="ROW" direction="vertical" droppableId={'task' + kanban.id}>
    <DropChild>
    {tasks?.map((task: Task, index: number) => (
    <Drag key={task.id} draggableId={'task' + task.id} index={index}>
    <div>
    <Card style={{ marginBottom: '0.5rem' }} key={task.id}>
    {task.name}
    </Card>
    </div>
    </Drag>
    ))}
    </DropChild>
    </Drop>
    </Container>
    )
    }
    )
    KanbanColumn.displayName = 'KanbanColumn'

    React.forwardRef

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    在 React 中,ref 属性用于获取组件或 DOM 元素的引用。但是,在函数组件中无法直接使用 ref 属性,因为函数组件不是一个类,它没有实例对象,也没有实例方法。因此,如果我们需要在函数组件中使用 ref 属性,就需要使用 React.forwardRef 来创建一个可以接收 ref 属性的函数组件。

    使用 React.forwardRef 创建的函数组件接收两个参数:

    props:函数组件的属性对象。
    ref:一个回调函数,用于接收从父组件传递下来的 ref 属性。当子组件需要将自己的引用传递给父组件时,可以调用这个回调函数。

    const MyComponent = React.forwardRef((props, ref) => {
    return <input type="text" ref={ref} />;
    });

    function ParentComponent() {
    const inputRef = React.useRef(null);
    return <MyComponent ref={inputRef} />;
    }

    gh-pages

    1
    2
    3
    4
    5
    6
    {
    “scripts”: {
    “predeploy”: “npm run build”,
    ”deploy”: “gh-pages -d build -r xx -b main”
    }
    }
    下一篇:
    Vuejs设计与实现
    本文目录
    本文目录