一、React Hooks
1.1 什么是Hooks
- 在
React中,useState以及任何其他以use开头的函数都被称为Hook,所以Hooks就是代表use函数的集合,也就是钩子的集合 Hooks其实就是一堆功能函数,一个组件想要实现哪些功能,就可以引入对应的钩子函数,像插件一样非常方便Hooks分为:内置Hooks,自定义Hooks,第三方Hooks
ReactHooks使用规则
- 只能在组件中或者其他自定义Hook函数中调用
- 只能在组件的顶层调用,不能嵌套在if、for、其他函数中
1.2 useRef
useRef 是一个 React Hook,它能帮助引用一个不需要渲染的值
1.2.1 用ref引用一个值做记忆功能
ref 和 state的区别
- 下面注释掉的代码,在连续点击按钮时,会开启多个定时器,原因是:每次setCount,会重新触发渲染,相当于每次的timer都为空,自然清除不了定时器
- 修改:使用
useRef给timer做记忆功能
import { useRef,useState } from 'react'
function App() {
const [count, setCount] = useState(0)
// let timer = null
const timer = useRef(null)
const handleClick = () => {
setCount(count + 1)
// clearInterval(timer)
clearInterval(timer.current)
// timer =
timer.current = setInterval(() => {
console.log(123)
}, 1000)
}
return (
<div>
<button onClick={handleClick}>计数</button>
<div>{count}</div>
</div>
)
}
export default App
1.2.2 useRef可通过ref操作DOM
React会自动更新DOM来匹配你的渲染输出,通常情况下不需要操作DOM- 但是,有时你可能需要访问由
React管理的DOM元素(例如:让一个节点获得焦点,滚动到它或测量它的尺寸和位置)
import { useRef } from 'react'
function App() {
const h2Ref = useRef()
const handleCLick = () => {
// 通过ref操作原生DOM
h2Ref.current.style.color = 'red'
}
return (
<div>
<button onClick={handleCLick}>点击变色</button>
<h2 ref={h2Ref}>2</h2>
</div>
)
}
export default App
- 注意:下面所示代码
eslint会报错,因为每次循环都会创建一个新的myRef引用,导致多个元素引用相同的myRef
- 在循环中操作
ref可以使用回调函数的写法,如下所示
import { useRef } from 'react'
function App() {
const list = [
{ id: 1, text: 'aaa' },
{ id: 2, text: 'bbb' },
{ id: 3, text: 'ccc' },
]
return (
<div>
<ul>
{list.map((item) => {
return (
<li
key={item.id}
ref={(myRef) => { myRef.style.background = 'red' }}>
{item.text}
</li>
)
})}
</ul>
</div>
)
}
export default App
1.2.3 组件设置ref需要forwardRef进行转发
当组件添加ref属性的时候,需要forwardRef进行转发,forwardRef让您的组件通过ref向父组件公开DOM节点
import { forwardRef, useRef } from 'react'
// forwardRef转发
const MyInput = forwardRef(function MyInput(props, ref) {
return (
<div>
<input type='text' ref={ref} />
</div>
)
})
function App() {
const ref = useRef(null)
const handleClick = () => {
ref.current.style.background = 'red'
}
return (
<div>
<div>App组件</div>
<button onClick={handleClick}>点击</button>
<MyInput ref={ref}></MyInput>
</div>
)
}
export default App
1.3 useImperativeHandle
useImperativeHandle 是 React 中的一个 Hook,它能让你自定义由ref暴露出来的句柄(这样就不会暴露出整个DOM 节点)
import { forwardRef, useImperativeHandle, useRef } from 'react'
// forwardRef转发
const MyInput = forwardRef(function MyInput(props, ref) {
const inputRef = useRef(null)
useImperativeHandle(ref, () => {
return {
// 自定义方法
focusAndStyle() {
inputRef.current.focus()
inputRef.current.style.background = 'red'
},
}
})
return (
<div>
<input type='text' ref={inputRef} />
</div>
)
})
function App() {
const ref = useRef(null)
const handleClick = () => {
ref.current.focusAndStyle()
// 使用 useImperativeHandle选择暴露的句柄后,下面代码便会报错了
// ref.current.style.background = 'red'
}
return (
<div>
<button onClick={handleClick}>点击获取焦点并变色</button>
<MyInput ref={ref}></MyInput>
</div>
)
}
export default App
1.4 useEffect
useEffect 是一个 React Hook,它允许你将组件与外部系统同步
1.4.1 useEffect基本使用
纯函数
- 只负责自己的任务,不会更改在该函数调用前就已存在的对象或变量
- 输入相同,则输出相同。给定相同的输入,纯函数总数返回相同的结果
副作用
- 函数在执行过程中对外部造成的影响。例如:Ajax调用、DOM操作、与外部系统同步等
- 在React组件中,事件操作是可以处理副作用的,但有时候需要初始化处理副作用,就需要
useEffect钩子
注意:初始渲染和更新渲染,都会触发useEffect(),它在当前函数组件作用域的最后时机触发
import { useRef,useEffect } from 'react'
function App() {
const inputRef = useRef(null)
// 副作用,符合纯函数的规范,因为事件可以处理副作用
const handleClick = () => {
inputRef.current.focus()
}
// 副作用,不符合纯函数的规范
setTimeout(() => {
inputRef.current.focus()
}, 1000)
// 可以在初始的时候进行副作用操作
// useEffect触发时机:JSX渲染后触发
useEffect(() => {
inputRef.current.focus()
})
return (
<div>
<button onClick={handleClick}>点击获取焦点</button>
<br />
<input type='text' ref={inputRef} />
</div>
)
}
export default App
1.4.2 useEffect依赖项
useEffect的第二个参数为依赖项数组
- 不传递依赖项数组:
Effect会在组件的每次单独渲染(和重新渲染)之后运行 - 传递依赖项数组:如果指定了依赖项,
Effect在初始渲染后以及依赖项变更的重新渲染后运行 - 传递空依赖项数组:如果
Effect没有使用任何响应式值,则它仅在初始渲染后运行,有响应值则eslint报错 - 注意:
eslint会检测依赖项是否正确,包括props,state,计算变量等
import { useEffect, useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const [msg, setMsg] = useState('你好')
// 1.不传递数组,每次初次渲染和点击按钮后,都会打印一次
// 2.传递数组
useEffect(() => {
console.log('A打印一次')
}, [count])
// 初次渲染时打印,更新时Object.js()做比较,依赖项改变才会触发
// 这里点击按钮,只改变了count,因此不会打印C
useEffect(() => {
console.log('C打印一次')
}, [msg])
// 3.传递空数组,只在初次渲染打印,若其中使用了state,eslint会报错
useEffect(() => {
console.log('B打印一次')
console.log(count) // 报错
}, [])
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick}>点击</button>
<div>{count}</div>
</div>
)
}
export default App
1.4.3 useEffect中定义函数
- 函数也可以成为计算变量,所以也要作为依赖项,虽然可以利用依赖项加
useCallBack来解决,但是很麻烦 - 解决方式:把函数定义在
useEffect内部
import { useEffect, useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const [msg, setMsg] = useState('')
const foo = () => {
console.log(count)
}
// 因为Object.is(function(){},function(){} -> false)
// 就算只修改msg,还是会执行useEffect中的foo()
useEffect(() => {
foo()
}, [foo])
// 解决方式,把函数定义在useEffect内部
useEffect(() => {
const foo = () => {
console.log(count)
}
foo()
}, [count])
return (
<div>
<div>{count}</div>
</div>
)
}
export default App
1.4.4 useEffect的清理操作
- 当卸载组件或更新组件的时候,可以通过
useEffect来实现一些清理工作 - 严格模式下,会检测useEffect是否实现了清理操作
- 初始化数据时,要注意清理操作,更简洁的方法是使用第三方,例如
ahooks中的useRequest
import { useEffect, useState } from 'react'
// 初始打印进入,点击按钮打印退出
function Chat() {
useEffect(() => {
console.log('进入')
// useEffect的清理工作,卸载和更新的时候都会触发
return () => {
console.log('退出')
}
})
return (
<div>
<div>hello chat</div>
</div>
)
}
function App() {
const [show, setShow] = useState(true)
const handleClick = () => {
setShow(false)
}
return (
<div>
<div>hello app</div>
<button onClick={handleClick}>关闭聊天室</button>
{show && <Chat></Chat>}
</div>
)
}
export default App
1.5 useLayoutEffect
useLayoutEffect 是 useEffect的一个版本,在浏览器重新绘制屏幕之前触发
与useEffect的区别
useEffect():dom修改前调用,微任务调用,页面渲染之后执行useLayoutEffect():dom修改后调用,同步调用,页面渲染前执行
大部分情况用useEffect(),性能更好,但当你的useEffect里面的操作需要处理dom,并且会改变页面样式,就需要使用useLayoutEffect,否则会出现闪屏问题
import { useEffect, useLayoutEffect, useState } from 'react'
function App() {
const [msg, setMsg] = useState('你好')
// 会出现闪屏问题,useEffect为异步
useEffect(() => {
for (let i = 0; i < 100000000; i++) {}
setMsg('你好啊')
}, [msg])
// 不会出现闪屏问题,useLayoutEffect为同步
useLayoutEffect(() => {
for (let i = 0; i < 100000000; i++) {}
setMsg('你好啊')
}, [msg])
return (
<div>
<h2>{msg}</h2>
</div>
)
}
export default App
1.6 useReducer
-
对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序令人不知所措。
-
对于这种情况,你可以将组件的所有状态更新逻辑整合到一个外部函数中,这个函数叫做
reducer -
Reducer是处理状态的另一种方式
import { useState, useEffect, useReducer } from 'react'
// state表示状态,action表示信号,根据不同的信号,就可以针对性地修改状态
// reducer 是管理员的意思,要修改状态必须通过reducer
// 管理员根据信号进行状态的修改
// 修改状态的流程:1)对于state进行深copy 2)修改更新state 3)返回修改后的state
const reducer = (state, action) => {
let newState = JSON.parse(JSON.stringify(state)) // 1)对于state进行深copy
// 根据信号更新state
switch (
action.type // action是一个对象,对象中有一个type,不同的type表示不同的信号
) {
case 'NUM_ADD':
newState.num += 1
break
case 'NUM_SUB':
newState.num -= 1
break
}
return newState
}
// 定义初始值
const initState = {
num: 1,
list: ['a', 'b'],
falg: true,
}
const A = (props) => {
// dispatch 是用来派发一个action,管理员就可以收到action
const [state, dispatch] = useReducer(reducer, initState)
return (
<div>
<h2>函数组件</h2>
<h3>{state.num}</h3>
<h3>{state.list}</h3>
<button onClick={() => dispatch({ type: 'NUM_ADD' })}>+1</button>
<button onClick={() => dispatch({ type: 'NUM_SUB' })}>-1</button>
</div>
)
}
export default A
1.7 useContext
-
在react函数式组件中,如果组件的嵌套层级很深,当父组件想把数据共享给最深的子组件时,传统的方法是使用
props,一层层向下传递 -
但是
props层层传递数据的维护性太差,可以使用createContext() + useContext实现多层组件的数据传递
import { createContext, useContext, useState } from 'react'
// 参数:是默认值
const Context = createContext(0)
function App() {
const [count, setCount] = useState(123)
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick}>点击+1</button>
<div>我是爷爷</div>
<Context.Provider value={count}>
<Father></Father>
</Context.Provider>
</div>
)
}
function Father() {
return (
<div>
<div>我是爸爸</div>
<Son></Son>
</div>
)
}
function Son() {
const value = useContext(Context)
return (
<div>
<div>我是儿子</div>
<div>这是爷爷的数据{value}</div>
</div>
)
}
export default App
1.8 Reducer配合Context实现共享状态管理
-
Reducer可以整合组件的状态更新逻辑。Context可以将信息深入传递给其他组件。组合使用二者可以实现管理一个复杂页面的状态 -
更复杂的状态管理,可以采用第三方库:Redux、Mobx、Zustand等
1.9 useMemo
useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。
import { memo, useMemo, useState } from 'react'
const Head = memo(function Head() {
return <div>hello Head,{Math.random()}</div>
})
function App() {
const [count, setCount] = useState(0)
const [msg, setMsg] = useState('hello react')
// const list = [msg.toLowerCase(), msg.toUpperCase()]
// 缓存后,点击不会更新随机数
const list = useMemo(() => [msg.toLowerCase(), msg.toUpperCase()], [msg])
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick}>点击</button>
<Head list={list}></Head>
</div>
)
}
export default App
1.10 useCallback
- 用于性能优化,缓存一个函数声明
useCallback(fn,dps)相当于useMemo(()=>fn,deps)
// 二者效果相同
const fn = useMemo(()=>()=>{console.log(msg)},[msg])
const fn = useCallback(()=>{console.log(msg)},[msg])
1.11 useTransition
- React18之前,渲染是一个单一的、不间断的、同步的事务,一旦渲染开始,就不能被中断
- React18引入并发模式,它允许你将标记更新作为一个transitions,这会告诉React它们可以被中断执行。这样可以把紧急的任务先更新,不紧急的任务后更新
useTransition可以让你在不阻塞UI的情况下更新状态,返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数
1.12 useDeferredValue
接受一个值,并返回该值的新副本,该副本将延迟到更紧急的更新之后
1.13 useId
useId是一个React Hook,可以生成传递给无障碍属性的唯一ID
1.14 自定义Hook函数
自定义Hook是以use打头的函数通过自定义Hook函数可以实现逻辑的封装和复用
封装自定义Hook的通用思路
- 声明一个use开头的函数
- 在函数体中封装可复用的逻辑
- 把函数中用到的状态或者回调return出去(以对象或数组)
- 在哪个组件中用到这个逻辑,就执行这个函数,解构出来状态和回调进行使用
import { useState } from 'react'
// 自定义hook——点击切换
function useToggle() {
const [show, setShow] = useState(true)
const handleClick = () => {
setShow(!show)
}
return {
show,
handleClick,
}
}
function App() {
const { show, handleClick } = useToggle()
return (
<div>
{show && <div>哈啊哈</div>}
<button onClick={handleClick}>toggle</button>
</div>
)
}
export default App