前言
React Hooks 是React 16.8版本新增的特性,让函数组件(Functional Component)也可以使用 state 以及其他的 React 特性。函数式组件有以下优势:
- 让函数组件可以使用state、生命周期方法等React特性
- 逻辑更清晰,相关逻辑可以归于一个hook中
- 代码可复用性高,自定义Hook可以抽离共享逻辑
- 更易于测试,函数纯函数容易测试
注意点:
- 只能在函数组件和自定义Hook中调用Hook
- 不能在循环、条件或嵌套函数中调用Hook
- 只能在React函数的最顶层调用Hook
总之,React Hooks让函数组件具有类组件的功能,使代码逻辑更清晰,复用性更强,是函数式编程在React中的重要体现。在所有的hooks中有一些常用的hooks也有一些不是很常用的,这篇文章我们都会逐一讲解。
常用hooks
有以下hooks是在项目中很常用的,它们分别是:useState,useEffect,useLayoutEffect,useRef,useCallback,useMemo,useContext,useReducer
useState
useState是React Hooks中最常用的一个,它用来声明状态变量,它会返回一个数组,数组第一项是当前的状态值,第二项是设置新状态的函数,当useState声明的变量发生改变时,会触发组件的重新渲染,可以在回调函数中获取更新后的state,也会在xml更新最新的值。设置状态变量有两种方式,一种是直接赋值,一种是回调函数的形式
直接赋值更新状态变量
直接赋值方式就是在setState中直接传入新的值:setState(value)
typescript
function Demo01() {
const [count, setCount] = useState(0)
const handleSetCount = () => {
setCount(count + 1)
}
return (
<div>
<h2>demo01 number is {count}</h2>
<button onClick={handleSetCount}>Add</button>
</div>
)
}
回调函数更新状态变量
setState修改值还可以传入一个回调函数,回调函数的参数就是对应的变量值,参数的变量值是当前的state值,最后回调函数返回的值就是需要设置的值,例如下面的例子,currentCount就是当前的count的值,最后return的值就是需要更新的值
typescript
function Demo01() {
const [count, setCount] = useState(0)
const handleSetCount = () => {
setCount((currentCount) => {
return currentCount + 2
})
}
return (
<div>
<h2>demo01 number is {count}</h2>
<button onClick={handleSetCount}>Add</button>
</div>
)
}
useEffect
useEffect用于处理副作用操作。副作用操作是指与组件渲染无关的操作,例如数据获取、订阅事件、手动操作 DOM 等。在函数组件中,由于没有生命周期方法,我们需要使用 useEffect 来处理这些副作用。useEffect 可以看作是类组件中 componentDidMount、componentDidUpdate 和 componentWillUnmount 这三个生命周期方法的组合。它在组件渲染后执行(类似于 componentDidMount),并且在每次组件更新后也会执行(类似于 componentDidUpdate),同时还会在组件卸载时执行(类似于 componentWillUnmount)。
useEffect基本使用
useEffect 接受两个参数:一个是副作用操作函数,另一个是依赖数组。副作用操作函数是一个回调函数,它会在组件渲染后执行,以及在组件更新后执行。依赖数组用于指定副作用操作的触发条件。
- 如果依赖数组为空 [],副作用操作只在组件挂载和卸载时执行,不依赖任何变量。
- 如果不传递依赖数组,副作用操作在每次组件更新后都会执行。
- 如果传递了特定变量数组,副作用操作只在特定变量发生改变时执行。
typescript
import React, { useEffect } from 'react'
function MyComponent() {
// 在 useEffect 中定义副作用操作
useEffect(() => {
// 副作用操作,可以进行数据获取、订阅事件、操作 DOM 等
// ...
// 返回一个清理函数(可选),用于在组件卸载时执行清理操作
return () => {
// 清理操作,例如取消订阅、清除定时器等
// ...
}
}, []) // 依赖数组(可选),用于控制副作用的触发条件
// 空数组表示只在组件挂载和卸载时执行副作用,不依赖任何变量
// 不传递依赖数组表示在每次组件更新后都执行副作用
// 传递特定变量数组表示只在特定变量发生改变时执行副作用
}
useEffect案例解析
typescript
function Demo01() {
const [count, setCount] = useState(0)
// 如果不传第二个参数,那么useEffect会在每次渲染之后都执行
useEffect(() => {
console.log('I will be called after every render')
})
// 如果传了一个空数组,那么useEffect只会在第一次渲染之后执行
useEffect(() => {
console.log('I will be called after the first render')
}, [])
// 如果传了一个数组,那么useEffect会在第一次渲染之后和数组中的值发生变化之后执行
useEffect(() => {
console.log('I will be called after every render and count is changed')
}, [count])
// 当第一次渲染之后,count发生变化时,会执行useEffect中的订阅函数
// 当组件卸载时,会执行useEffect中的取消订阅函数,这样就不会造成内存泄漏
// 当count发生变化时重新订阅,保证了订阅函数中的逻辑是最新的
useEffect(() => {
const handleAddCount = () => {
setCount(count + 1)
}
document.addEventListener('click', handleAddCount)
return () => {
document.removeEventListener('click', handleAddCount)
}
}, [count])
return (
<div>
<p>You clicked {count} times</p>
</div>
)
}
useLayoutEffect
useLayoutEffect和useEffect用法上完全相同,只是触发时机不同,因此有着不同的使用场景
useLayoutEffect和useEffect具体区别及整个react的render过程,请查阅文章React渲染(Render)全过程解析,在这我大概概括一下,useLayoutEffect是在浏览器绘制页面之前同步调用的hook函数,此时react已经生成了等待绘制的真实dom,此时可在useLayoutEffect中获取到最新的dom元素,这也是它的一般用法,例如下面这个案例:
tsx
function Demo01() {
const [isShowMenu, setIsShowMenu] = useState(false)
const [isShowTopClass, setIsShowTopClass] = useState(false)
useLayoutEffect(() => {
// 获取dom计算是否触底
// 、、、、
// 计算结果发现触底了,在顶部显示
setIsShowTopClass(true)
}, [isShowMenu])
return (
<div>
<h1>Demo01 Page</h1>
<Button type="primary" onClick={() => setIsShowMenu(true)}>
Add
</Button>
{isShowMenu && (
<div className={isShowTopClass ? 'top-class' : 'bottom-class'}>
Menu Content
</div>
)}
</div>
)
}
该案例实现了在一个列表中点击一个icon,在该icon底部会出现一个更多菜单,但是要求如果该菜单弹窗后超过了页面底部,需要从上面弹出的需求(上述只是伪代码,具体代码可按这个逻辑实现)
useRef
useRef 允许你在函数组件中保存和访问可变的数据,而且这些数据的修改不会引发组件重新渲染。useRef 返回一个可变的 ref 对象,它在组件的整个生命周期内保持不变。通俗来讲useRef可以保存一个全局变量(组件内使用),该变量变化时不会重新渲染组件,通常用作保存非状态的全局变量,读取和设置值都是操作useRef 返回对象的current属性,下面是useRef常用的几个案例
使用useRef保存dom元素
我们使用useRef保存一个列表dom,并且在组件加载时获取list的宽高并打印出来
tsx
function Demo01() {
const listRef = React.useRef<HTMLDivElement>(null)
useLayoutEffect(() => {
const listHeight = listRef.current?.clientHeight
const listWidth = listRef.current?.clientWidth
console.log(listHeight, listWidth)
}, [])
return (
<div className="list" ref={listRef}>
<p>This is Demo01 Page</p>
</div>
)
}
使用useRef保存普通的全局变量
我们使用useRef保存一个普通的全局变量count,当点击事件触发时,时count加一,每次点击button,只有console.log(countRef.current, '111')会被打印,因为修改useRef的值,组件不会重新渲染。
tsx
function Demo01() {
const countRef = React.useRef(0)
const buttonClick = () => {
countRef.current += 1
console.log(countRef.current, '111')
}
// 点击按钮,countRef.current的值会变化,但是页面不会重新渲染,因此这里不会打印出来
console.log(countRef.current, '222')
return (
<div>
<p>This is Demo01 Page</p>
<button onClick={buttonClick}>Add</button>
</div>
)
}
使用useRef保存定时器
我们使用useRef保存一个定时器,组件加载时开始运行,每秒打印出一个timer,组件卸载时将其清楚
tsx
function Demo01() {
// 1. 使用useRef保存定时器
const timer = React.useRef<NodeJS.Timeout>()
useEffect(() => {
timer.current = setInterval(() => {
console.log('timer')
}, 1000)
return () => {
clearInterval(timer.current)
}
}, [])
return (
<div>
<p>This is Demo01 Page</p>
</div>
)
}
useCallback
useCallback定义
useCallback 用于优化函数组件的性能。它的作用是缓存函数的引用,避免在每次渲染时重新创建新的函数实例,从而减少不必要的重新渲染。
因为JavaScript 中的函数是一种引用类型,每次定义一个函数时,都会创建一个新的函数对象。当函数组件被调用时(也就是组件渲染时),组件中的所有函数都会被重新定义。这意味着,每次组件重新渲染时,函数的引用都会发生变化。
测试组件中函数是否每次渲染都会重新定义
我们可以做如下测试,定义一个测试函数,并且用一个副作用函数useEffect依赖它,测试一下我们是不是修改任何一个state值,该useEffect都会执行
tsx
function Demo01() {
const [count, setCount] = useState(0)
const [number, setNumber] = useState(0)
const testFun = () => {
console.log('testFun')
}
useEffect(() => {
console.log('useEffect is running')
}, [testFun])
return (
<div>
<p>This is Demo01 Page</p>
<button onClick={() => setCount(count + 1)}>AddCount</button>
<button onClick={() => setNumber(number + 1)}>AddNumber</button>
</div>
)
}
经过测试,我们发现不管我们点击哪个按钮,都会在控制台打印出useEffect is running,说明每次修改state值,都会重新生产一个新的函数。
使用useCallback优化函数每次都被重新定义的问题
为了避免函数每次渲染都被重新定义引起不必要的开销,我们可以将函数使用useCallback返回,useCallback使用方法很简单,它接受两个参数,第一个参数就是我们需要定义的函数本身,第二个参数就是依赖项,和useEffect一样,只有依赖项中的状态发生改变,函数才会被重新定义
- 没有依赖项的函数,只需要组件创建时定义就好,组件这样改造之后,不管你如何点击按钮修改state,都不会触发useEffect is running,只有组件加载的时候触发一次。
tsx
function Demo01() {
const [count, setCount] = useState(0)
const [number, setNumber] = useState(0)
const testFun = useCallback(() => {
console.log('testFun')
}, [])
useEffect(() => {
console.log('useEffect is running')
}, [testFun])
return (
<div>
<p>This is Demo01 Page</p>
<button onClick={() => setCount(count + 1)}>AddCount</button>
<button onClick={() => setNumber(number + 1)}>AddNumber</button>
</div>
)
}
- 存在依赖项的函数,当一个函数中存在状态变量时,我们必须把该状态放入依赖数组,这样才能保证当函数中引用的状态发生改变时,及时重新定义函数,函数中引用的状态值才能是最新的值。例如下面案例:
tsx
function Demo01() {
const [count, setCount] = useState(0)
const testFun = useCallback(() => {
// 这里任何时候打印的count都是0
console.log('testFun', count)
}, [])
useEffect(() => {
console.log('useEffect is running')
}, [testFun])
return (
<div>
<p>This is Demo01 Page</p>
<button onClick={() => setCount(count + 1)}>AddCount</button>
<button onClick={() => testFun()}>testFun</button>
</div>
)
}
当我们点击AddCount按钮将count加到3时,再点击testFun按钮执行testFun函数,此时打印出来的结果就是testFun 0,因为该函数没有依赖任何状态,它只有组件加载时定义了一次,函数定义时count为0,因此它任何时候调用打印的结果都是0.下面我们将依赖加上再做测试
tsx
function Demo01() {
const [count, setCount] = useState(0)
const testFun = useCallback(() => {
// 这里打印的count都是最新值
console.log('testFun', count)
}, [count])
useEffect(() => {
console.log('useEffect is running')
}, [testFun])
return (
<div>
<p>This is Demo01 Page</p>
<button onClick={() => setCount(count + 1)}>AddCount</button>
<button onClick={() => testFun()}>testFun</button>
</div>
)
}
此时不管任何时候点击testFun打印出来的count都是最新的值。
useMemo
useMemo定义
useMemo用于在函数组件中进行性能优化。**它的作用是缓存计算结果,**避免在每次组件渲染时重新计算,从而提高组件的性能。
在 React 组件中,有些计算可能是耗时的,例如复杂的计算、昂贵的数据处理或是从 API 获取数据等。如果这些计算在每次组件渲染时都重新执行,会增加不必要的开销,导致组件渲染变慢。
使用 useMemo 可以解决这个问题,它接受两个参数:一个计算函数和一个依赖项数组。useMemo 会在组件渲染时执行计算函数,并将计算结果缓存起来。在下一次组件渲染时,如果依赖项数组中的值没有发生变化,useMemo 将直接返回缓存的计算结果,避免重复计算。
useMemo有两种用法,一种是返回一个函数,一种是返回一个计算结果。当返回一个函数时,其效果与useCallback完全相同,可以避免函数被重复定义。当返回一个计算结果时,类似于vue中的计算属性computed,计算并返回一个依赖多个状态的值,只有当依赖的状态改变时才会被重新计算。下面我们分别介绍一下这两种用法
useMemo返回一个函数
上面说了,当useMemo返回一个函数时,与useCallback效果完全相同,写法上的区别就是useCallback第一个参数就是我们需要定义的函数,而useMemo第一个参数是个回调函数,其返回值应该是我们需要定义的函数。例如改造上面的count案例如下
tsx
function Demo01() {
const [count, setCount] = useState(0)
const testFun = useMemo(() => {
return () => {
console.log('testFun', count)
}
}, [count])
useEffect(() => {
console.log('useEffect is running')
}, [testFun])
return (
<div>
<p>This is Demo01 Page</p>
<button onClick={() => setCount(count + 1)}>AddCount</button>
<button onClick={() => testFun()}>testFun</button>
</div>
)
}
useMemo返回一个计算结果
我们上面也说了,当useMemo返回一个计算结果时,类似于vue的计算属性,其作用就是缓存一个依赖状态的复杂计算结果。例如下面的案例,我们有个状态是个时间戳,和一个时间格式化类型(12小时制,24小时制),我们需要根据这两个状态格式化时间后显示在页面上,下面是使用和不使用useMemo的两种情况
不使用useMemo的情况
在这个例子中,如果我们点击设置时间setTime按钮或者设置事件类型setTimeType按钮我们可以获取到当前时间根据事件类型的格式化时间,并且会执行console.log('getTimeStr is running'),这是合理的。
但是当我们点击AddCount按钮时,也会执行getTimeStr方法并且打印getTimeStr is running,说明getTimeStr方法内的逻辑还会执行一遍,但是此时getTimeStr方法内使用的两个变量time,timeType都没有任何改变,输出结果完全和上次相同,此时执行getTimeStr方法就是一种额外的开销。
tsx
function Demo01() {
const [time, setTime] = useState(0)
const [timeType, setTimeType] = useState('12H')
const [count, setCount] = useState(0)
const getTimeStr = () => {
console.log('getTimeStr is running')
const date = new Date(time)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
let hours = date.getHours()
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
if (timeType === '12H') {
const amOrPm = hours >= 12 ? 'PM' : 'AM'
hours = hours % 12 || 12
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${amOrPm}`
}
return `${year}-${month}-${day} ${String(hours).padStart(2, '0')}:${minutes}:${seconds}`
}
return (
<div>
<p>This is Demo01 Page {count}</p>
<p>{getTimeStr()}</p>
<button onClick={() => setTime(Date.now())}>setTime</button>
<button onClick={() => setTimeType(timeType === '12H' ? '24H' : '12H')}>setTimeType</button>
<button onClick={() => setCount(count + 1)}>AddCount</button>
</div>
)
}
使用useMemo的情况
下面我们使用useMemo改造该场景,使计算的时间字符串结果缓存起来,在不修改time和timeType两个状态时不会重新计算。使用useMemo缓存计算结果后,只有我们点击setTime按钮或者setTimeType按钮时,才会重新计算新的timeStr,点击AddCount时,console.log('getTimeStr is running')并不会执行,这样就避免了一些不必要的开销。
tsx
function Demo01() {
const [time, setTime] = useState(0)
const [timeType, setTimeType] = useState('12H')
const [count, setCount] = useState(0)
const timeStr = useMemo(() => {
console.log('getTimeStr is running')
const date = new Date(time)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
let hours = date.getHours()
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
if (timeType === '12H') {
const amOrPm = hours >= 12 ? 'PM' : 'AM'
hours = hours % 12 || 12
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${amOrPm}`
}
return `${year}-${month}-${day} ${String(hours).padStart(2, '0')}:${minutes}:${seconds}`
}, [time, timeType])
return (
<div>
<p>This is Demo01 Page {count}</p>
<p>{timeStr}</p>
<button onClick={() => setTime(Date.now())}>setTime</button>
<button onClick={() => setTimeType(timeType === '12H' ? '24H' : '12H')}>setTimeType</button>
<button onClick={() => setCount(count + 1)}>AddCount</button>
</div>
)
}
useContext
useContext定义
我们都知道,父子组件之间传递数据时可以使用props,但如果是祖孙组件之间呢,当我们组件结构比较深的时候,如果再使用props传递数据,就会出现层层传递的情况,稍有不慎中间某个组件忘记传递就会无法获取数据,因此react创建了一个上下文钩子函数useContext
useContext 用于在函数组件中访问 React 的上下文(context)。上下文是一种在组件树中共享数据的机制,可以使得在组件之间传递数据变得更加简洁和高效,避免了通过 props 层层传递的繁琐过程。
通过 useContext,我们可以在组件内部直接获取上下文中的数据,而无需通过 props 从父组件一层层地传递数据。这样,在某些特定情况下,可以更加方便地处理全局状态、主题、用户认证信息等数据。
useContext 需要与 React.createContext 一起使用。React.createContext 用于创建一个上下文对象,它包含了一个 Provider 和一个 Consumer。Provider 用于在组件树的某一层级提供共享的数据,而 Consumer 用于在组件树的任意位置获取这些数据。
useContext简单使用
首先我们创建两个组件Demo01,Demo02,我们在App中引入Demo01,在Demo01中引入Demo02,然后在App中使用createContext向组件树传递一个数据,并在Demo02中使用useContext获取该数据,以下是案例代码:
(注意:我们下面在App入口组件中使用createContext只是案例,createContext实际可以在任意组件中使用)
- App.tsx
代码解析:
首先我们使用createContext创建了AppContext实例对象,并且指定了该对象的类型,这里指定的类型就是我们在AppContext.Provide的value中所传的数据类型。
然后我们使用useMemo返回了一个我们需要传递的数据对象,这里使用useMemo的原因是如果直接在AppContext.Provider的value中写数据对象,每次app的render都会重新生成一个新的对象,使用useMemo返回可以避免每次生成对象带来的额外开销
最后我们将AppContext, AppContextType导出,供后续组件使用
注意:我们想要使用AppContext中传递的数据的组件,必须包裹在AppContext.Provider中,否则无法拿到。
tsx
import React, { createContext, useMemo } from 'react'
import Demo01 from '@pages/demo/demo01'
type AppContextType = {
value: string
}
const AppContext = createContext<AppContextType | null>(null)
function App() {
const appContextDate = useMemo(() => {
return {
value: 'appContextValue'
}
}, [])
return (
<AppContext.Provider value={appContextDate}>
<div>
<h1>React App</h1>
<Demo01 />
</div>
</AppContext.Provider>
)
}
export default App
export { AppContext, AppContextType }
- Demo01.tsx
中间组件,只是简单的引用Demo02
tsx
import React from 'react'
import Demo02 from '@pages/demo02/demo02'
function Demo01() {
return (
<div>
<p>This is Demo01 Page</p>
<Demo02 />
</div>
)
}
export default Demo01
- Demo02.tsx
代码解析:
首先我们要引入AppContext, AppContextType,并且从react中导入useContext
然后获取上下文数据,使用appContextDate接收,此时我们使用类型断言,告诉ts我们的appContextDate一定是AppContextType类型的,因为我们之前定义类型时有null的情况,但是我们确定我们现在不是null,就可以直接使用类型断言。
最后在代码中就可以直接使用appContextDate中的数据
tsx
import React, { useContext } from 'react'
import { AppContext, AppContextType } from '@app/app'
function Demo02() {
const appContextDate = useContext(AppContext) as AppContextType
return (
<div>
<p>This is Demo02 Page</p>
<p>{appContextDate.value}</p>
</div>
)
}
export default Demo02
useContext传递响应式数据,并在子组件修改数据
上面我们的案例是传递一个静态数据,我们可以与useState结合,传递一个响应式数据,并且可以传入修改该数据的方法,供子组件使用,可以实现在任意子组件都可以修改上下文数据。
例如我们实现这样一个功能,在app组件传递一个count变量和一个修改count的方法,我们在其子组件demo01显示该count的值,并且实现在demo01的兄弟组件demo02的子组件button中点击按钮修改count的值,在demo01中响应式显示,组件结构图解如下
- App.tsx创建context,并将count和setCount方法传入
tsx
import React, { createContext, useMemo } from 'react'
import Demo01 from '@pages/demo/demo01'
import Demo02 from '@pages/demo02/demo02'
type AppContextDateType = {
count: number,
setCount: (newCount: number) => void
}
const AppContext = createContext<AppContextDateType | null>(null)
function App() {
const [count, setCount] = React.useState(0)
const appContextDate = useMemo(() => {
return {
count,
setCount: (newCount: number) => setCount(newCount)
}
}, [count])
return (
<AppContext.Provider value={appContextDate}>
<div>
<h1>React App</h1>
<Demo01 />
<Demo02 />
</div>
</AppContext.Provider>
)
}
export default App
export { AppContext, AppContextDateType }
- Demo01.tsx显示count
tsx
import React, { useContext } from 'react'
import { AppContext, AppContextDateType } from '@app/app'
function Demo01() {
const { count } = useContext(AppContext) as AppContextDateType
return (
<div>
<p>This is Demo01 Page Count is {count}</p>
</div>
)
}
export default Demo01
- Demo02.tsx引入Button组件
tsx
import React from 'react'
import MyButton from '@pages/demo02/myButton'
function Demo02() {
return (
<div>
<p>This is Demo02 Page</p>
<MyButton />
</div>
)
}
export default Demo02
- Button.tsx修改count值
tsx
import React, { useContext } from 'react'
import { AppContext, AppContextDateType } from '@app/app'
function MyButton() {
const { count, setCount } = useContext(AppContext) as AppContextDateType
return (
<button onClick={() => setCount(count + 1)}>AddCount</button>
)
}
export default MyButton
同时使用多个context
我们使用context时,并不是只能使用一个,可以创建多个context并且同时使用,例如下面案例就是创建了一个ThemeContext和一个UsersContext同时使用,并且在其子组件中获取这两个context传下来的值
- App.tsx创建并使用ThemeContext和一个UsersContext
tsx
import React, { createContext, useMemo } from 'react'
import Demo01 from '@pages/demo/demo01'
type ThemeContextDateType = {
theme: string
}
type UsersContextDateType = {
name: string
age: number
}
const ThemeContext = createContext<ThemeContextDateType | null>(null)
const UsersContext = createContext<UsersContextDateType | null>(null)
function App() {
const themeContextDate = useMemo(() => {
return {
theme: 'red'
}
}, [])
const usersContextDate = useMemo(() => {
return {
name: '张三',
age: 18
}
}, [])
return (
<ThemeContext.Provider value={themeContextDate}>
<UsersContext.Provider value={usersContextDate}>
<div>
<h1>React App</h1>
<Demo01 />
</div>
</UsersContext.Provider>
</ThemeContext.Provider>
)
}
export default App
export {
ThemeContext,
UsersContext,
ThemeContextDateType,
UsersContextDateType
}
- Demo01.tsx获取context值
tsx
import React, { useContext } from 'react'
import {
ThemeContext,
UsersContext,
ThemeContextDateType,
UsersContextDateType
} from '@app/app'
function Demo01() {
const { theme } = useContext(ThemeContext) as ThemeContextDateType
const { name, age } = useContext(UsersContext) as UsersContextDateType
return (
<div>
<p>This is Demo01 Page theme is {theme}</p>
<p>name: {name}</p>
<p>age: {age}</p>
</div>
)
}
export default Demo01
抽取context到单独组件
像我们上面这些写法,都需要把创建context和使用context,包括相应的数据和修改函数都定义在App组件中,App组件是我们的入口组件,不应该包含复杂的逻辑,因此我们可以将所有的context抽成一个单独的组件,然后在App中使用。
- 抽取context到AppContext公共组件中
tsx
import React, { createContext, useMemo } from 'react'
type ThemeContextDateType = {
theme: string
}
type UsersContextDateType = {
name: string
age: number
}
type IAppContext = {
children: React.ReactNode
}
const ThemeContext = createContext<ThemeContextDateType | null>(null)
const UsersContext = createContext<UsersContextDateType | null>(null)
function AppContext(props: IAppContext) {
const { children } = props
const themeContextDate = useMemo(() => {
return {
theme: 'red'
}
}, [])
const usersContextDate = useMemo(() => {
return {
name: '张三',
age: 18
}
}, [])
return (
<ThemeContext.Provider value={themeContextDate}>
<UsersContext.Provider value={usersContextDate}>
{children}
</UsersContext.Provider>
</ThemeContext.Provider>
)
}
export default AppContext
export {
ThemeContext,
UsersContext,
ThemeContextDateType,
UsersContextDateType
}
- 在App.tsx中使用AppContext组件
tsx
import React from 'react'
import Demo01 from '@pages/demo/demo01'
import AppContext from './appContext'
function App() {
return (
<AppContext>
<div>
<h1>React App</h1>
<Demo01 />
</div>
</AppContext>
)
}
export default App
useReducer
useReducer定义
useReducer用于在函数组件中管理复杂的状态逻辑。它是受控于 Redux 的状态管理库中 reducer 概念的简化版本。
useReducer 的作用是将组件的状态和状态更新逻辑分离,通过一个函数来管理组件的状态。它接收一个 reducer 函数和一个初始状态(或称为初始值),返回一个包含当前状态和状态更新函数的数组。
reducer 函数是一个纯函数,它接收两个参数:当前的状态(state)和操作(action),并根据操作来返回新的状态。useReducer 钩子将会根据 reducer 函数的返回值来更新组件的状态。
简而言之,useReducer就是把组件的状态和修改状态的函数抽离出来,我们下面用一个案例说明
useReducer的简单使用
我们定义一个state变量count,但是我们不用useState来定义,而是使用useReducer来定义,useReducer传入三个参数,后两个可选,一般我们只用前两个,一个时修改该状态的函数reducer,一个是状态的初始值。
我们在组件外部定义一个修改该状态的reducer函数,组件内部使用dispatch来触发该函数并修改state的值
tsx
import React, { useReducer } from 'react'
const changeCount = (state: number, action: string) => {
switch (action) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
function Demo02() {
const [count, countDispatch] = useReducer(changeCount, 0)
return (
<div>
<p>This is Demo02 Page count {count}</p>
<button onClick={() => countDispatch('INCREMENT')}>add</button>
<button onClick={() => countDispatch('DECREMENT')}>sub</button>
</div>
)
}
export default Demo02
dispatch携带参数
在触发dispatch时,还可以传入个对象,其中包含操作类型和参数,例如上述案例,我们可以指定加减的数字值是多少
tsx
import React, { useReducer } from 'react'
const changeCount = (state: number, action: { type: string, num: number }) => {
const { type, num } = action
switch (type) {
case 'INCREMENT':
return state + num
case 'DECREMENT':
return state - num
default:
return state
}
}
function Demo02() {
const [count, countDispatch] = useReducer(changeCount, 0)
return (
<div>
<p>This is Demo02 Page count {count}</p>
<button onClick={() => countDispatch({ type: 'INCREMENT', num: 3 })}>add</button>
<button onClick={() => countDispatch({ type: 'DECREMENT', num: 2 })}>sub</button>
</div>
)
}
export default Demo02
useContext与useReducer结合使用
如果useReducer只是上述案例的用法,那使用场景完全可以由useState代替,因此我们接下来看一下useReducer真正应该应用的场景,与useContext配合实现全局状态共享或者是部分组件状态共享,这里非常类似react-redux的作用。
接下来我们实现一个很小的需求,定义两个状态,一个count,一个user,这两个状态使用useReducer创建,并通过context共享到app的两个子组件demo01和demo02,在demo02中修改这两个状态的值,并且在demo01显示最新修改的值,功能图解如下:
创建AppContext公共组件
在这个组件中,我们定义了两个reducer函数,分别用来修改count和user,创建了一个AppContextProvider组件,并且在组件中使用useReducer注册了count和user两个状态,并将count和user两个状态和修改状态的两个派发函数通过context注入组件树供组件中的所有子组件使用。
最后我们使用自定义hooks简化了useContext的使用,使用自定义hooks可以避免子组件在使用useContext时,需要导入AppContext和useContext,只需要导入一个useAppContext就可以直接使用
tsx
import React, {
createContext,
useReducer,
useMemo,
useContext
} from 'react'
// 定义count的action类型和user的action类型
type countActionType = { type: 'INCREMENT' | 'DECREMENT', value: number }
type userActionType = { type: 'CHANGE_NAME' | 'CHANGE_AGE', name?: string, age?: number }
// 定义修改count的reducer和修改user的reducer
const changeCount = (state: number, action: countActionType) => {
const { type, value } = action
switch (type) {
case 'INCREMENT':
return state + value
case 'DECREMENT':
return state - value
default:
return state
}
}
const changeUser = (state: { name: string, age: number }, action: userActionType) => {
const { type, name, age } = action
switch (type) {
case 'CHANGE_NAME': {
if (name) {
return { ...state, name }
} else {
return state
}
}
case 'CHANGE_AGE': {
if (age) {
return { ...state, age }
} else {
return state
}
}
default:
return state
}
}
// 定义AppContext的数据类型
type AppContextDataType = {
count: number,
user: {
name: string,
age: number
},
countDispatch: React.Dispatch<countActionType>,
userDispatch: React.Dispatch<userActionType>
}
// 定义AppContextProvider的props类型
type IAppContextProvider = {
children: React.ReactNode
}
// 创建AppContext
const AppContext = createContext<AppContextDataType | null>(null)
function AppContextProvider(props: IAppContextProvider) {
const { children } = props
const [count, countDispatch] = useReducer(changeCount, 0)
const [user, userDispatch] = useReducer(changeUser, { name: 'jack', age: 18 })
// 使用useMemo优化性能
const usersContextDate = useMemo(() => {
return {
count,
user,
countDispatch,
userDispatch
}
}, [count, user])
return (
<AppContext.Provider value={usersContextDate}>
{children}
</AppContext.Provider>
)
}
// 定义使用AppContext的hooks,方便使用
function useAppContext() {
const context = useContext(AppContext) as AppContextDataType
if (!context) {
throw new Error('useAppContext必须在AppContextProvider中使用')
}
return context
}
export default AppContextProvider
export { useAppContext }
修改和使用useReducer注册的状态
我们已经创建了AppContext,接下来就是在子组件中使用状态和修改状态了
App.tsx中使用AppContext
tsx
import React from 'react'
import Demo01 from '@pages/demo/demo01'
import Demo02 from '@pages/demo/demo02'
import AppContextProvider from './appContext'
function App() {
return (
<AppContextProvider>
<div>
<h1>React App</h1>
<Demo01 />
<Demo02 />
</div>
</AppContextProvider>
)
}
export default App
使用状态
我们在Demo01中使用这两个状态
tsx
import React from 'react'
import { useAppContext } from '@src/app/appContext'
function Demo01() {
const { count, user } = useAppContext()
return (
<div>
<p>This is Demo01 Page count is {count}</p>
<p>name: {user.name}</p>
<p>age: {user.age}</p>
</div>
)
}
export default Demo01
修改状态
我们在Demo02中修改这两个状态
tsx
import React from 'react'
import { useAppContext } from '@src/app/appContext'
function Demo02() {
const { countDispatch, userDispatch } = useAppContext()
return (
<div>
<p>This is Demo02 Page</p>
<button onClick={() => countDispatch({ type: 'INCREMENT', value: 3 })}>AddCount</button>
<button onClick={() => userDispatch({ type: 'CHANGE_NAME', name: '李四' })}>ChangeName</button>
<button onClick={() => userDispatch({ type: 'CHANGE_AGE', age: 20 })}>ChangeAge</button>
</div>
)
}
export default Demo02
自定义hooks
在hooks中有一个很特殊的hooks,它不是固定的某个hooks,而是你自己创建的,可以根据你的要求实现不同的功能,在自定义hooks中你可以使用所有组件中可以使用的官方hooks。
自定义 Hook 是一种在 React 中复用状态逻辑的方式。它允许你将组件之间共享的逻辑提取到可重用的函数中,以便在不同的组件中使用。自定义 Hook 是普通的 JavaScript 函数,但有两个重要的规则:
1、自定义 Hook 的名称必须以 "use" 开头。这是 React 的约定,以便能够快速识别该函数是一个自定义 Hook。
2、自定义 Hook 可以调用其他的 Hook。这允许你将多个 Hook 组合成一个更大的自定义 Hook。
使用自定义 Hook,你可以将组件之间的状态逻辑抽象出来,使代码更加简洁、易于维护和重用。下面我们举两个简单的自定义hooks案例:
自定义管理表单状态hook
useFormState传入一个表单初始对象,返回一个对象,里面包含表单最新值和修改表单的函数,在组件中使用useFormState,获取表单对象和修改表单函数,并将对应值传入input输入框的属性内
tsx
import React, { ChangeEvent, useState } from 'react'
// 自定义 Hook:用于管理表单状态
type FormState = {
[key: string]: string
}
const useFormState = (initialFormState: FormState) => {
const [formState, setFormState] = useState(initialFormState)
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setFormState({
...formState,
[event.target.name]: event.target.value,
})
}
return {
formState,
handleChange,
}
}
function Test() {
const { formState, handleChange } = useFormState({
username: '',
password: '',
})
return (
<form>
<input
type="text"
name="username"
value={formState.username}
onChange={handleChange}
/>
<input
type="password"
name="password"
value={formState.password}
onChange={handleChange}
/>
<button type="submit">Submit</button>
</form>
)
}
export default Test
自定义定时器hook
自定义一个定时器hook,useTimer根据传入的值,每秒加一并返回计算后的值,在组件中使用useTimer并传入初始值0,使用count接受最新值并显示
tsx
import React, { useEffect, useState } from 'react'
// 自定义 Hook:用于管理计时器
const useTimer = (initialCount: number) => {
const [count, setCount] = useState(initialCount)
useEffect(() => {
const interval = setInterval(() => {
setCount((prevCount) => prevCount + 1)
}, 1000)
return () => {
clearInterval(interval)
}
}, [])
return count
}
function Test() {
const count = useTimer(0)
return (
<div>
<h1>Timer: {count}</h1>
</div>
)
}
export default Test
其他hooks
除了常用的hooks之外,react还提供了一些其他的hooks,他们分别有着不同的作用,下面我们逐一简单较少一下
useDebugValue
useDebugValue 用于在 React 开发者工具中显示自定义的调试值。通常用于在开发过程中帮助开发者更好地理解和调试自定义钩子的运行情况。
在实际使用中,useDebugValue 可以用于在 React 开发者工具中展示一些有用的信息,比如自定义钩子中的状态、计算值、或者其他任意值。这样,当开发者在使用自定义钩子时,在 React 开发者工具中能够更直观地查看到相关信息,方便调试和理解自定义钩子的作用。
useDebugValue传入两个参数,第一个参数是你想要展示的值,第二个参数是格式化你想展示值的函数,函数接收一个参数,该参数就是useDebugValue第一个参数,这个hooks没有返回值,例如下面这个案例,我们在输入框输入内容后,可以在react的开发者工具(React Developer Tools,安装和使用方法不做介绍,可自行查询教程)看到字符串的长度
tsx
import React, {
ChangeEvent,
useDebugValue,
useEffect,
useState
} from 'react'
// 自定义钩子,用于计算输入字符串的长度,并在开发者工具中展示调试值
function useStringLength(input: string) {
const [length, setLength] = useState(0)
useEffect(() => {
setLength(input.length)
}, [input])
// 使用 useDebugValue 来展示调试值
useDebugValue(length, (value) => `String Length: ${value}`)
return length
}
function Test() {
const [text, setText] = useState('')
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
setText(event.target.value)
}
const length = useStringLength(text)
return (
<div>
<p>This is Test Page</p>
<input type="text" value={text} onChange={handleInputChange} />
<p>String Length: {length}</p>
</div>
)
}
export default Test
useDeferredValue
请查阅单独文章:完全搞懂useDeferredValue
useId
useId 是 React 18 中新增的一个 hook,它可以用来生成一个唯一且稳定的 id。生成的id在整个react的声明周期都是稳定的,并且是唯一的,就算组件被多次调用,每个组件内部的id也是唯一的,可以用于我们之前自定义的字符串id所用到的地方。
我们就看一个同一组件内部生成id后多次调用会有什么结果
tsx
import React, { useId, memo } from 'react'
function Demo04() {
const id = useId()
return (
<div>
<p>This is Demo04 Page Id {id}</p>
</div>
)
}
function App() {
return (
<div>
<h1>React App</h1>
<Demo04 />
<Demo04 />
</div>
)
}
页面输出结果:
所以使用useId生成的id在组件中使用时,不用担心组件被多次引用造成的id不唯一的情况。
useImperativeHandle
useImperativeHandle是React中的一个自定义Hook,它允许您向父组件暴露子组件的实例或某些特定的功能,使得在父组件中能够直接调用子组件上的方法或属性。通常情况下,子组件的实例和方法对父组件是不可见的,但使用useImperativeHandle,您可以选择性地将子组件的一部分公开给父组件。
当我们想在父组件访问子组件的一些方法或者属性时,react是没有提供直接的方法的,此时我们就可以使用useImperativeHandle钩子函数,配合react内置api:forwardRef来实现。
forwardRef介绍
forwardRef 允许你的组件使用 ref 将一个 DOM 节点暴露给父组件。也就是说当我们直接在组件上使用ref时,react会有以下报错,例如如下代码:
tsx
import React, { memo, useRef } from 'react'
function Demo05() {
return (
<div>
<p>This is Demo05 Page {name}</p>
</div>
)
}
function Demo04() {
const demoRef = useRef(null)
return (
<div>
<p>This is Demo04 Page</p>
<Demo05 ref={demoRef} />
</div>
)
}
export default memo(Demo04)
当我们这样直接给组件添加ref时,控制台会有如下报错:意思就是不能直接给组件设置ref,如果想要这样做就需要用到React.forwardRef
我们可以把上述代码做如下改造:这样改造之后就不会有报错,然后我们就可以在子组件的参数中拿到这个ref,并且可以把它绑定到任何dom元素上,这样父组件就可以获取到子组件的元素
tsx
import React, { memo, useRef, forwardRef } from 'react'
function Demo05(props, ref) {
return (
<div>
<p>This is Demo05 Page {name}</p>
</div>
)
}
const MyDemo05 = forwardRef(Demo05)
function Demo04() {
const demoRef = useRef(null)
return (
<div>
<p>This is Demo04 Page</p>
<MyDemo05 ref={demoRef} />
</div>
)
}
export default memo(Demo04)
使用useImperativeHandle
上面我们虽然向子组件传入了一个ref,但是还是没有获取到子组件的任何方法或者属性,接下来我们就可以使用useImperativeHandle钩子,useImperativeHandle有三个参数,前两个必选,第三个可选。第一个参数就是我们传进来的ref,第二个参数是个回调函数,回调函数返回一个对象,该对象就是传进来ref的current属性,因此我们可以在回调函数返回的对象上加任何属性,然后父组件就可以在ref.current上获取这些属性。
useImperativeHandle的第三个参数时依赖数字,和其他钩子的依赖数组一样,当你回调函数中用到了某个state时,需要将其放到依赖数组中去
我们下面看个案例,点击父组件的按钮,设置子组件的state
父组件代码
tsx
import React, { memo, useRef } from 'react'
import Demo05, { RefType } from './demo05'
function Demo04() {
const demoRef = useRef<RefType>(null)
const handleSetName = () => {
if (demoRef.current) {
console.log('demoRef.current: ', demoRef.current)
demoRef.current.handleSetName('张三')
}
}
return (
<div>
<p>This is Demo04 Page</p>
<Demo05 ref={demoRef} />
<button type="button" onClick={handleSetName}>Set Name</button>
</div>
)
}
export default memo(Demo04)
子组件代码
tsx
import React, {
memo,
useState,
forwardRef,
useImperativeHandle
} from 'react'
export type RefType = {
handleSetName: (value: string) => void,
name: string
}
function Demo05<T>(props: T, ref: React.Ref<RefType>) {
const [name, setName] = useState('')
const handleSetName = (value: string) => {
setName(value)
}
useImperativeHandle(ref, () => {
return {
handleSetName,
name
}
}, [name])
return (
<div>
<p>This is Demo05 Page {name}</p>
</div>
)
}
export default memo(forwardRef(Demo05))
当我们点击父组件的Set Name按钮时,子组件的name会被修改为'张三',并且控制台会打印出如下结果:说明我们父组件的demoRef上已经可以读取到子组件暴露出的属性和方法了。
useInsertionEffect
useInsertionEffect 是为 CSS-in-JS 库的作者特意打造的。除非你正在使用 CSS-in-JS 库并且需要注入样式,否则你应该使用 useEffect 或者 useLayoutEffect。这是官方的介绍,我们几乎很少使用CSS-in-JS的写法,因此在这里就不做介绍了,想要了解的可以自行查阅资料。
useSyncExternalStore
请查阅单独文章:详解useSyncExternalStore
useTransition
useTransition 是 React 18 中新增的 Hook,它可以让组件在状态更新时平滑过渡而不是突然重新渲染。useTransition 的主要作用有两个:
- 解决组件重新渲染时的界面跳变问题。通过 useTransition 你可以让组件在状态更新时,先保留当前界面,等待数据准备好后再过渡到新界面。
- 解决组件重新渲染时的加载卡顿问题。useTransition 允许你把状态更新和组件渲染分离开,先更新状态,等数据准备好后再过渡到新界面。这样可以避免每次状态变化都重新渲染组件带来的卡顿。
useTransition最主要的特点是它可以先更新状态,再等待ui的渲染。例如实现如下官方文档的案例,在一个页面中有很多的选项卡,点击选项卡显示选项对应内容,其中一个选项卡组件加载十分缓慢,当我们快速点击选项卡时,如果不使用useTransition情况下,当我们点击了加载缓慢的选项卡后,立马点击其他的选项卡,会因为加载缓慢的组件还没有加载完成而导致卡顿。而使用了useTransition之后,当正在加载缓慢组件时若又更新了状态,**useTransition可以中断之前的渲染,直接进行下次渲染。**这就是useTransition的作用关键所在。
不使用useTransition实现案例
实现模拟加载缓慢list组件
typescript
import React, { memo } from 'react'
// 定义一个列表组件List
function List(props: { inputValue: string }) {
const { inputValue } = props
console.log('List render: ', inputValue)
let k = 0
for (let i = 0; i <= 300000000; i += 1) {
k = i
}
return (
<ul>
<li>Cycle Times {k}Text: {inputValue}</li>
<li>Cycle Times {k}Text: {inputValue}</li>
<li>Cycle Times {k}Text: {inputValue}</li>
<li>Cycle Times {k}Text: {inputValue}</li>
<li>Cycle Times {k}Text: {inputValue}</li>
</ul>
)
}
export default memo(List)
实现其他组件
typescript
import React, { memo, useState, useTransition } from 'react'
import List from './list'
function ContactTab() {
return (
<>
<p>
You can find me online here:
</p>
<ul>
<li>[email protected]</li>
<li>+123456789</li>
</ul>
</>
)
}
function AboutTab() {
return (
<p>Welcome to my profile!</p>
)
}
function TabButton({ children, isActive, onClick }) {
if (isActive) {
return <b>{children}</b>
}
return (
<button onClick={() => {
onClick()
}}>
{children}
</button>
)
}
function Demo08() {
const [tab, setTab] = useState('about')
function selectTab(nextTab: string) {
setTab(nextTab)
}
return (
<div>
<p>This is Demo08 Page</p>
<TabButton
isActive={tab === 'about'}
onClick={() => selectTab('about')}
>
About
</TabButton>
<TabButton
isActive={tab === 'posts'}
onClick={() => selectTab('posts')}
>
Posts (slow)
</TabButton>
<TabButton
isActive={tab === 'contact'}
onClick={() => selectTab('contact')}
>
Contact
</TabButton>
<hr />
{tab === 'about' && <AboutTab />}
{tab === 'posts' && <List inputValue='123'/>}
{tab === 'contact' && <ContactTab />}
</div>
)
}
export default memo(Demo08)
页面如下:
当我们快速点击按钮时,有明显的卡顿,这是因为我们修改了tab之后,List组件加载缓慢,再点击其他按钮时,会等待List组件加载完成再更新状态,去加载其他组件。
使用useTransition实现
我们只修改Demo08组件代码即可
typescript
function Demo08() {
const [isPending, startTransition] = useTransition()
const [tab, setTab] = useState('about')
function selectTab(nextTab: string) {
startTransition(() => {
setTab(nextTab)
})
}
return (
<div>
<p>This is Demo08 Page</p>
<TabButton
isActive={tab === 'about'}
onClick={() => selectTab('about')}
>
About
</TabButton>
<TabButton
isActive={tab === 'posts'}
onClick={() => selectTab('posts')}
>
Posts (slow)
</TabButton>
<TabButton
isActive={tab === 'contact'}
onClick={() => selectTab('contact')}
>
Contact
</TabButton>
<hr />
{tab === 'about' && <AboutTab />}
{tab === 'posts' && <List inputValue='123'/>}
{tab === 'contact' && <ContactTab />}
</div>
)
}
使用useTransition之后,遇到加载缓慢的组件,如果立马更新状态,他会体制加载缓慢组件,直接更新状态,避免卡顿。