hook 简介

什么是hook

hook 是React 16.8 的新增特性,它可以让你在不编写class的情况下使用state以及其他React其他特性

hook官方文档:React 官方中文文档 (docschina.org)

useState

useState能让函数组件拥有自己的状态,因此,它是一个状态管理的hooks API。通过useState可以实现状态的初始化、读取、更新。

使用方式
1
2
const [状态名, set函数] = useState(初始值)
// 其中:状态名所代表的数据,可以被函数组件使用;如果要修改状态名所代表的数据,需要调用set函数进行修改
案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ./count.tsx
import React,{useState} from 'react'

export const count:React.FC = ()=>{
//定义状态count,设置初始值为0
const [count, setCount] = useState(0)
//如果要修改count值,需要调用setCount()

return (
<>
<h1> {count} </h1>
<button onClick={ ()=>setCount(count+1) }> +1 </button>
</>
)
}
状态改变时,会触发函数组件的重新执行

在函数组件中使用setState定义状态之后,每当状态发生变化,都会触发函数组件的重新执行,从而根据最新数据更新渲染DOM结构。

当函数式组件被重新执行时,不会重复调用useState() 给数据赋初始值,而是复用上次的state值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React,{useState} from 'react'

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

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

return (
<>
<h1>{count}</h1>
<button onClick={add} >+1</button>
</>
)
}
以函数的形式为状态赋初始值

在使用useState定义状态是,除了可以直接给定初始值,还可以通过函数返回值的形式,为状态赋初始值

以函数的形式为状态赋初始值时,只有组件首次渲染才会执行useState(fn(){}) 里的fn() ; 当组件状态更新时,fn() 是不会执行的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React,{useState} from 'react'

export const DateCom:React.FC = ()=>{
// 使用函数返回值的形式给状态赋初始值
const [date, setDate] = useState( ()=>{
const dt = new Date()
return {year:dt.getFullyear, month:dt.getMonth()+1, day:dt.getDate()}
})

return (
<>
<h1>日期</h1>
<p>年:{data.year}</p>
<p>月:{data.month}</p>
<p>日:{data.day}</p>
</>
)
}
useState 是异步变更状态的

调用useState() 会返回一个变更状态的函数,这个函数内部是以异步形式修改状态的,所以修改状态后,无法立即拿到最新状态

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

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

const add = ()=>{
//自增
setCount(count++)
// 打印值
console.log(count) // 打印的都是count修改前的值
}

return(
<>
<h1>{count}</h1>
<button onClick={add} >+1</button>
</>
)
}
解决值更新不及时的bug

当连续多次以相同的操作更新状态的值时,React内部会对传递过来的值进行比较,如果值相同,则会屏蔽后续的更新行为,从而防止组件频繁渲染的问题。

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

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

const add = ()=>{
// 自增
setCount(count++)
// 再一次自增
setCount(count++)
}

return (
<>
<h1>{count}</h1>
<button onClick={add}>+1</button>
</>
)
}

结果为每次加为1,因为setCount是异步更新状态的,前后两次调用传递的都是相同值。React内部如果两次修改时一样的,则会默认阻止组件再次更新。

解决

如果要两次都生效,我们可以使用函数的方式给状态赋新值。当函数执行时才通过函数的形参,拿到当前状态值,并基于它的返回新的状态值

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

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

const add = ()=>{
// 传递更新函数, 通过返回值更新
setCount((val)=>(val+1))
// 再次自增
setCount((val)=>(val+1))
}

return (
<>
<h1>{count}</h1>
<button onClick={add}>+1</button>
</>
)
}
更新对象类型的值

如果要更新对象类型的值,并触发组件的重新渲染,则必须使用展开运算符或Object.assing()生成一个新对象,用新对象覆盖旧对象,才能正常触发组件的重新渲染

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
import React,{useState} from 'react'

export const UserInfo:React.FC = ()=>{
const [user, setUser] = useState({
name: 'sz',
age: 10,
gender: 0
})

const updateUserInfo = ()=>{
user.name = 'Jeson'
// 这种写法是直接将user覆盖,是错误写法,让user丢失age和gender属性
// setUser(user)
//使用展开运算符或Object.assing()
setUser({...user})
// or
setUser(Object.assign({},user))
}

return (
<>
<h1>用户信息</h1>
<p>昵称:{user.name}</p>
<p>年龄:{user.age}</p>
<p>性别:{user.gender == 0 ? '女' : '男'}</p>
<button onClick={updateUserInfo}>更改昵称</button>
</>
)
}
模拟组件强制刷新

在函数组件中,我们可以通过useState来模拟forceUpdate强制刷新操作。只要useState状态发生变化,就会触发函数组件的重新渲染,从而达到强制刷新目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React,{useState} from 'react'

export const Update:React.FC = ()=>{
const [, forceUpdate] = useState({})

//调用onRefresh(),都会给forceUpdate传递一个新对象( 每次传入的对象地址不同,所以会刷新),从而触发更新
const onRefresh = ()=>forceUpdate({})

return(
<>
<button onClick={onRefresh}>刷新组件 - {Date.now()}</button>
</>
)
}

useRef

使用方法

useRef函数返回一个可变的ref对象,该对象只有一个current属性。可以在调用useRef函数时为其定义指定初始值。并且这个返回的ref对象在组件的整个生命周期内保持不变。

useRef函数用来解决两个问题:

  • 获取DOM元素或子组件的实力对象
  • 存储渲染周期之间共享的数据
1
2
3
4
5
6
// 导入API
import {useRef} from 'react'
// 获取ref对象
const objRef = useRef(初始值)
// 访问元素中存储的值
console.log(objRef.current)
获取DOM元素实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React,{useRef} from 'react'

export const InputObject:React.FC = ()=>{
// 引用ref
const inpRef = useRef<HTMLInputElement>(null)

const getFocus = ()=>{
// 调用foucs API 获取焦点
inpRef.current?.focus()
}

return (
<>
<input ref={inpRef} type='text' />
<button onClick={getFocus}>点击获取input框焦点</button>
</>
)
}
存储渲染周期之间共享的数据

基于useRef创建名为prevCountRef 的数据对象,用来存储上一次的旧count值。每当点击按钮触发count自增时,都把最新的旧值赋值给prevCountRef.current 即可

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

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

// 默认值为undefind
const prevCountRef = useRef<number>()

const add = ()=>{
// 点击按钮时,让count异步+2
setCount((c)=>c+2)
// 同时,把count所代表的旧值记录到prevCountRef中
prevCountRef.current=count
}

return (
<>
<div>新值:{count}, 旧值:{prevCount.current}</div>
<button onclick={add}>+2</button>
</>
)
}
useRef的三个注意事项
  1. 在组件rerender时useRef不会被重新初始化

在RefTimer组件时,通过点击+1按钮修改状态,从而触发RefTimer组件的rerender。但是RefTimer组件中的时间戳保持不变。说明组件每次渲染不会重复调用useRef函数进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React,{useState, useRef} from 'react'

export const RefTimer:React.FC = ()=>{
const [count, setCount] = useState(0)
// 若每次重新渲染调用useRef API时,会重新获取time
const time = useRef(Date.now)
console.log(Date.now)

return(
<>
<div>useState-count:{count}</div>
<div>useRef-time:{time.current}</div>
<button onCliick={()=>{setCount(value=>count+1)}}>+1</button>
</>
)
}
  1. ref.current变化时不会造成组件的rerender

点击给ref赋新值,为time.current 赋新值,执行的结果是:

  1. 终端中输出了最新的time.current的值
  2. 没有触发RefTimer组件的rerender

说明改变ref.current不会造成组件的rerender

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React,{useState, useRef} from 'react'

export const RefTimer:React.FC = ()=>{
const [count, setCount] = useState(0)
// 若每次重新渲染调用useRef API时,会重新获取time
const time = useRef(Data.now)
console.log(Data.now)

const updateTime = ()=>{
time.current = Data.now
}
return(
<div>useState-count:{count}</div>
<div>useRef-time:{time.current}</div>
<button onCliick={updateTime}>change timRef</button>
)
}
  1. ref.current不能作为其他Hooks的依赖项

由于ref.current值的变化不会造成组件的rerender,而且React也不会跟踪ref.current变化,因此ref.current不能作为其他Hooks( useMemo,useCallback,useEffect等)的依赖项

forwardRef - React提供的一个函数

ref的作用是获取实例,但由于函数组件不存在实例,因此无法通过ref获取函数组件的实例引用。而React.forwardRef就是用来解决这个问题。React.forwardRef会创建一个React组件,这个组件能够将其接受的的ref属性转发到自己的组件数

forwardRef 是 React 中用于转发 ref 的函数。当使用 forwardRef 包裹一个组件时,我们可以在父组件中通过 ref prop 传递一个 ref 给子组件,从而访问子组件的 DOM 元素或实例。

forwardRef使用方法
1
2
3
4
5
6
7
//子组件
import React, { forwardRef, useRef } from 'react';

// 使用forwardRef包裹
const MyComponent = forwardRef((props, ref) => {
// 子组件逻辑
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//父组件
import React, { forwardRef, useRef } from 'react';
import MyComponent from './MyComponent'


const ParentComponent:React.FC = () => {
const parentRef = useRef(null);

return (
<MyComponent ref={parentRef}>
This is a child component
</MyComponent>
);
};
useImperativeHandle 使用方法

直接使用ref获取DOM实例,会全面暴露DOM实例上的API,从而导致外部使用ref时有更大的自由度。在实际开发中,我们应该严格控制ref的暴露颗粒度,控制它能够调用的方法,只向外暴露主要的功能函数,其他功能不暴露

在组件中引用forwardRef获取的ref返回值为undefind。React官方提供了useImperativeHandle, 让你在使用ref时可以自定义暴露给外部那些功能或属性

使用方法

1
useImperativeHandle( 通过forwardRef接受到的父组件ref对象, ()=>{自定义ref对象}, [依赖项数组])

案例

在被React.forwardRef() 包裹的组件中,需要结合useImperativeHandle Hooks API,向外暴露子组件内部成员属性和功能

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
import React,{useRef, useState, useImperativeHandle} from 'react'

// 子组件
const Child = React.forwardRef((_, ref)=>{
const [count, setCount] = useState(0)

const add = (step: number)=>{
setCount((val)=>(val += step))
}

// 定义子组件需要向外暴露的属性和功能
useImperativeHandle(ref,()=>({
//useImperativeHandle 向外暴露成员
count,
setCount
}))

return (
<>
<div>Child-count: {count}</div>
<button onClick={()=>{add(1)}}>+1</button>
</>
)
})

// 父组件
const RefTimer:React.FC = ()=>{
const ChildRef = useRef()

const showChildRef = ()=>{
console.log(ChildRef.current)
// 重置子组件count
ChildRef.current?.setCount(0)
}

return(
<>
<Child ref={ChildRef}></Child>
<button onClick={showChildRef}>打印子组件ref信息</button>
</>
)
}
控制成员暴露的颗粒度

在Child子组件中,希望对外暴露一个将count重置为0的函数并且做过滤,而不希望直接把setCount() 暴露出去,因为父组件调用setCount() 可以传递任何参数。可以提供一个reset() 函数

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
import React, {useRef, useState, useImperativeHandle} from 'react'

// 子组件
const Child = React.forwardRef((_, ref)=>{
const [count, setCount] = useState(0)

const add = (step:number)=>{
setCount((val)=>(val+=step))
}

useImperativeHandle((ref, ()=>{
count,
// 在组件内封装一个重置为0的函数
reset: ()=>setCount(0)
}))

return(
<>
<h3>Child</h3>
<div>{count}</div>
<button onclick={()=>add(+1)}>+1</button>
<button onclick={()=>add(-1)}>-1</button>
</>
)
})


// 父组件
const FatherReact.FC = ()=>{



const ChildRef = useRef<{count:number;reset:()=>viod}>(null)

return(
<>
<h3>Father</h3>
<Child ref={ChildRef}/>
<button onClick={()=>{ChildRef.current.reset}}>重置</button>
</>
)
}
useImperativeHandle第三个参数
1
2
3
4
5
6
useImperativeHandle(ref, creatHandle, [deps])
/*
第一个参数为父组件传递的ref
第二个参数为一个函数,返回对象会自动绑定到子组件的ref上。即子组件可以将自己内部的方法或者值,通过useImperativeHandle添加到父组件中的useRef定义的对象中
第三个参数时函数依赖值(可选),若createHandle函数中使用到了子组件内部定义的变量,则还需要将该变量作为依赖变量成为useImperativeHandle第三个参数
*/

第一种用法 - 传递空数组

只有在子组件首次渲染时,执行useImperativeHandle中的fn回调,从而把return的对象作为父组件接受到的ref

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
import React.{ useState, useImperativeHandle} from 'react'

// 子组件
const Child = ReactforwardRef((_, ref)=>{
const [count, setCount] = useState(0)

const add = (step:number)=>{
setCount((val)=>(val += step))
}

useImperativeHandle(
ref,
()=>{
// 第三个参数为空数字,子组件只有首次被渲染时,才会执行该回调。
console.log('只会执行一次')
return{
count,
reset: ()=>setCount(0)
}
},[])

return(
<>
<h3>Child</h3>
<div>{count}</div>
<button onCLick={()=>add(+1)}>+1</button>
<button onCLick={()=>add(-1)}>-1</button>
</>
)
})

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

}

第一种用法 - 传递依赖项数组

子组件首次渲染时和依赖项发生改变时,会执行useImperativeHandle的fn回调,从而让父组件通过ref获取到最新依赖值

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
import React.{ useState, useImperativeHandle} from 'react'

// 子组件
const Child = ReactforwardRef((_, ref)=>{
const [count, setCount] = useState(0)

const add = (step:number)=>{
setCount((val)=>(val += step))
}

useImperativeHandle(
ref,
()=>{
// 第三个参数为依赖项数组,子组件首次被渲染时,执行该回调。当依赖项数组内的数据(count)状态发生变化,执行该回调
console.log('执行一次')
return{
count,
reset: ()=>setCount(0)
}
},[count])

return(
<>
<h3>Child</h3>
<div>{count}</div>
<button onCLick={()=>add(+1)}>+1</button>
<button onCLick={()=>add(-1)}>-1</button>
</>
)
})

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

}

第三种用法 - 不传递第三个参数

组内任何state的变化,都会导致useImperativeHandle中的回调函数重新执行

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
import React.{ useState, useImperativeHandle} from 'react'

// 子组件
const Child = ReactforwardRef((_, ref)=>{
const [count, setCount] = useState(0)

const add = (step:number)=>{
setCount((val)=>(val += step))
}

useImperativeHandle(
ref,
()=>{
// 第三个参数为依赖项数组,子组件首次被渲染时,执行该回调。当依赖项数组内的数据(count)状态发生变化,执行该回调
console.log('执行一次')
return{
count,
reset: ()=>setCount(0)
}
})

return(
<>
<h3>Child</h3>
<div>{count}</div>
<button onCLick={()=>add(+1)}>+1</button>
<button onCLick={()=>add(-1)}>-1</button>
</>
)
})

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

}

useEffect

什么时函数的副作用

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

useEffect是React 中的Hooks API。通过useEffect 可以执行一些副作用操作。例如:请求数据,事件监听等

使用语法
1
2
3
4
5
6
7
useEffect(fn, deps?)
/*
第一个参数fn是一个副作用函数,该函数会在每次渲染完成之后被调用
第二个参数是可选的依赖项数组,这个数组中的每一项内容都会被用来进行渲染前后的对比
当依赖项发生变化时,会重新执行fn副作用函数
当依赖项没有任何变化时,则不会执行fn副作用函数
*/
useEffect 的执行时机

useEffect 会在函数组件每次渲染完成后执行。

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

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

// 打印h1 的值, 这里打印的值都是更改之前的旧值
console.log(documentquerySelector('h1')?.innerHTML)

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

// 组件每次渲染完成之后,都会重新执行useEffect中的回调函数
useEffect(()=>{
console.log('useEffect',documentquerySelector('h1')?.innerHTML)
})

return(
<>
<h1>{count}</h1>
<button onClock={()=>{add}}>+1</button>
</>
)
}
deps为依赖数组

如果想有条件的触发副作用函数的重新执行,则需要通过deps数组执行依赖项列表

React会在组件每次渲染完成后,对比渲染前后每一个依赖项的变化,只要任何一个依赖项发生变化,都会触发副作用函数的重新执行

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

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

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

useEffect(()=>{
console.log('useEffect')
}, [count])

return(
<>
<div>{count}</div>
<div>{flag}</div>
<button onclick={()=>{add}}>+1</button>
<button onclick={()=>{setFlag((val)=>!val}}>change flag</button>
</>
)
}
deps为空数组

如果useEffect指定一个空数组作为依赖项,则副作用只会在组件首次渲染完成后执行一次。当组件rerender时,也不会触发副作用函数

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

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

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

useEffect(()=>{
console.log('useEffect')
}, [])

return(
<>
<div>{count}</div>
<div>{flag}</div>
<button onclick={()=>{add}}>+1</button>
<button onclick={()=>{setFlag((val)=>!val}}>change flag</button>
</>
)
}
useEffect 两个使用注意事项
  1. 不要在useEffect中改变依赖项值,会造成死循环
  2. 多个不同功能的副作用可以分开声明
清理副作用

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

清理函数触发时机

  1. 当组件卸载时调用
  2. 当useEffect 副作用函数调用之前,会先执行清理函数
1
2
3
4
5
6
useEffect(()=>{
// 执行逻辑

// 返回一个清理副作用的函数
return ()=>{/*清理逻辑操作*/}
}, [依赖项])

实际应用场景

可以在返回的函数中清除定时器或事件监听,或者终止未完成的ajax请求

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
import React, {useState, useEffect} from 'react'

export const MouseInfo:React.FC = ()=>{
// 定义节流延时器
let timerId: null | NodeJS.Timeout = null
// 记录鼠标位置
const [position, setPosition] = useState({x:0, y:0})

// 副作用函数
useEffect(()=>{
// 如果不存在延时器时,直接return
if(timerId !== null) return
//存在延时器,获取鼠标坐标
timerId = setTimeout(()=>{
const mouseMoveHandler = (e:MouseEvent)=>{
console.log({x:e.clientX, y:e.clientY})
setPosition({x:e.clientX, y:e.clientY})
timerId = null
}
},500)


//组件渲染完后添加鼠标监听事件
window.addEventListener('mousemove', mouseMoveHandler)

// 返回一个清理函数,在每次组件卸载时,解绑鼠标监听事件
return ()=>window.removeEventListener('mousemove', mouseMoveHandler)
}, [])

return(
<>
<div>鼠标位置:{JSON.stringify(poption)}</div>
</>
)
}

useLayoutEffect

useLayoutEffect和useEffect区别
  1. 用法相似
    • useLayoutEffect 接受一个函数和一个依赖项数组作为参数
    • 只有在数组依赖项发生改变时才会触发副作用函数
    • useLayoutEffect也可以返回一个清理函数
  2. 区别
hooks名称 执行时机 执行过程
useEffect 在浏览器重新绘制屏幕之后触发 异步执行,不会阻塞浏览器绘制
useLayoutEffect 在浏览器重新绘制屏幕之前触发 同比执行,阻塞浏览器重新绘制

案例

点击按钮,把num值设置为0,当页面更新完后,判断num是否等于0,如果等于0,则在useEffect把num赋值为随机函数

使用useEffect 会发现,页面数字会出现闪烁效果。会先渲染为0,然后渲染随机数字。因为useEffect是浏览器绘制之后触发的

解决办法:使用useLayoutEffect,在浏览器绘制之前触发

useReducer

当状态更新逻辑较为复杂时,可以考虑使用useReducer。useReducer可以同时更新多个状态,而且能把对状态的修改从组件中独立出来。相比于useState,useReducer可以更好的描述 如何更新状态 。例如组件负责发出行为,useReducer负责更新状态。

可以让代码逻辑更清晰,代码行为更易预测

useReducer 语法格式
1
2
3
4
5
6
7
8
const [state,dispatch] = useReducer(reducer, initState, initAction?)
/*
reducer 是一个函数,类似于(prevState, action)=>newState 。 形参prevState表示旧状态,形参action表示本次的行为,返回值newState表示处理完毕后的新状态
initState 表示初始状态,也就是默认值
initAction 是进行状态初始化时的处理函数,是可选的。如果提供该函数,则会把initState传递给该函数进行处理,initAction的返回值会被当做初始状态

返回值state是状态值,dispatch是更新state的方法,让它接受action作为参数,useReducer只需要调用dispatch(action)方法传入action即可更新state
*/
基本使用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 父组件
import React,{useReducer} from 'react'

type UserType = typeof defaultState

//初速状态
const defaultState = {name:'zhangsan', age:16}
//reducer函数
const reducer = (val:UserType)=>{
console.log('reducer函数')
return val
}
export const Father:React.FC = ()=>{

const [state] = useReducer(reducer, defaultState)
console.log(state)

return(
<>
<button>修改name</button>
</>
)
}
initAction处理初始化数据 - 可选参数

定义名为 initAction 的处理函数,如果初始化数据中age为小数,负数,0,则对age进行非法值处理。在Father组件中,声明initAction函数

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

type UserType = typeof defaultState

//初速状态
const defaultState = {name:'zhangsan', age:16}
//reducer函数
const reducer = (val:UserType)=>{
console.log('reducer函数')
return val
}
// initAction
const initAction = (initState:UserType)=>{
// 当写了initAction ,会把initAction的返回值,当做useReducer的初始值
return {...initState, age:Math.round(Math.abs(initState.age)) || 18}
}


export const Father:React.FC = ()=>{
const [state] = useReducer(reducer, defaultState, initAction)
}
案例 - 在Father组件中点击修改按钮修改name值
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
import React, {useReducer} from 'react'

type UserType = typeof defaultState
type ActionType = {
type: 'UPDATE_NAME';
payload: string
} | {
type: 'UPDATE_AGE';
payload: number
}

//初速状态
const defaultState = {name:'zhangsan', age:16}
//reducer函数
const reducer = (val:UserType, action:ActionType)=>{
console.log('reducer函数')
return val

switch(action.type){
case 'UPDATE_NAME':
return {...val, name:action.payload}
case 'UPDATE_AGE':
return {...val, age:action.payload}
default:
return val
}
}
// initAction
const initAction = (initState:UserType)=>{
// 当写了initAction ,会把initAction的返回值,当做useReducer的初始值
return {...initState, age:Math.round(Math.abs(initState.age)) || 18}
}


export const Father:React.FC = ()=>{
const [state, dispatch] = useReducer(reducer, defaultState, initAction)

const changeUserName = ()=>{
// 直接修改state数据,页面不会变化;而是应该触发reduce函数,使用reducer函数进行修改,才会被重新渲染
// state.name = '张三',
dispatch({type: 'UPDATE_NAME', payload: '张三'})
console.log(state)
}

return(
<>
<div>{JSON.stringify(state)}</div>
<button onClick={changeUserName}>修改用户名</button>
</>
)
}
案例 - 把用户信息渲染到子组件中
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
import React, {useReducer} from 'react'

type UserType = typeof defaultState
type ActionType = {
type: 'UPDATE_NAME';
payload: string
} | {
type: 'UPDATE_AGE';
payload: number
}

//初速状态
const defaultState = {name:'zhangsan', age:16}
//reducer函数
const reducer = (val:UserType, action:ActionType)=>{
console.log('reducer函数')
return val

switch(action.type){
case 'UPDATE_NAME':
return {...val, name:action.payload}
case 'UPDATE_AGE':
return {...val, age:action.payload}
default:
return val
}
}
// initAction
const initAction = (initState:UserType)=>{
// 当写了initAction ,会把initAction的返回值,当做useReducer的初始值
return {...initState, age:Math.round(Math.abs(initState.age)) || 18}
}

//父组件
export const Father:React.FC = ()=>{
const [state, dispatch] = useReducer(reducer, defaultState, initAction)

const changeUserName = ()=>{
// 直接修改state数据,页面不会变化;而是应该触发reduce函数,使用reducer函数进行修改,才会被重新渲染
// state.name = '张三',
dispatch({type: 'UPDATE_NAME', payload: '张三'})
console.log(state)
}

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

// 子组件Son1
const Son1:React.FC:<UserType> = (props)=>{

return(
<>
<div className='son1'>
<p>{JSON.stringify(props)}</p>
</div>
</>
)
}
// 子组件Son2
const Son2:React.FC:<UserType> = (props)=>{

return(
<>
<div className='son2'>
<p>{JSON.stringify(props)}</p>
</div>
</>
)
}
通过子组件点击增加执行父组件reducer函数
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
91
92
93
94
95
import React, {useReducer} from 'react'

type UserType = typeof defaultState
type ActionType = {
type: 'UPDATE_NAME';
payload: string
} | {
type: 'UPDATE_AGE';
payload: number
}

//初速状态
const defaultState = {name:'zhangsan', age:16}
//reducer函数
const reducer = (val:UserType, action:ActionType)=>{
console.log('reducer函数')
return val

switch(action.type){
case 'UPDATE_NAME':
return {...val, name:action.payload}
case 'UPDATE_AGE':
return {...val, age:val.age + action.payload}
default:
return val
}
}
// initAction
const initAction = (initState:UserType)=>{
// 当写了initAction ,会把initAction的返回值,当做useReducer的初始值
return {...initState, age:Math.round(Math.abs(initState.age)) || 18}
}

//父组件
export const Father:React.FC = ()=>{
const [state, dispatch] = useReducer(reducer, defaultState, initAction)

const changeUserName = ()=>{
// 直接修改state数据,页面不会变化;而是应该触发reduce函数,使用reducer函数进行修改,才会被重新渲染
// state.name = '张三',
dispatch({type: 'UPDATE_NAME', payload: '张三'})
console.log(state)
}

return(
<>
<div>{JSON.stringify(state)}</div>
<button onClick={changeUserName}>修改用户名</button>
<!-- 通过props传递dispatch -->
<Son1 {...state} dispatch={dispatch}/>
<Son2 {...state} dispatch={dispatch}/>
</>
)
}

// 子组件Son1
const Son1:React.FC:<UserType & {dispatch: React.Dispatch<ActionType>}> = (props)=>{
// 解构出dispatch,剩余的保存在user
const {dispatch, ...user} = props

const add = ()=>{
// 调用父组件dispatch
dispatch({type:'UPDATE_AGE', payload:1})
}

return(
<>
<div className='son1'>
<p>{JSON.stringify(user)}</p>
<!--Son1 +1-->
<button onClick={add}>+1</button>
</div>
</>
)
}
// 子组件Son2
const Son2:React.FC:<UserType & {dispatch: React.Dispatch<ActionType>}> = (props)=>{
const {dispatch, ...user} = props


const add = ()=>{
// 调用父组件dispatch
dispatch({type:'UPDATE_AGE', payload:2})
}

return(
<>
<div className='son2'>
<p>{JSON.stringify(user)}</p>
<!--Son1 +2-->
<button onClick={add}>+2</button>
</div>
</>
)
}
Immer - 简写reducer更新逻辑

安装immer相关依赖

1
npm install immer use-immer --save

使用 useImmerReducer() 代替 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
import React from 'react'
import {useImmerReducer} from 'use-immer'

type UserType = typeof defaultState
type ActionType = {
type: 'UPDATE_NAME';
payload: string
} | {
type: 'UPDATE_AGE';
payload: number
} | {
type: 'REST'
}

//初速状态
const defaultState = {name:'zhangsan', age:16}
//reducer函数
const reducer = (val:UserType, action:ActionType)=>{
console.log('reducer函数')
return val
// 使用immer,更改state更方便
switch(action.type){
case 'UPDATE_NAME':
val.name = action.payload
case 'UPDATE_AGE':
val.age += action.payload
case 'REST':
val = defaultState
default:
return val
}
}
// initAction
const initAction = (initState:UserType)=>{
// 当写了initAction ,会把initAction的返回值,当做useReducer的初始值
return {...initState, age:Math.round(Math.abs(initState.age)) || 18}
}


export const Father:React.FC = ()=>{
const [state, dispatch] = useImmerReducer(reducer, defaultState, initAction)

const changeUserName = ()=>{
// 直接修改state数据,页面不会变化;而是应该触发reduce函数,使用reducer函数进行修改,才会被重新渲染
// state.name = '张三',
dispatch({type: 'UPDATE_NAME', payload: '张三'})
console.log(state)
}

return(
<>
<div>{JSON.stringify(state)}</div>
<button onClick={changeUserName}>修改用户名</button>
</>
)
}

useContext

在react函数式组件中,如果组件嵌套层级很深,当父组件想把数据共享给最深的子组件时,使用传统props,一层一层往下传维护性太差。这时候我们可以使用React.createContext() + useContext() 解决多层组件数据传递问题

image-20240817012546013

语法格式
  1. 在全局创建Context对象
  2. 在父组件中使用 Context.Provider 提供数据
  3. 在子组件中使用 useContext 使用数据
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
import React, {useContext} from 'react'

type User = {name: string; age:number}

// 父组件
const Fatch:React.FC = ()=>{
// 全局 Context对象
const MyContext = React.createContext("初始化数据")

const user:User = {name: 'zhangsan', age:22}

return(
<>
<MyContext.Provider value={user}>
<Son />
</MyContext.Provider>
</>
)
}

// 子组件
const Son:React.FC = ()=>{
// 子组件使用useContext()获取数据
const MyCtx = useContext(MyContext)

return(
<>
<div>
<p>姓名:{MyCtx.name}</p>
<p>年龄:{MyCtx.age}</p>
</div>
</>
)
}
以非入侵式的方式使用Context

在之前案例中,父组件使用侵入了<MyContext.Provider> 这样的代码结构
为了保证父组件使用代码的单一性,提高Provider的通用性。我们可以将Context.Provider封装到独立的Wrapper函数式组件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 将封装成组件 ./components/useContext/index.ts
// 声明ts类型
type ContextType = { count:number; setCount: React.Dispatch<React.SetStateAction<number>> }

// 创建Context对象
const AppContext

// 定义一个独立 Wrapper 组件,Wrapper 嵌套的子组件会被Provider注入数据
export const AppContextWrapper = React.FC<React.PropsWithChildren> = (props)=>{
// 定义共享数据
const [count, setCount] = useState(0)
// 使用AppCounttext.Provider 向下共享数据
return <AppContext.Provider value={{count, setCount}}>{props.children}</AppContext.Provider>
}
1
2
3
4
5
6
7
8
9
10
11
12
// 在父组件中使用
import React from 'react'
// 引用封装组件
import {AppContextWrapper} from 'components/useContext/index.ts'

const App:React.FC = ()=>{
return(
<AppContextWrapper>
<>
</AppContextWrapper>
)
}
案例 - 使用Context 重构useReducer

将props透传改成context传递

React.memo - 使用memo函数缓存组件

当父组件被重新渲染时,也会触发子组件的重新渲染。如果子组件状态没有变化,也会被触发重新渲染。多出了无意义的性能开销
而React.memo 可以解决上述问题,防止子组件不必要的重新渲染,达到提高性能目的

基本用法
1
2
import React from 'react'
const 组件 = React.memo(函数式组件)
案例
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
import React,{useState, useEffect} from 'react'

// 父组件
const Father:React.FC = ()=>{
const [num, setNum] = useState(0)
const [count, setCount] = useState(0)

return(
<>
<div>{num , count}</div>
<button onClick={()=>setNum(num += 1)}>+1</button>
<button onClick={()=>setCount(count += 1)}>+1</button>
<Son num={num}></Son>
</>
)
}

// 子组件 依赖于父组件props传递的num
// 使用React.memo() 包裹可以避免未修改到props数据子组件也会熏染问题
const Son:React.FC<{num:number}> = React.memo(({num})=>{
useEffect(()=>{
console.log('子组件触发')
})
return(
<>
<div>{num}</div>
</>
)
})

useMemo - 使用useMemo对计算的结果进行缓存 (计算属性)

当某个计算属性的值依赖于其他状态或属性,且这个计算开销较大时,可以使用useMemo来避免重复计算。它接受一个函数和依赖数组作为参数,并返回计算结果,只有依赖数组中的值发生变化时,才会重新计算结果。

基本用法
1
2
3
4
5
6
7
const squaredCount = useMemo(() => {  
return count;
}, [count]);
/*
第一个参数是一个回调函数,这个函数中包含了需要进行缓存计算的业务逻辑。在这个例子中,回调函数中计算了count的平方并返回结果。
第二个参数是一个依赖数组,这个数组中包含了所有会影响到计算结果的变量或状态。在这个例子中,我们将count作为依赖项传递,表示只有count发生变化时,才会触发重新计算。
*/
案例
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
import React, { useMemo, useState } from 'react';  

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

// 使用useMemo来缓存计算结果
// squaredCount 将得到函数的return结果
const squaredCount = useMemo(() => {
console.log('Calculating squared count');
return count ** 2;
}, [count]); // 当依赖数组变化时,函数才会重新执行
/*
不传数组:每次更新都会重新计算
空数组:仅在组件首次渲染时调用,不依赖于任何值
传递依赖数组:只有对应值发生变化才会重新执行
*/

return (
<div>
<p>Count: {count}</p>
<p>Squared Count: {squaredCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

export default App;

useCallback

useMemo hooks API 是对某个值的结果进行缓存,而useCallback 则是用来对组件内部的函数进行缓存,它返回的缓存的函数

基本用法
1
2
3
4
5
6
7
8
9
const memoizedCallback = useCallback(callback, dependencies);  
/*
useCallback会返回一个memorized回调函数供组件使用,从而防止组件每次rerender时反复创建相同函数,浪费内存开支
1. callback 是一个函数,用于处理业务逻辑,该函数就是需要被缓存的函数
2. dependencies:数组依赖项
不传:每次更新都会重新计算
为空数组:只有初始化时才会计算(计算一次)
传递依赖数组:只有依赖项变化时,才会重新计算
*/
案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { useCallback } from 'react';  

function MyComponent() {
const handleButtonClick = useCallback(() => {
// 处理按钮点击事件的逻辑
console.log('Button clicked!');
}, []);

return (
<div>
<button onClick={handleButtonClick}>Click me</button>
</div>
);
}
基于React.memo和useCallback实现搜索内容联想案例
1
2
3
4
5
6
7
8
9
10
11
12
// 返回数据
{
"code": 0,
"data": [
{id: 1, word: "a1"},
{id: 2, word: "a2"},
{id: 3, word: "a3"},
{id: 4, word: "a4"},
{id: 5, word: "a5"},
],
length:5
}
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
import React, { useState, useEffect, useCallback } from 'react';  

// 父组件
const Father: React.FC = () => {
// 定义列表数据状态
const [list, setList] = useState('');

// 当子组件 SearchInput 修改 input 的值时,更新列表数据
const onKwChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setList(e.currentTarget.value);
}, []);

return (
<div className="Father">
<SearchInput onChangeValue={onKwChange} />
<SearchList inputValue={list} />
</div>
);
};

// 搜索输入组件
type SearchInputType = {
onChangeValue: (e: React.ChangeEvent<HTMLInputElement>) => void;
};

const SearchInput: React.FC<SearchInputType> = React.memo(({ onChangeValue }) => {
// 输入框展示
return (
<input type="text" onInput={onChangeValue} />
);
});

// 单词列表联想组件
type wordType = { id: number; word: string };

const SearchList: React.FC<{ inputValue: string }> = ({ inputValue }) => {
// 定义列表数据状态和请求数据的副作用
const [list, setList] = useState<wordType[]>([]);

useEffect(() => {
if (!inputValue) return; // 如果输入值为空,不做请求
fetch('https://www.xxx.com/xxx?kw=' + inputValue)
.then(res => res.json())
.then(res => setList(res.data))
.catch(e => console.log(e));
}, [inputValue]);

// 渲染列表
return (
list.map((item) => <p key={item.id}>{item.word}</p>)
);
};

useTransition - React18 新特性

正常开发,如果一个组件渲染很慢,就会阻塞页面的其他事件交互。导致用使用体验差;useTransition 可以将一个更新转为低优先级更新,使其可以被打断,不阻塞UI对用户操作的响应,能够提高用户的使用体验。常用于视图切换时的用户体验

语法格式
1
2
3
4
5
6
7
8
9
10
11
import {useTransition} from 'react'

function fn(){
const [isPending, startTransition] = useTransition()
/*
不用传递任何参数
返回值:
isPending:布尔值,判断是否在待处理,如果true。说明页面上存在待渲染部分,可以给用户展示加载提示
startTransition:函数,调用此函数,可以把状态的更新标记为低优先级,不阻塞用户对页面的操作响应
*/
}
使用isPending显示加载中状态;使用startTransition该操作降低优先级
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
import React, { useState, useTransition } from 'react';  

const ExampleComponent = () => {
// 定义状态变量isVisible和setIsVisible,初始值为false
const [isVisible, setIsVisible] = useState(false);
// 使用useTransition钩子,设置过渡时长为2000毫秒
const [startTransition, isPending] = useTransition({
timeoutMs: 2000,
});

// 点击按钮触发的事件处理函数
const handleClick = () => {
// 开始状态过渡 把setIsVisible() 降低优先级
startTransition(() => {
// 点击按钮后切换isVisible状态
setIsVisible(!isVisible);
});
};

// 渲染组件
return (
<div>
<button onClick={handleClick}>
{isVisible ? 'Hide' : 'Show'}
</button>
{isPending ? <p>Loading...</p> : null} {/* 根据过渡状态显示加载状态 */}
{isVisible ? (
<div style={{ width: 200, height: 200, background: 'red' }}></div>
) : null}
</div>
);
};

export default ExampleComponent;
使用注意事项
  1. 传递给startTransition 的函数必须是同步的。React会立即执行此函数,并将在期间发生的所有状态标记为transition。 如果在执行期间,尝试稍后执行状态更新( 例如在一个定时器中执行状态更新),这些状态更新不会被标记为transition
  2. 标记transition的状态更新将被其他状态更新打断。例如transition中更新图表组件,并在图表组件任在重新渲染时继续在输入框中输入,React将首先处理输入框的更新,之后再重新启动对图表组件的渲染工作
  3. transition 更新不能用于控制文本输入 - 文本输入是一个需要即时响应的用户交互操作。如果把文本输入标记为低优先级,被标记为transition的状态更新将被其他状态更新打断。会导致输入状态丢失,数据丢失

useDeferredValue - React18 新特性

useDeferredValue 提供一个state的延迟版本,根据返回的延迟的state推迟更新UI的某一部分,从而达到性能优化的目的。常见的用法是结合 SuspenseuseTransition 来创建平滑的用户体验,比如在加载大量数据时保持页面的响应性。

语法格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React,{useState, useDeferredValue} from 'react'

export const fn:React.FC = ()=>{
const [kw, setKw] = useState('')
//得到延迟kw
const deferredKw = useDeferredValue(kw, { timeoutMs: 1000 })


//在使用 deferredKw 时,你可以通过比较原始值和延迟值来执行不同的逻辑
if (deferredValue === value) {
// 执行某些操作,比如渲染 UI,加载组件等
} else {
// 可以进行其它操作,或者显示 loading 图标等
}
}
/*
useDeferredValue会返回一个延迟版的状态
1. 在组件首次渲染期间,返回值将与传入的值相同
2. 在组件更新期间,React将首先使用旧值重新渲染UI结构,这能够跳过某些复杂抓紧的rerender,从而提高渲染效率。随后,React将使用新值更新deferredValue,并在后台使用新值重新渲染一个优先级的更新。如果后台使用新值更新时value再次改变,将打断那次更新
*/

Suspense组件

Suspense 是 React 的一个组件,用于在异步加载数据时显示加载动画或其他过渡效果。

它可以与以下功能一起使用:

  1. React.lazy: React.lazy 是一个函数,可让你使用动态导入(Dynamic Import)的方式来定义组件的懒加载。当组件被懒加载时,可以使用 Suspense 组件来显示加载过渡效果,在组件加载完成之前显示一个占位符。
  2. React.SuspenseList: React.SuspenseList 是一个用于包装多个 Suspense 组件的组件,它提供了更精细的控制和配置选项,以处理多个异步加载的场景。

Suspense 组件的基本用法

  1. 导入 Suspense 组件:
1
import React, { Suspense } from 'react';  
  1. 在需要异步加载的组件上方使用 Suspense 组件,并设置 fallback 属性作为加载过渡元素:
1
2
3
<Suspense fallback={<div>Loading...</div>}>  
{/* 异步加载的组件 */}
</Suspense>

fallback 属性用于指定一个占位符(例如加载动画,loading 文字等),在异步组件加载完成之前会显示在页面上。

  1. 异步加载组件的方式可以使用 React.lazy 结合动态导入的方式:
1
2
3
4
const MyComponent = React.lazy(() => import('./MyComponent'));  
/*
React.lazy 接受一个函数作为参数,该函数返回一个动态导入的 Promise。在组件被实际渲染之前,这个 Promise 会被解析并加载相应的组件。
*/
案例 - 表示内容过时

当kw的值频繁更新时,deferredKw的值会明显的滞后,此时用户在页面上看到的数据并不是最新的,为了防止用户感到困惑,我们可以给内容添加opacity透明度,表示内容过时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 按需导入 useDeferredValue 这个hooks api
import React,{useState, useDeferreValue} from 'react'

// 父组件
export const SearchBox:React.FC = ()=>{
cosnt [kw, setKw] = useState('')
// 给kw 创建延迟 deferredValue
const deferredValue = useDeferredValue(kw)

const onInputChange = (e: React.ChangeEvent<HtmlImputElement>) =>{
setKw(e.currentTarget.value)
}

return(
<div style={{height:500}}>
<input type="text" value={kw} onChange={onInputChange}></input>
<div style={{ opacity:kw !== deferredValue ? 0.3 : 1, transition: 'opacity 0.5s ease'}}>
<SearchResult query={deferredValue}></SearchResult>
</div>
</div>
)
}

自定义封装的hooks

封装鼠标位置

在src目录下新建 hooks/index.ts 模块,并将获取鼠标位置的代码封装成useMousePosition 的自定义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
// ./src/hooks/index.js
import {useState, useEffect} from 'react'

export const MouseInfo = ()=>{
// 定义节流延时器
let timerId: null | NodeJS.Timeout = null
// 记录鼠标位置
const [position, setPosition] = useState({x:0, y:0})

// 副作用函数
useEffect(()=>{
// 如果不存在延时器时,直接return
if(timerId !== null) return
//存在延时器,获取鼠标坐标
timerId = setTimeout(()=>{
const mouseMoveHandler = (e:MouseEvent)=>{
console.log({x:e.clientX, y:e.clientY})
setPosition({x:e.clientX, y:e.clientY})
timerId = null
}
},500)


//组件渲染完后添加鼠标监听事件
window.addEventListener('mousemove', mouseMoveHandler)

// 返回一个清理函数,在每次组件卸载时,解绑鼠标监听事件
return ()=>window.removeEventListener('mousemove', mouseMoveHandler)
}, [])

return position
}

在MouseInfo组件中,可以导入自己封装的kook进行使用

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, {useState, useEffect} from 'react'
import {useMousePosition} from '@/hooks/index.ts'

const MouseInfo:React.FC = ()=>{
// 调用自定义hook
const position = useMousePosiion()

return (
<>
<div>{position}</div>
</>
)
}
封装倒计时

功能分析:

  1. 点击一个button 调用useCountDown(5) 的hook,可以传递倒计时的秒数,如果未指定秒数则默认值为10秒,当倒计时未结束时,不能点击
  2. 每个1秒让秒数-1,并使用一个布尔值记录按钮是否被禁用
  3. 以数组的形式,向外返回每次的秒数和当前的禁用状态

在src/hooks/index.ts 模块中,封装名为 useCountDown自定义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
// ./src/hooks/index.ts
import {useState, useEffect} from 'react'

// 设置类型
type UseCountDown = (seconds:number)=>[number, boolean]

export const useCountDown:UseCountDown = (second:number=10)=>{
const [count, setCount] = useState(second)
// 默认button非禁用状态
const [disable, setDisabled] = useState(true)

useEffect(()=>{
// 设置定时器,每秒减1
const timerId = setTimeout(()=>{
if(count > 1){
setCount((val)=>val-1)
}else{ //秒数小于等于0时,终止行为
// 清除定时器
clearTimeout()
setDisabled(false)
}
},1000)

//清除函数
return ()=>clearTimeout(timerId)
}, [count])

return [count, disable]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react'
import {useCountDown} from '@/hooks/index.ts'

const CountDown:React.FC = ()=>{
// 返回[number, boolean]
const [count, disable] = useCountDown(3)

return(
<>
<button disable={disable} onClick={()=>console.log('协议生效')}>
{disable ? `请仔细阅读文档协议,请${count}秒再试` : 确认协议}
</button>
</>
)
}