React常用Hooks及使用示例大全

vite创建react项目

注意node.js版本,Vite 需要 Node.js 版本 20.19+, 22.12+。

npm create vite@latest

这里framework我们选择react,语言选择typescript

npm i安装依赖

npm run dev启动项目

配置路径别名

1.安装node的类型声明

npm i -D @types/node

2.配置vite.config.json

ts 复制代码
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { join } from 'path'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve:{
    alias:{
      '@':join(__dirname,'./src')
    }
  }
})

3.tsconfig.app.json和tsconfig.node.json 配置导入路径提示

ts 复制代码
"baseUrl": ".",
"paths": {
  "@/*": ["./src/*"]
}

去掉StrictMode

mian.ts文件中注释掉StrictMode,因为它在开发阶段会导致纯函数式组件执行两次。

tsx 复制代码
// <StrictMode>
//   <App />
// </StrictMode>,

<App />

React.FC 函数式组件类型

jsx 复制代码
// 这里的React.FC表示函数式组件类型

import React from "react"

const App:React.FC = () => {
   return(
     <h1>这是App组件</h1>
   )
}

export default App

useState

让函数式组件拥有自己的状态

const [ 状态名,set函数 ] = useState(初始值)

1.基本语法案例

tsx 复制代码
import React, { useState } from "react";

export const Count: React.FC = () => {
    const [count, setCount] = useState(0);

    const add = () => {
       setCount(count + 1)
    }

    return (
        <>
            <h1>count的值是:{count}</h1>
            <button onClick={add}>加1</button>
        </>
    )
}

2.注意点

  • 状态变化,必然会导致函数的重新执行(组件重新渲染)。
  • 当函数式组件重新执行时,不会重复调用useState给函数赋予初始值,而是复用上次state值。

以函数的形式为状态赋予初始值

ts 复制代码
const [value,setValue] = useState(()=>初始值)

代码案例:

ts 复制代码
import React, { useState } from "react";
export const DateCom: React.FC = () => {
    const [date] = useState(()=>{
        const dt = new Date()
        return {year:dt.getFullYear(),month:dt.getMonth() + 1,day:dt.getDate()}
    })
    return (
        <>
            <h1>当前的年月日信息</h1>
            <p>年份:{date.year}</p>
            <p>月份:{date.month}</p>
            <p>日期:{date.day}</p>
        </>
    )
}

useState是异步变更状态

useState函数内部是以异步的形式修改状态的,所以修改状态后,无法立即拿到最新的状态

代码示例

tsx 复制代码
export const Counter2: React.FC = () => {

    const [count, setCount] = useState(0);

    const add = () => {
        setCount(count + 1)
        console.log(count)  //打印获取不到最新的count值
    }

    return (
        <>
            <h1>count的值是:{count}</h1>
            <button onClick={add}>加1</button>
        </>
    )
}

解决方法:

配合useEffect使用

tsx 复制代码
    // 依赖count值,回调函数会在count变化时执行
    // 回调函数在组件首次渲染的时候也会执行
    useEffect(()=>{
        console.log(`count最新的值是:${count}`)
    },[count])

解决更新不及时的问题

tsx 复制代码
export const Counter3: React.FC = () => {

    const [count, setCount] = useState(0);

    const add = () => {
        // setCount内部更新是异步的,但是这里连续两次调用的js是同步的
        setCount(count + 1)
        // 这里再次调用是,count的状态还有修改过来,还是0, setCount运行了两次,值还是1
        setCount(count + 1)
    }

    return (
        <>
            <h1>count的值是:{count}</h1>
            <button onClick={add}>加1</button>
        </>
    )
}

解决方法:

tsx 复制代码
// 解决方法
...
// prev是上一次的旧值  setCount((prev)=>{ return prev + 1 })
setCount(prev => prev + 1)
setCount(prev => prev + 1)
...

当我们修改state状态时,如果发现是新值依赖与旧值,需要通过fn形参拿到旧值,再进行计算,并返回

更新对象类型的值

如果变更的类型是对象,需要使用展开运算符或者Object.assign()生成一个新对象,用新对象才能触发渲染(旧对象即使值改变了,但是引用地址没变,react是检测不到的)

tsx 复制代码
export const UserInfo: React.FC = () => {
    const [user, setUser] = useState({
        name: '张三',
        age: 18,
        gender: '男'
    })

    const edit = () => {
       user.name = '李四'
       user.age = 20
      //  setUser(user)  //无效
      //  setUser({...user}) //有效
       setUser(Object.assign({},user)) //有效
    }

    return (
        <>
            <h1>用户信息:</h1>
            <p>姓名:{user.name}</p>
            <p>年龄:{user.age}</p>
            <p>性别:{user.gender}</p>
            <br />
            <button onClick={edit}>修改用户信息</button>
        </>
    )
}

使用setState模拟组件强制刷新

tsx 复制代码
export const FUpdate: React.FC = () => {
    const [, forceUpdate] = useState({})
    const update = () => {
        // 每次传入一个新对象,组件就会刷新
        forceUpdate({})
    }
    return (
        <>
            <h1>{Date.now()}</h1>
            <button onClick={update}>强制刷新</button>
        </>
    )
}

useRef

  • 获取DOM或组件的实例对象
  • 存储渲染周期之间的共享数据

获取DOM

tsx 复制代码
import React, { useRef } from "react";


export const InputFocus: React.FC = () => {

    const inputRef = useRef<HTMLInputElement>(null)

    const getFocus = () => {
        inputRef.current?.focus()
    }

    return (
        <>
            <input type="text" ref={inputRef} />
            <br />
            <button onClick={getFocus}>获取焦点</button>
        </>
    )
} 

存储渲染周期之间的共享数据

tsx 复制代码
export const Counter: React.FC = () => {

    const [count, setCount] = useState(0)

    // useRef仅在函数第一次调用时执行,重新调用渲染时组件时,不会再次执行
    const prevCount = useRef<number>(0)

    const add = () => {
        setCount((prev) => {
            prevCount.current = prev
            return prev + 1
        })
    }
    return (
        <div>
            <h1>count的新值是:{count},旧值是:{prevCount.current}</h1>
            <br />
            <button onClick={add}>+1</button>
        </div>
    )
}

注意事项

  • 1.组件rerender时useRef不会被重复初始化
  • 2.ref.current变化时不会造成组件的rerender

案例:

tsx 复制代码
export const RefTimer:React.FC = () => {
    const [count, setCount] = useState(0)

    const refTime = useRef<number>(Date.now())

    const changeCount = () => {
        // 变化能更新渲染
        setCount(prev => prev + 1)
    }

    const changeTime = () => {
        //变化并不能更新渲染
        refTime.current = Date.now()
        console.log(refTime.current)
    }


    // 1.useEffect 会在组件首次渲染完毕之后,默认执行一次
    // 2.组件每次渲染完毕之后,会触发useEffect的回调函数,如果给了依赖项数组,则还要判断依赖项是否变化,再决定能否触发函数
    useEffect(() => { 
        console.log("触发了useEffect的回调执行")
    }, [refTime.current])

    return (
        <>
            <h1>count的值是:{count}</h1>
            <h1>refTime当前的值是:{refTime.current}</h1>
            <button onClick={changeCount}>更改count</button>
            <button onClick={changeTime}>更改refTime</button>
        </>
    )
}

React.forwardRef useImperativeHandle

假如想实现一个获取函数组件实例的操作:

父组件获取子组件

tsx 复制代码
const Child: React.FC = () => {
    const [count, setCount] = useState(0)
    const add = (num:number)=>{
       setCount(prev=>prev += num)
    }
    return (
        <>
            <h2>子组件的count值是:{count}</h2>
            <button onClick={()=>add(1)}>+1</button>
            <button onClick={()=>add(-1)}>-1</button>
        </>
    )
}

export const Father: React.FC = () => {
    const childRef = useRef(null)
    return (
        <>
            <h1>这是父组件</h1>
            <hr />
            {/* 这里直接获取会报错 */}
            <Child ref={childRef} />
        </>
    )
}

要想获取函数式组件,我们需要额外以下几个步骤:

1.使用React.forwardRef包裹子组件

tsx 复制代码
const Child =  React.forwardRef(() => {
    const [count, setCount] = useState(0)
    const add = (num:number)=>{
       setCount(prev=>prev += num)
    }
    return (
        <>
            <h2>子组件的count值是:{count}</h2>
            <button onClick={()=>add(1)}>+1</button>
            <button onClick={()=>add(-1)}>-1</button>
        </>
    )
})

2.useImperativeHandle获取子组件信息

useImperativeHandle返回的对象 就是childRef.current拿到的值

tsx 复制代码
//useImperativeHandle返回的对象  就是childRef.current拿到的值
const Child =  React.forwardRef((_,ref) => {
    
    const [count, setCount] = useState(0)

    const add = (num:number)=>{
       setCount(prev=>prev += num)
    }

    // 这里我们返回一个对象
    useImperativeHandle(ref,()=>{
        return {
            name:'张三',
            age:18
        }
    })

    return (
        <>
            <h2>子组件的count值是:{count}</h2>
            <button onClick={()=>add(1)}>+1</button>
            <button onClick={()=>add(-1)}>-1</button>
        </>
    )
})


export const Father: React.FC = () => {
    const childRef = useRef(null)

    // 打印子组件信息
    const showMessage = ()=>{
       console.log(childRef.current)   // {name: '张三', age: 18}
    }
    return (
        <>
            <h1>这是父组件</h1>
            <button  onClick={showMessage}>获取子组件信息</button>
            <hr />
            <Child ref={childRef} />
        </>
    )
}

父组件重置子组件信息实战

tsx 复制代码
const Child =  React.forwardRef((_,ref) => {

    const [count, setCount] = useState(0)

    const add = (num:number)=>{
       setCount(prev=>prev += num)
    }

    useImperativeHandle(ref,()=>{
        // 暴露count,setCount
        return {
            count,
            setCount
        }
    })

    return (
        <>
            <h2>子组件的count值是:{count}</h2>
            <button onClick={()=>add(1)}>+1</button>
            <button onClick={()=>add(-1)}>-1</button>
        </>
    )
})


interface ChildRefMessage {
    count:number,
    setCount:(value:number)=>void
}

export const Father: React.FC = () => {
    const childRef = useRef<ChildRefMessage | null>(null)

    // 打印子组件信息
    const showMessage = ()=>{
       console.log(childRef.current)
    }
    // 重置子组件信息
    const resetChild = ()=>{
        childRef.current?.setCount(0)
    } 
    return (
        <>
            <h1>这是父组件</h1>
            <button  onClick={showMessage}>获取子组件信息</button>
            <button onClick={resetChild}>重置</button>
            <hr />
            <Child ref={childRef} />
        </>
    )
}

useImperativeHandle的第三个参数

useImperativeHandle(ref,createHandle,[deps])

  • 参数一:父组件传递的ref
  • 参数二:函数,返回想往外暴漏的属性
  • 参数三:可选,函数依赖的值,有依赖值,才会更新暴漏的属性,假如 [] ,则不会更新暴露值。 不传的话,就会一直更新。

以下代码,我们依赖值为[],改变了count值后,父组件获取的值还是初始值0

tsx 复制代码
const Child =  React.forwardRef((_,ref) => {

    const [count, setCount] = useState(0)

    const add = (num:number)=>{
       setCount(prev=>prev += num)
    }

    useImperativeHandle(ref,()=>{
        // 暴露count,setCount
        return {
            count,
            setCount
        }
    },[])

    return (
        <>
            <h2>子组件的count值是:{count}</h2>
            <button onClick={()=>add(1)}>+1</button>
            <button onClick={()=>add(-1)}>-1</button>
        </>
    )
})

添加依赖项,获取更新后的数据:

tsx 复制代码
// 此时父组件能够获取最新的count值 
useImperativeHandle(ref,()=>{
    // 暴露count,setCount
    return {
        count,
        setCount
    }
},[count])

useEffect

什么是函数的副作用

React函数的副作用就是 函数除了返回值外外界环境造成的其他影响,即与组件渲染无关的操作,例如:获取数据、修改全局变量、更新DOM等。

useEffect可以执行一写副作用,例如:请求数据、事件监听等。

useEffect(fn,[deps])

  • 参数一:fn是一个副作用函数,函数会在每次渲染完成之后被调用。

  • 参数二:可选,依赖项数组, 依赖项变化,重新执行fn副作用函数 依赖项没有变化时,则不会执行fn副作用函数

    复制代码
      省略情况下:函数会在每次渲染完成之后被调用

没有依赖项的情况下:初始化渲染时会执行useEffect打印useEffect执行了!,后面每次count变化,视图刷新都会执行useEffect打印useEffect执行了!

tsx 复制代码
export const Count: React.FC = () => {
    const [count, setCount] = useState(0)

    const add = () => {
        setCount(prev => prev += 1)
    }

    useEffect(()=>{
        console.log("useEffect执行了!")
    })

    return (
        <>
            <h1>count的值是:{count}</h1>
            <hr />
            <button onClick={add}>+1</button>
        </>
    )
}

有依赖的情况下:

以下代码,仅当count的状态改变时,才会打印useEffect执行了!,flag状态改变,虽然刷新视图,但是不会再次执行打印。

tsx 复制代码
export const Count: React.FC = () => {
    const [count, setCount] = useState(0)
    const [flag, setFlag] = useState(false)

    const add = () => {
        setCount(prev => prev += 1)
    }

    const toggle = () => {
        setFlag(prev => !prev)
    }

    useEffect(() => {
        console.log("useEffect执行了!")
    }, [count])

    return (
        <>
            <h1>count的值是:{count}</h1>
            <h1>flag的值时:{ String(flag)}</h1>
            <hr />
            <button onClick={add}>+1</button>
            <button onClick={toggle}>toggle</button>
        </>
    )
}

当依赖项是空数组的情况下,仅会在首次渲染完毕后默认执行一次。

注意事项

  • 1.不要在useEffect中改变依赖项的值,会造成死循环
  • 2.多个不同的副作用尽量分开声明,不要写在一个useEffect中

清理副作用

useEffect可以返回一个函数,用于清理副作用的回调

tsx 复制代码
useEffect(()=>{
  //1.执行副作用操作
  //2.返回一个清理副作用的函数  
  return ()=>{/**执行清理操作 */}
},[依赖项])

典型应用场景:

  • 1.组件卸载时,请求还未结束,清理请求。
  • 2.组件中使用了定时器或绑定了监听事件,清除定时器或绑定事件

清理函数的触发时机:

  • 1.组件被卸载的时候,会调用
  • 2.当effect副作用函数被执行之前,会先执行清理函数

案例,卸载组件时清除事件监听:

tsx 复制代码
const MouseMove: React.FC = () => {
    const [position, setPosition] = useState({ x: 0, y: 0 })

    const mouseMoveHandle = (e: MouseEvent) => {
        console.log(e.clientX, e.clientY)
        setPosition({ x: e.clientX, y: e.clientY })
    }

    useEffect(() => {
        window.addEventListener('mousemove', mouseMoveHandle)
        // 清除事件监听
        return ()=>{ window.removeEventListener('mousemove', mouseMoveHandle) }
    }, [])

    return (
        <>
            <h1>鼠标的位置在:{JSON.stringify(position)}</h1>
        </>
    )
}

export const TestMouseMove: React.FC = () => {
    const [flag, setFlag] = useState(true)
    const toggle = () => {
       setFlag(prev=>!prev)
    }
    return (
        <>
            <button onClick={toggle}>Toggle</button>
            <hr />
            { flag &&  <MouseMove />}
        </>
    )
}

自定义封装Hooks案例

新建src/hooks/index.ts

获取鼠标位置自定义Hook

tsx 复制代码
import {useState,useEffect} from 'react'
export const useMousePosition = (time: number = 0) => {
    const [position, setPosition] = useState({ x: 0, y: 0 })

    useEffect(() => {

        let timerId: null | NodeJS.Timeout = null

        const mouseMoveHandle = (e: MouseEvent) => {
            if (timerId) return
            timerId = setTimeout(() => {
                console.log(e.clientX, e.clientY)
                setPosition({ x: e.clientX, y: e.clientY })
                timerId = null
            }, time)
        }

        window.addEventListener('mousemove', mouseMoveHandle)
        return () => { window.removeEventListener('mousemove', mouseMoveHandle) }
    }, [])

    return position
}

使用:

tsx 复制代码
const MouseMove: React.FC = () => {
    
    let position = useMousePosition(200)

    return (
        <>
            <h1>鼠标的位置在:{JSON.stringify(position)}</h1>
        </>
    )
}

实现计时器Hook

实现基础功能:

1.10s后可以点击打印确认此协议

tsx 复制代码
export const CounterDown: React.FC = () => {

    const [count, setCount] = useState(10)

    const [disabled, setDisabled] = useState(true)

    const makeSure = () => {
       console.log('确认此协议')
    }

    useEffect(() => {
        let timerId = setTimeout(() => {
            if (count > 1) {
                setCount(prev => prev - 1)
            } else {
                setDisabled(false)
            }
        }, 1000)
        return () => { clearTimeout(timerId) }
    }, [count])

    return (
        <>
            <button disabled={disabled} onClick={makeSure}> {disabled ? `请仔细阅读本协议(${count})秒` : '确认此协议'}  </button>
        </>
    )
}

2.抽离功能函数

ts 复制代码
// 定义函数类型
type UseCountDown = (time?:number)=>[number,boolean]

export const useCountDown:UseCountDown = (time:number = 10) => {
    const [count, setCount] = useState(time)

    const [disabled, setDisabled] = useState(true)

    useEffect(() => {
        let timerId = setTimeout(() => {
            if (count > 1) {
                setCount(prev => prev - 1)
            } else {
                setDisabled(false)
            }
        }, 1000)
        return () => { clearTimeout(timerId) }
    }, [count])

    return [count,disabled]
}

3.引入使用

ts 复制代码
import { useCountDown } from '@/hooks/index'
export const CounterDown: React.FC = () => {

    const [count,disabled] = useCountDown(10)

    const makeSure = () => {
       console.log('确认此协议')
    }

    return (
        <>
            <button disabled={disabled} onClick={makeSure}> {disabled ? `请仔细阅读本协议(${count})秒` : '确认此协议'}  </button>
        </>
    )
}

useLayoutEffect

与useEffect区别

用法与useEffect一样,区别在于,useEffect在DOM绘制之后触发,useLayoutEffect在DOM绘制之前触发

useLayoutEffect会阻策页面渲染

useReducer

当状态更新逻辑复杂时可以考虑使用useReducer,useReducer可以同时更新多个状态,而且把状态从组件中独立出来。

语法:

ts 复制代码
const [ state,dispatch ] = useReducer(reducer,initState,initAction)

简单使用useReducer

tsx 复制代码
import React, { useReducer } from "react";

type UserType = typeof defaultState

// 初始状态
const defaultState = { name: "zhangsan", age: 18 }


// prevState形参是上一次的旧状态
const reducer = (prevState:UserType) => {
    return prevState
}

export const Father: React.FC = () => {

    const [state] = useReducer(reducer,defaultState)
    console.log(state) //{name: 'zhangsan', age: 18}
    
    return (
        <div>
            <button>修改用户名</button>
            <div className="father">
                <Son1 />
                <Son2 />
            </div>
        </div>
    )
}

const Son1: React.FC = () => {
    return (
        <div className="son1">
        </div>
    )
}


const Son2: React.FC = () => {
    return (
        <div className="son2">
        </div>
    )
}

使用initAction 处理初始数据

tsx 复制代码
import React, { useReducer } from "react";

type UserType = typeof defaultState

// 初始状态
const defaultState = { name: "zhangsan", age: -18 }


// prevState形参是上一次的旧状态
const reducer = (prevState: UserType) => {
    return prevState
}


//形参:初始状态
//返回值:处理好的初始状态
const initAction = (initState: UserType) => {
    // 处理age字段为正整数
    return { ...initState, age: Math.round(Math.abs(initState.age)) || 18 } 
}

export const Father: React.FC = () => {

    const [state] = useReducer(reducer, defaultState,initAction)
    console.log(state) //{name: 'zhangsan', age: 18}

    return (
        <div>
            <button>修改用户名</button>
            <div className="father">
                <Son1 />
                <Son2 />
            </div>
        </div>
    )
}

const Son1: React.FC = () => {
    return (
        <div className="son1">
        </div>
    )
}


const Son2: React.FC = () => {
    return (
        <div className="son2">
        </div>
    )
}

dispatch reducer修改状态

tsx 复制代码
import React, { useReducer } from "react";

type UserType = typeof defaultState

// 初始状态
const defaultState = { name: "zhangsan", age: -18 }


// prevState形参是上一次的旧状态
const reducer = (prevState: UserType, action) => {
    console.log("reducer执行了")
    switch (action.type) {
        case "UPDATE_NAME":
            return { ...prevState, name: action.payload }
        default:
            return prevState
    }
}


//形参:初始状态
//返回值:处理好的初始状态
const initAction = (initState: UserType) => {
    // 处理age字段为正整数
    return { ...initState, age: Math.round(Math.abs(initState.age)) || 18 }
}

export const Father: React.FC = () => {

    const [state, dispatch] = useReducer(reducer, defaultState, initAction)
    console.log(state) //{name: 'zhangsan', age: 18}

    const changeState = () => {
        // 直接修改state,打印显示修改成功,但是页面不会更新渲染
        // state.name = 'lisi'
        // console.log(state)

        // dispatch(信息对象)
        dispatch({ type: "UPDATE_NAME", payload: "lisi" })
    }

    return (
        <div>
            <div>{JSON.stringify(state)}</div>
            <button onClick={changeState}>修改用户名</button>
            <div className="father">
                <Son1 />
                <Son2 />
            </div>
        </div>
    )
}

const Son1: React.FC = () => {
    return (
        <div className="son1">
        </div>
    )
}

const Son2: React.FC = () => {
    return (
        <div className="son2">
        </div>
    )
}

使用props把父组件的数据传递给子组件使用

tsx 复制代码
// Father
<div className="father">
    <Son1 {...state}></Son1>
    <Son2 {...state}></Son2>
</div>
tsx 复制代码
// Son
const Son1: React.FC<UserType> = (props) => {
    return (
        <div className="son1">
            <p>{JSON.stringify(props)}</p>
        </div>
    )
}

子组件修改父组件属性

点击子组件按钮,使年龄+1

tsx 复制代码
import React, { useReducer } from "react";

type UserType = typeof defaultState

// 初始状态
const defaultState = { name: "zhangsan", age: -18 }
type ActionType = { type: "UPDATE_NAME", payload: string } | { type: "INCREMENT", payload: number }

// prevState形参是上一次的旧状态
const reducer = (prevState: UserType, action: ActionType) => {
    console.log("reducer执行了")
    switch (action.type) {
        case "UPDATE_NAME":
            return { ...prevState, name: action.payload }
        case "INCREMENT":
            return { ...prevState, age: prevState.age + action.payload }
        default:
            return prevState
    }
}


//形参:初始状态
//返回值:处理好的初始状态
const initAction = (initState: UserType) => {
    // 处理age字段为正整数
    return { ...initState, age: Math.round(Math.abs(initState.age)) || 18 }
}

export const Father: React.FC = () => {

    const [state, dispatch] = useReducer(reducer, defaultState, initAction)
    console.log(state) //{name: 'zhangsan', age: 18}

    const changeState = () => {
        // 直接修改state,打印显示修改成功,但是页面不会更新渲染
        // state.name = 'lisi'
        // console.log(state)

        // dispatch(信息对象)
        dispatch({ type: "UPDATE_NAME", payload: "lisi" })
    }

    return (
        <div>
            <div>{JSON.stringify(state)}</div>
            <button onClick={changeState}>修改用户名</button>
            <div className="father">
                <Son1 {...state} dispatch={dispatch}></Son1>
                <Son2 {...state}></Son2>
            </div>
        </div>
    )
}

const Son1: React.FC<UserType & {dispatch:React.Dispatch<ActionType>}> = (props) => {
    const { dispatch, ...user } = props
    const add = () => {
       dispatch({type:"INCREMENT",payload:1})
    }
    return (
        <div className="son1">
            <p>{JSON.stringify(user)}</p>
            <button onClick={add}>年龄+1</button>
        </div>
    )
}


const Son2: React.FC<UserType> = (props) => {
    return (
        <div className="son2">
            <p>{JSON.stringify(props)}</p>
        </div>
    )
}

在GrandSon组件中重置数据案例

tsx 复制代码
import React, { useReducer } from "react";

type UserType = typeof defaultState

// 初始状态
const defaultState = { name: "zhangsan", age: -18 }
type ActionType = { type: "UPDATE_NAME", payload: string } | { type: "INCREMENT", payload: number } | { type: "RESET" }

// prevState形参是上一次的旧状态
const reducer = (prevState: UserType, action: ActionType) => {
    console.log("reducer执行了")
    switch (action.type) {
        case "UPDATE_NAME":
            return { ...prevState, name: action.payload }
        case "INCREMENT":
            return { ...prevState, age: prevState.age + action.payload }
        case "RESET":
            return defaultState
        default:
            return prevState
    }
}


//形参:初始状态
//返回值:处理好的初始状态
const initAction = (initState: UserType) => {
    // 处理age字段为正整数
    return { ...initState, age: Math.round(Math.abs(initState.age)) || 18 }
}

export const Father: React.FC = () => {

    const [state, dispatch] = useReducer(reducer, defaultState, initAction)
    console.log(state) //{name: 'zhangsan', age: 18}

    const changeState = () => {
        // 直接修改state,打印显示修改成功,但是页面不会更新渲染
        // state.name = 'lisi'
        // console.log(state)

        // dispatch(信息对象)
        dispatch({ type: "UPDATE_NAME", payload: "lisi" })
    }

    return (
        <div>
            <div>{JSON.stringify(state)}</div>
            <button onClick={changeState}>修改用户名</button>
            <div className="father">
                <Son1 {...state} dispatch={dispatch}></Son1>
                <Son2 {...state} dispatch={dispatch}></Son2>
            </div>
        </div>
    )
}

const Son1: React.FC<UserType & { dispatch: React.Dispatch<ActionType> }> = (props) => {
    const { dispatch, ...user } = props
    const add = () => {
        dispatch({ type: "INCREMENT", payload: 1 })
    }
    return (
        <div className="son1">
            <p>{JSON.stringify(user)}</p>
            <button onClick={add}>年龄+1</button>
        </div>
    )
}


const Son2: React.FC<UserType & { dispatch: React.Dispatch<ActionType> }> = (props) => {
    return (
        <div className="son2">
            <p>{JSON.stringify(props)}</p>
            <GrandSon dispatch={props.dispatch} />
        </div>
    )
}



const GrandSon: React.FC<{dispatch:React.Dispatch<ActionType>}> = (props) => {
    const reset = () => {
       props.dispatch({type:"RESET"})
    }
    return (
        <>
            <h3>这是GrandSon组件</h3>
            <button onClick={reset}>重置</button>
        </>
    )
}

使用Immer编写更简洁的reducer更新逻辑

npm install immer use-immer -S

tsx 复制代码
import { useImmerReducer } from 'use-immer'

useImmerReducer可以之替换useReducer,使用和useReducer一样的功能


const [state, dispatch] = useImmerReducer(reducer, defaultState, initAction)


// reducer修改状态
const reducer = (prevState: UserType, action: ActionType) => {
    console.log("reducer执行了")
    switch (action.type) {
        case "UPDATE_NAME":
            // return { ...prevState, name: action.payload }
            prevState.name = action.payload
            break
        case "INCREMENT":
            // return { ...prevState, age: prevState.age + action.payload }
            prevState.age += action.payload
            break
        case "RESET":
            return defaultState
        default:
            return prevState
    }
}

useContext

在react函数式组件中,如果组件的嵌套层级很深,当父组件想把数据共享给深层的子组件,传统的方法是使用props一层层把数据向下传递。

使用props层层传递数据的维护性太差,可以使用 React.createContext() + useContext()轻松实现多层组件的数据传递。

使用步骤如下:

  • 1.在全局下创建Context对象
  • 2.在父组件中使用Context.Provider提供数据
  • 3.在子组件中使用useContext

基础用法示例:

tsx 复制代码
import React, { useState, useContext } from "react";


type AppContextType = { count: number, setCount: React.Dispatch<React.SetStateAction<number>> }

//1.创建context对象
const AppContext = React.createContext<AppContextType>({} as AppContextType)

export const LevelA: React.FC = () => {

    const [count, setCount] = useState(0)

    return (
        <div className="levelA">
            <h3>count的值是:{count}</h3>
            <button onClick={() => { setCount(prev => prev + 1) }}>+1</button>

            {/* 2.AppContext.Provider提供数据 */}
            <AppContext.Provider value={{ count, setCount }}>
                <LevelB />
            </AppContext.Provider>
        </div>
    )
}

const LevelB: React.FC = () => {
    return (
        <div className="levelB">
            <LevelC />
        </div>
    )
}


const LevelC: React.FC = () => {
    const ctx = useContext(AppContext)
    const add = () => {
        ctx.setCount((prev) => prev + 1)
    }
    const reset = ()=>{
        ctx.setCount(() => 0)
    }
    return (
        <div className="levelC">
            <p>count的值是:{ctx.count}</p>
            <button onClick={add}>+1</button>
            <button onClick={reset}>重置</button>
        </div>
    )
}

***以非侵入的方式使用Context

创建wrapper组件:

tsx 复制代码
import React, { useState, useContext } from "react";


type AppContextType = { count: number, setCount: React.Dispatch<React.SetStateAction<number>> }

//1.创建context对象
const AppContext = React.createContext<AppContextType>({} as AppContextType)


// 创建一个wrapper组件
export const AppContextWrapper:React.FC<React.PropsWithChildren> = (props)=>{
  const [count, setCount] = useState(0)
  return(
    // {props.children} 这里使用了插槽  即是 levelA
    <AppContext.Provider value={{ count, setCount }}>{props.children}</AppContext.Provider>
  )
}


export const LevelA: React.FC = () => {
    const ctx =  useContext(AppContext)
    return (
        <div className="levelA">
            <h3>count的值是:{ctx.count}</h3>
            <button onClick={() => { ctx.setCount(prev => prev + 1) }}>+1</button>

            <LevelB />
        </div>
    )
}

const LevelB: React.FC = () => {
    return (
        <div className="levelB">
            <LevelC />
        </div>
    )
}


const LevelC: React.FC = () => {
    const ctx = useContext(AppContext)
    const add = () => {
        ctx.setCount((prev) => prev + 1)
    }
    const reset = ()=>{
        ctx.setCount(() => 0)
    }
    return (
        <div className="levelC">
            <p>count的值是:{ctx.count}</p>
            <button onClick={add}>+1</button>
            <button onClick={reset}>重置</button>
        </div>
    )
}

使用:

tsx 复制代码
import React from "react"
import { AppContextWrapper, LevelA } from "@/components/useContext/base"

const App: React.FC = () => {
    return (
        <AppContextWrapper>
            <LevelA />
        </AppContextWrapper>
    )
}

export default App

React.memo useMemo

React.memo缓存组件

以下代码,当父组件中引用子组件时,父组件更新,连带着子组件也会更新,而子组件仅仅是用上了父组件穿过来的count。

tsx 复制代码
// 优化前
import React, { useEffect, useState } from "react";

export const Father:React.FC = () => {
    const [count, setCount] = useState(0)
    const [flag, setFlag] = useState(false)

    const add = () => {
        setCount(prev => prev + 1)
    }

    const toggle = () => {
       setFlag(prev=>!prev)
    }

    return (
        <div>
            <h1>父组件</h1>
            <h3>count的值是:{count}</h3>
            <h3>flag的值是:{String(flag)}</h3>
            <button onClick={add}>+1</button>
            <button onClick={toggle}>Toggle</button>
            <Son count={count} />
        </div>
    )
}

const Son:React.FC<{count:number}> = ({ count }) => {
    useEffect(() => {
        console.log("触发了子组件的渲染")
    })
    return (
        <div>
            <h1>子组件</h1>
            <h3>count的值是:{count}</h3>
        </div>
    )
}

假如子组件需要仅仅在count值改变时,更新,而flag这个没用上的值改变时不更新,就可以使用React.memo来实现。

tsx 复制代码
// 优化后:
// 使用React.memo包裹子组件即可

const Son:React.FC<{count:number}> = React.memo(({ count }) => {
    useEffect(() => {
        console.log("触发了子组件的渲染")
    })
    return (
        <div>
            <h1>子组件</h1>
            <h3>count的值是:{count}</h3>
        </div>
    )
})

useMemo缓存计算结果

useMemo是React中的一个Hook,用于在组件渲染期间缓存计算结果,避免不必要的重复计算,从而优化性能

ts 复制代码
const memoValue = useMemo(()=>{
   return 计算的值
},[value]) //表示监听value变化

依赖项:

  • 不传,每次更新都会执行
  • \],只会计算一次

Father中的以下代码,tips计算仅仅与flag有关,但是我们在修改count的值时,重新渲染组件时,也会执行tips计算,即更新组件就是进行tips计算。

tsx 复制代码
// 优化前
export const Father: React.FC = () => {
    const [count, setCount] = useState(0)
    const [flag, setFlag] = useState(false)

    const add = () => {
        setCount(prev => prev + 1)
    }

    const toggle = () => {
        setFlag(prev => !prev)
    }

    //根据布尔值进行计算  动态返回内容
    const tips = () => {
        console.log("触发了tips的重新计算")
        return flag ? "上班" : "下班"
    }

    return (
        <div>
            <h1>父组件</h1>
            <h3>count的值是:{count}</h3>
            <h3>flag的值是:{String(flag)}</h3>
            <h1>{tips()}</h1>  
            <button onClick={add}>+1</button>
            <button onClick={toggle}>Toggle</button>
            <Son count={count} />
        </div>
    )
}

使用useMemo缓存计算

tsx 复制代码
export const Father: React.FC = () => {
    const [count, setCount] = useState(0)
    const [flag, setFlag] = useState(false)

    const add = () => {
        setCount(prev => prev + 1)
    }

    const toggle = () => {
        setFlag(prev => !prev)
    }

    //根据布尔值进行计算  动态返回内容
    const tips = useMemo(() => {
        console.log("触发了tips的重新计算")
        return flag ? "上班" : "下班"
    },[flag]) 

    return (
        <div>
            <h1>父组件</h1>
            <h3>count的值是:{count}</h3>
            <h3>flag的值是:{String(flag)}</h3>
            <h1>{tips}</h1>  
            <button onClick={add}>+1</button>
            <button onClick={toggle}>Toggle</button>
            <Son count={count} />
        </div>
    )
}

useCallback

useMemo 是缓存某个变量值的效果 useCallback用来对组件内的函数进行缓存,它返回的事缓存的函数

ts 复制代码
const memoCallback = useCallback(cb,array)

useCallback会返回一个memorized回调函数供组件使用

  • 1.cb是一个函数,用于处理业务逻辑,这个cb就是需要被缓存的函数
  • 2.array是依赖项数组,当array中的依赖项发生变化时才会重新执行useCallback

-- 省略array,每次更新都会重新计算

-- array为空数组,则会在组件第一次初始化时计算一次

-- array不为空,只有在依赖项变化才会执行

tsx 复制代码
//优化前:

import React, { useState } from "react";


export const Search: React.FC = () => {
    const [kw,setKw] = useState("")

    // 这里只要输入一个字符  就会创建一次onKwChange方法
    const onKwChange = (e:React.ChangeEvent<HTMLInputElement>)=>{
       setKw(e.currentTarget.value)
    }

    return (
        <>
            <input type="text" value={kw} onChange={onKwChange} />
            <hr />
            <p>{kw}</p>
        </>
    )
}
tsx 复制代码
//优化后:
export const Search: React.FC = () => {
    const [kw, setKw] = useState("")

    // useCallback缓存函数 避免重新创建
    const onKwChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
        setKw(e.currentTarget.value)
    },[])

    return (
        <>
            <input type="text" value={kw} onChange={onKwChange} />
            <hr />
            <p>{kw}</p>
        </>
    )
}

useTransition

useTransition 可以将一个更新转为低优先级 更新,不阻塞UI对用户操作的响应

tsx 复制代码
function TabContainer(){
    const [ isPending,startTransition ] = useTransition();
}
  • 调用useTransition不需要任何参数 返回值:
  • isPending 布尔值:是否存在待处理的transition,如果值为true,说明页面上存在待渲染的部分,可以提示用户页面正在加载
  • startTransition 函数:调用此函数,可以把状态的更新标记为低优先级的,不阻塞UI对用户的响应

注意事项 1.传递给startTransition的函数必须是同步的,React会立即执行此函数。(startTransition() 中代码不能是异步的) 2.被标记为transition的状态更新将被其他状态更新打断。(低优先级渲染,切换为其他渲染,会打断低优先级渲染)

使用案例: 以下代码,当鼠标悬浮在按钮上会有样式变化,当我们切换到Movie标签时,由于Movie标签下渲染了一万条数据,所以切换过程中,在将鼠标悬浮在别的tab,不会立即变化,而是等Movie标签下渲染完毕了,才有样式变化, 这样会造成卡顿现象,也就是页面渲染渲染阻塞了用户交互

tsx 复制代码
import React, { useState } from "react";

export const TabContainer: React.FC = () => {
    const [activeTab, setActiveTab] = useState('home')

    const onBtnClick = (name: string) => {
        setActiveTab(name)
    }

    return (
        <div style={{ height: '500px' }}>
            <TabButton isActive={activeTab === 'home'} onClick={() => { onBtnClick('home') }}>Home</TabButton>
            <TabButton isActive={activeTab === 'movie'} onClick={() => { onBtnClick('movie') }}>Movie</TabButton>
            <TabButton isActive={activeTab === 'about'} onClick={() => { onBtnClick('about') }}>About</TabButton>
            <hr />
            {activeTab === 'home' && <HomeTab />}
            {activeTab === 'movie' && <MovieTab />}
            {activeTab === 'about' && <AboutTab />}
        </div>
    )
}


const TabButton: React.FC<React.PropsWithChildren & { onClick: () => void; isActive: boolean }> = (props) => {
    return (
        <>
            <button className={['btn', props.isActive ? 'active' : ''].join(" ")} onClick={props.onClick}>{props.children}</button>
        </>
    )
}


const HomeTab: React.FC = () => {
    return (
        <>
            HomeTab
        </>
    )
}

const MovieTab: React.FC = () => {
    const items = Array(10000).fill("MovieTab").map((item,index)=>{
        return <p key={index}>{item}</p>
    })
    return items
}

const AboutTab: React.FC = () => {
    return (
        <>
            AboutTab
        </>
    )
}
css 复制代码
.btn{
    border: 1px solid #fff;
    margin: 5px;
    padding: 10px;
    background-color: rgb(8, 92, 238);
    color: #fff;
    transition: opacity 0.5s ease;
}

.btn:hover{
    opacity: 0.6;
    transition: opacity 0.5s ease;
}


.btn.active{
    background-color: rgb(3, 150, 0);
}

优化

将切换tab时渲染变为低优先级:

关键代码:

tsx 复制代码
const [,startTransition] = useTransition()

const onBtnClick = (name: string) => {
    // 把某次更新,标记为低优先级的
    startTransition(()=>{
        setActiveTab(name)
    })
}

isPending实现页面loading

tsx 复制代码
import React, { useState, useTransition } from "react";


export const TabContainer: React.FC = () => {
    const [activeTab, setActiveTab] = useState('home')

    const [isPending, startTransition] = useTransition()

    const onBtnClick = (name: string) => {
        // 把某次更新,标记为低优先级的
        startTransition(() => {
            setActiveTab(name)
        })
    }

    // isPending为true则展示loading
    const Tab = () => {
        if (isPending) {
            return <h3>loading...</h3>
        }
        switch (activeTab) {
            case 'home':
                return <HomeTab />
            case 'movie':
                return <MovieTab />
            case 'about':
                return <AboutTab />
        }
    }

    return (
        <div style={{ height: '500px' }}>
            <TabButton isActive={activeTab === 'home'} onClick={() => { onBtnClick('home') }}>Home</TabButton>
            <TabButton isActive={activeTab === 'movie'} onClick={() => { onBtnClick('movie') }}>Movie</TabButton>
            <TabButton isActive={activeTab === 'about'} onClick={() => { onBtnClick('about') }}>About</TabButton>
            <hr />
            {Tab()}
        </div>
    )
}

const TabButton: React.FC<React.PropsWithChildren & { onClick: () => void; isActive: boolean }> = (props) => {
    return (
        <>
            <button className={['btn', props.isActive ? 'active' : ''].join(" ")} onClick={props.onClick}>{props.children}</button>
        </>
    )
}


const HomeTab: React.FC = () => {
    return (
        <>
            HomeTab
        </>
    )
}

const MovieTab: React.FC = () => {
    const items = Array(10000).fill("MovieTab").map((item, index) => {
        return <p key={index}>{item}</p>
    })
    return items
}

const AboutTab: React.FC = () => {
    return (
        <>
            AboutTab
        </>
    )
}

useDeferredValue

使用useDeferredValue可以让状态延迟更新

以下案例,useDeferredValue + React.memo实现输入框查询效果,类似防抖,但是和防抖有些区别:

与防抖或节流不同,useDeferredValue 不需要选择任何固定延迟时间。如果用户的设备很快(比如性能强劲的笔记本电脑),延迟的重渲染几乎会立即发生并且不会被察觉。如果用户的设备较慢,那么列表会相应地"滞后"于输入,滞后的程度与设备的速度有关。

此外,与防抖或节流不同,useDeferredValue 执行的延迟重新渲染默认是可中断的。这意味着,如果 React 正在重新渲染一个大型列表,但用户进行了另一次键盘输入,React 会放弃该重新渲染,先处理键盘输入,然后再次开始在后台渲染。相比之下,防抖和节流仍会产生不顺畅的体验,因为它们是阻塞的:它们仅仅是将渲染阻塞键盘输入的时刻推迟了。

tsx 复制代码
//未优化前,输入一个字符就立即渲染一下,会有卡顿效果
import React, { useState } from "react";


export const SearchList: React.FC = () => {
    const [text, setText] = useState('')
    const changeText = (e: React.ChangeEvent<HTMLInputElement>) => {
       setText(e.target.value)
    }
    return (
        <>
            <input type="text" value={text} onChange={(e) => { changeText(e) }} />
            <hr />
            <ShowList text={text}  />
        </>
    )
}

const ShowList: React.FC<{text:string}> = (props) => {
    return Array(10000).fill(props.text).map((item,index)=>{
       return <p key={index}>{item}</p>
    })
}
tsx 复制代码
// 优化后
import React, { useDeferredValue, useEffect, useState } from "react";


export const SearchList: React.FC = () => {
    const [text, setText] = useState('')
    // deferredText会延迟更新
    const deferredText = useDeferredValue(text)
    const changeText = (e: React.ChangeEvent<HTMLInputElement>) => {
       setText(e.target.value)
    }
    useEffect(()=>{
        // 这里打印,能看出text和deferredText的差异
        console.log(deferredText,"deferredText"),
        console.log(text,'text')
    },[text])
    return (
        <>
            <input type="text" value={text} onChange={(e) => { changeText(e) }} />
            <hr />
            <ShowList text={deferredText}  />
        </>
    )
}

// React.memo缓存,deferredText更新后组件才会重新渲染
const ShowList: React.FC<{text:string}> = React.memo((props) => {
    return Array(10000).fill(props.text).map((item,index)=>{
       return <p key={index}>{item}</p>
    })
}) 
相关推荐
OEC小胖胖2 小时前
组件化思维(下):表单与交互组件,倾听用户的心声
前端·微信小程序·小程序·微信开放平台
长安——归故李2 小时前
【PLC程序学习】
java·c语言·javascript·c++·python·学习·php
狼爷2 小时前
前端项目从 Windows 到 Linux:构建失败的陷阱
前端·node.js·angular.js
1024小神2 小时前
vitepress多语言实现第一次跟随浏览器,第二次不跟随
前端
叫我詹躲躲2 小时前
🚀 震撼!10道DFS&BFS神级题目让你的算法能力飙升300%
前端·leetcode
ssshooter2 小时前
WebGL 切换 Shader 的策略
前端·webgl
WDyinh2 小时前
积分球领取补位动画实现
前端·javascript
前端开发爱好者3 小时前
v5.0 版本发布!Vue3 生态最强大的 3D 开发框架!
前端·javascript·vue.js
Sosse3 小时前
window.close()失效 + Chrome浏览器调试线上代码
前端·浏览器