React Hooks
# useState
让函数组件也可以有state状态, 并进行状态数据的读写操作
# 语法
const [xxx, setXxx] = React.useState(initValue)
# 参数
-initValue:第一次初始化指定的值在内部作缓存
-xxx:内部当前状态值
-setXxx:更新状态值的函数
# 实例
// setXxx(newValue): 参数为非函数值, 直接指定新的状态值, 内部用其覆盖原来的状态值
setCount(count + 1)
// setXxx(value => newValue): 参数为函数, 接收原本的状态值, 返回新的状态值, 内部用其覆盖原来的状态值
setCount(count => count + 1)
2
3
4
5
# 基础用法
function State(){
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
2
3
4
5
6
7
8
9
10
11
# 注意事项
每当状态变化时,会触发函数组件重新执行,从而根据最新的数据更新渲染DOM结构
export function Count(){ const [count,setCount] = useState(0); console.log("每次count值发生变化,都会打印"); const add = ()=>{ setCount(count +1) } }
1
2
3
4
5
6
7
8
9除了可以给定初始值,还可以通过函数返回值的形式,为状态赋初始值
export const DateCom = ()=>{ const [date,setDate] = useState(()=>{ const dt = new Date() return { year:dt.getFullYear(), month:dt.getMonth()+1, day:dt.getDate() } }) return { <> <Text>年份:{date.year}</Text> </> } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16usetate是异步变更状态,修改后无法立即拿到最新的状态
结合useEffect监听状态的变化,执行相对应的回调函数
useEffect(()=>{依赖变化时,触发的函数},[依赖项])
1更新不及时的bug,连续多次以相同更新状态值,如果值相同会屏蔽后续的更新行为,从而防止组件频繁渲染的问题
强调:当我们修改state状态的时候,如果我们发现:新值依赖于旧值(基于旧值进行计算,最终得到新值)
此时,不要直接在外部进行计算,而是要通过fn函数的形参拿到旧值,并进行计算,最终return新值
const [count,setCount] = useState(()=>0) const add = ()=>{ setCount(count + 1) setCount(count + 1) } // 改成 // 基于prev计算并return一个新值 const add = ()=>{ setCount((prev)=>prev + 1) setCount((prev)=>prev + 1) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15更新对象类型的值
set函数内部,会对更新前后的值进行对比
const [user,setUser] = useState({ name:'hhh', age:2 }) // 用新对象的引用类型替换旧对象的引用,即可正常触发组件的重新渲染 const changeUserInfo = ()=>{ user.name = 'aaa' // 1. setUser({...user}) // 2. setUser(Object.assign({},user)) }
1
2
3
4
5
6
7
8
9
10
11
12使用setState模拟组件的强制刷新
因为每次传入的对象地址不一样,所以会组件刷新
export const FnUpdate = ()=>{ const [,forceUpdate] = useState({}) // 每次调用onRefresh函数,都会给forceUpdate传递一个新对象,从而触发组件重新渲染 const onRefresh = ()=>forceUpdate({}) }
1
2
3
4
5
6
# useContext
用来处理多层级传递数据的方式
# 实例
使用 React Context API,在组件外部建立一个 Context
import React from 'react'; const ThemeContext = React.createContext(0); export default ThemeContext;
1
2
3使用 Context.Provider提供了一个 Context 对象,这个对象可以被子组件共享
import React, { useState } from 'react'; import ThemeContext from './ThemeContext'; import ContextComponent1 from './ContextComponent1'; function ContextPage () { const [count, setCount] = useState(1); return ( <div className="App"> <ThemeContext.Provider value={count}> <ContextComponent1 /> </ThemeContext.Provider> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } export default ContextPage;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19useContext()钩子函数用来引入 Context 对象,并且获取到它的值
// 子组件,在子组件中使用孙组件 import React from 'react'; import ContextComponent2 from './ContextComponent2'; function ContextComponent () { return ( <ContextComponent2 /> ); } export default ContextComponent; // 孙组件,在孙组件中使用 Context 对象值 import React, { useContext } from 'react'; import ThemeContext from './ThemeContext'; function ContextComponent () { const value = useContext(ThemeContext); return ( <div>useContext:{value}</div> ); } export default ContextComponent;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# useRef
可以在函数组件中存储/查找组件内的标签或任意其它数据
# 语法
const refContainer = useRef()
# 作用
保存标签对象,功能与
React.createRef()
一样获取DOM元素和子组件实例对象
存储渲染周期之间共享的数据
用于存储上一次旧count值,每当点击按钮触发count自增时,都把最新的旧值赋值
# 实例
获取DOM元素
function show() { alert(myRef.current.value) // 让文本框获取焦点 myRef.current?.focus } <input type="text" ref={myRef}/>
1
2
3
4
5
6
7存储渲染周期之间共享的数据
useRef
只有在组件首次渲染的时候被创建如果组件是重新渲染的时候,不会重复创建
ref
对象,因此可以保存旧值const [count,setCount] = useState(0) const prevCountRef = useRef<number>() const add = ()=>{ setCount((prev)=>prev + 1) prevCountRef.current = count }
1
2
3
4
5
6
7
# 注意事项
组件
rerender
时useRef
不会被重复初始化const [count,setCount] = useState(0) // rerender后刷新后不变化 const time = useRef(Date.now())
1
2
3ref.current
变化时不会造成组件rerender
const [count,setCount] = useState(0) // rerender后刷新后不变化 const time = useRef(Date.now()) const updateTime = ()=>{ time.current = Date.now() // 更新时间log执行,但是页面没有更新 console.log(time.current) } // 更换ref值后,没有执行 console.log('组件渲染了') return( <> <h3>{count},{time.current}</h3> </> )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17ref.current
不能作为其他Hooks的依赖项useEffect
会在组件首次渲染完毕之后,默认执行一次,组件每次渲染完毕之后,会触发useEffect中的回调函数,如果给了依赖项数组,则还要判断依赖项是否变化,再决定是否触发回调
# forwardRef
React一个API
在使用函数组件,无法直接使用ref
引用函数式组件
<Child ref={inputRef} />
因为默认情况下,自己组件不会暴露内部的DOM节点的ref
解决方法
使用React.forwardRef()函数式组件包装起来
// props用的时候,将_改成props
const Child = React.forwardRef((_,ref)=>{
// 导出的对象父组件才能拿到
useImperativeHandle(ref,()=>({
}))
})
2
3
4
5
6
7
8
# useImperativeHandle
以上解决方法是基本使用
基于
useImperativeHandle
按需向外暴露成员const [count,setCount] = useState(0) const Father = ()=>{ const onReset = ()=>{ childRef.current?.setCount(0) } const childRef = useRef<>() <Child ref={childRef} /> } const Child = React.forwardRef((_,ref)=>{ useImperativeHandle(ref,()=>({ count, setCount })) })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20控制成员暴露的粒度
const Child = React.forwardRef((_,ref)=>{ // 向外暴露count的值和reset函数 useImperativeHandle(ref,()=>({ count, // 在组件内部封装一个重置为0的函数,API的粒度更小 reset:()=>setCount(0) })) })
1
2
3
4
5
6
7
8
9useImperativeHandle
第三个参数空数组:子组件首次渲染时,执行useImperativeHandle中的反调,从而把return的对象作为父组件接收到的
ref
useImperativeHandle(ref,()=>({ count, // 在组件内部封装一个重置为0的函数,API的粒度更小 reset:()=>setCount(0), [] }))
1
2
3
4
5
6依赖项数组
# useEffect
除了返回值外对外界环境造成其他影响,即与组件渲染无关的操作。
可以把 useEffect Hook 看做如下三个函数的组合(componentDidMount()、componentDidUpdate()、componentWillUnmount())
React中的副作用操作:
- 发ajax请求数据获取
- 设置订阅 / 启动定时器
- 手动更改真实DOM
# 语法
useEffect(() => {
// 在此可以执行任何带副作用操作
return () => { // 在组件卸载前执行
// 在此做一些收尾工作, 比如清除定时器/取消订阅等
}
}, [stateValue]) // 如果指定的是[], 回调函数只会在第一次render()后执行,数组有值就(componentDidUpdate、componentWillUnmount)
2
3
4
5
6
# 实例
执行时机
没有为指定依赖项数组,会在函数组件每次渲染完成后执行
useEffect(()=>{ console.log('好好好好') })
1
2
3空数组
只会在组件首次渲染完成执行后唯一一次
React.useEffect(() => { let timer = setInterval(() => { setCount(count => count + 1) },1000) return ()=>{ console.log(1) clearInterval(timer) } },[]) function unmount() { root.unmount() } <button onClick={unmount}>点我卸载组件</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15依赖项
每次渲染完毕后,判断依赖项是否变化,再决定是否执行副作用函数
清理副作用
清理网络请求
清理事件绑定
useEffect(()=>{ //1.执行副作用操作 //2.返回一个清理副作用的函数 return()=>{ // 在这里执行自己的清理操作 } },[依赖项])
1
2
3
4
5
6
7
# 注意事项
- 会在组件首次渲染完毕之后,默认执行一次
- 不要在useEffect中改变依赖项的值,会造成死循环
- 多个不同功能的副作用尽量分开声明,不要写到一个useEffect中
# useReducer
比 useState 更适用的场景:例如 state 逻辑处理较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等
# useMemo
缓存值
当父组件重新渲染时,子组件也会重新渲染,即使子组件的 props 和 state 都没有改变
// 父组件
import {useState} from 'react'
function IndexPage(){
const [value,setValue] = useState('')
return(
<>
<div>
<input value={value} onChange={(ev)=>setValue(ev.target.value)}/>
</div>
</>
)
}
// 子组件
import {memo} from 'react'
const Child = ({count}) => {
const show = () => {
console.log('子组件渲染')
}
return (
<>
<h3>Child组件</h3>
<div>{count}</div>
</>
)
}
export default memo(Child)
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
# useCallback
缓存函数
假设需要将事件传给子组件,当点击父组件按钮时,发现控制台会打印出子组件被渲染的信息,说明子组件又被重新渲染了
// 父组件
import {useState,useCallback} from 'react'
function IndexPage(){
const [value,setValue] = useState('')
// [] 执行一次
// [count] 每次改变count都执行一次
const updateCount = useCallback(()=>console.log('父业务'),[])
return(
<>
<div>
<input value={value} onChange={(ev)=>setValue(ev.target.value)}/>
<Child updateCount = {updateCount} />
</div>
</>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# useImperativeHandle
# useLayoutEffect
# useTransition
# 语法
import {useTransiton} from 'react'
function TabContainer(){
const [isPending,startTransiton] = useTransiton()
}
2
3
4
5
# 参数
调用useTransiton时不需要传递任何参数
# 返回值
isPending
:是否存在待处理的transition
,如果为true,说明页面上存在待渲染的部分startTransiton
:调用此函数,可以把状态的更新标记为低优先级,不阻塞UI对用户的操作响应
# 解决问题
export const Tabs = ()=>{
const [active,setActive] = useStste('home')
// 新增
const [isPending,startTransition] = useTransition()
const click = (name)=>{
// 把某次更新标记,标记为低优先级,从而防止页面卡顿情况
startTransition(()=>{
setActive(name)
})
}
return (
<>
<View onClick={()=>click('home')}>home</View>
<View onClick={()=>click('movie')}>movie</View>
<View onClick={()=>click('about')}>about</View>
</>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
使用isPending
展示加载状态
使用
useTransition
期间,接收isPending
参数const [isPending,startTransition] = useTransition()
1将标签页的渲染,抽离到
renderTabs
函数中const renderTabs = ()=>{ if(isPending) return <h3>Loading...</h3> switch(activeTab){ case'home': return <Home/> case'movice': return <Movie/> case'about': return <About/> default: return <Home/> } }
1
2
3
4
5
6
7
8
9
10
11
12
13调用
renderTabs
函数,渲染标签页到组件中{renderTabs()}
1
# 完整案例
export const Tabs = ()=>{
const [active,setActive] = useStste('home')
// 新增
const [isPending,startTransition] = useTransition()
const click = (name)=>{
// 把某次更新标记,标记为低优先级,从而防止页面卡顿情况
startTransition(()=>{
setActive(name)
})
}
const renderTabs = ()=>{
if(isPending) return <h3>Loading...</h3>
switch(activeTab){
case'home':
return <Home/>
case'movice':
return <Movie/>
case'about':
return <About/>
default:
return <Home/>
}
}
return (
<>
<View onClick={()=>click('home')}>home</View>
<View onClick={()=>click('movie')}>movie</View>
<View onClick={()=>click('about')}>about</View>
{renderTabs()}
</>
)
}
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
# 注意事项
- 传递给
startTransition
的函数必须是同步的,React会立即执行此函数,并将在其执行期间发生的所有状态更新标记为transition
。如果在其执行期间,尝试稍后执行状态更新(例如在一个定时器中执行状态更新)。这些状态更新不会被标记为trainsition
- 标记为transition的状态更新将被其他状态更新打断。例如,在transition中更新图表组件,并在图表组件仍在重新渲染时继续输入框输入,react将首先处理输入框的更新。之后再重新启动对图表组件的渲染工作
- transition更新不能用于控制文本输入
# useDeferredValue
提供一个state的延迟版本,根据返回的延迟的state能够推迟更新UI中某一部分,从而达到性能优化
function Search(){
const [kw,setKw] = useState('')
const deferred = useDeferredValue(kw)
}
2
3
4
useDeferredValue
返回值是一个延迟班的状态
在组件首次渲染期间,返回值与传入的值相同
在组件更新期间,react将首先使用旧值重新渲染UI结构,能够跳过某些复杂组件的rerender,从而提高渲染效率
随后,react将使用新值更新
useDeferredValue
, 并在后台使用新值重新渲染的一个低优先级的更新。意味着后台使用新值更新时value再次改变,它将打断那次更新
# 案例
function Search(){
const [kw,setKw] = useState('')
const deferred = useDeferredValue(kw)
const onInput = (e)=>{
setKw(e.currentTarget.value)
}
return {
<>
<input onChange={onInput} value={} />
<SearchResult query={deferred}/>
</>
}
}
// 子组件 当props没有变化时会跳过子组件的rerender
const SearchResult:React.FC<{query:string}>=React.memo((props)=>{
if(!props.query) return
const items = Array(4000).fill(props.query)
.map((item,i)=><p key={i}>{item}<p/>)
return items
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25