前语

React Hooks 是React 16.8版别新增的特性,让函数组件(Functional Component)也能够运用 state 以及其他的 React 特性。函数式组件有以下优势:

  1. 让函数组件能够运用state、生命周期办法等React特性
  2. 逻辑更明晰,相关逻辑能够归于一个hook中
  3. 代码可复用性高,自界说Hook能够抽离同享逻辑
  4. 更易于测验,函数纯函数容易测验

留意点:

  1. 只能在函数组件和自界说Hook中调用Hook
  2. 不能在循环、条件或嵌套函数中调用Hook
  3. 只能在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)

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的值便是需求更新的值

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 承受两个参数:一个是副效果操作函数,另一个是依靠数组。副效果操作函数是一个回调函数,它会在组件烘托后履行,以及在组件更新后履行。依靠数组用于指定副效果操作的触发条件。

  1. 假如依靠数组为空 [],副效果操作只在组件挂载和卸载时履行,不依靠任何变量。
  2. 假如不传递依靠数组,副效果操作在每次组件更新后都会履行。
  3. 假如传递了特定变量数组,副效果操作只在特定变量产生改动时履行。
import React, { useEffect } from 'react'
function MyComponent() {
    // 在 useEffect 中界说副效果操作
    useEffect(() => {
        // 副效果操作,能够进行数据获取、订阅事情、操作 DOM 等
        // ...
        // 回来一个整理函数(可选),用于在组件卸载时履行整理操作
        return () => {
            // 整理操作,例如撤销订阅、清除定时器
            // ...
        }
    }, []) // 依靠数组(可选),用于操控副效果的触发条件
    // 空数组表明只在组件挂载和卸载时履行副效果,不依靠任何变量
    // 不传递依靠数组表明在每次组件更新后都履行副效果
    // 传递特定变量数组表明只在特定变量产生改动时履行副效果
}

useEffect事例解析

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元素,这也是它的一般用法,例如下面这个事例:

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的宽高并打印出来

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的值,组件不会从头烘托。

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,组件卸载时将其清楚

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都会履行

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相同,只要依靠项中的状况产生改动,函数才会被从头界说

  1. 没有依靠项的函数,只需求组件创立时界说就好,组件这样改造之后,不论你怎么点击按钮修正state,都不会触发useEffect is running,只要组件加载的时分触发一次。
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>
    )
}
  1. 存在依靠项的函数,当一个函数中存在状况变量时,咱们必须把该状况放入依靠数组,这样才干确保当函数中引证的状况产生改动时,及时从头界说函数,函数中引证的状况值才干是最新的值。例如下面事例:
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.下面咱们将依靠加上再做测验

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事例如下

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办法便是一种额定的开支。

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’)并不会履行,这样就防止了一些不必要的开支。

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实践能够在恣意组件中运用)

  1. App.tsx

代码解析:
首要咱们运用createContext创立了AppContext实例目标,而且指定了该目标的类型,这儿指定的类型便是咱们在AppContext.Provide的value中所传的数据类型。
然后咱们运用useMemo回来了一个咱们需求传递的数据目标,这儿运用useMemo的原因是假如直接在AppContext.Provider的value中写数据目标,每次app的render都会从头生成一个新的目标,运用useMemo回来能够防止每次生成目标带来的额定开支
最终咱们将AppContext, AppContextType导出,供后续组件运用
留意:咱们想要运用AppContext中传递的数据的组件,必须包裹在AppContext.Provider中,否则无法拿到。

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 }
  1. Demo01.tsx

中间组件,只是简略的引证Demo02

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
  1. Demo02.tsx

代码解析:
首要咱们要引进AppContext, AppContextType,而且从react中导入useContext
然后获取上下文数据,运用appContextDate接纳,此刻咱们运用类型断言,告知ts咱们的appContextDate一定是AppContextType类型的,由于咱们之前界说类型时有null的状况,可是咱们确认咱们现在不是null,就能够直接运用类型断言。
最终在代码中就能够直接运用appContextDate中的数据

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中呼应式显现,组件结构图解如下

一篇搞懂所有React Hooks

  1. App.tsx创立context,并将count和setCount办法传入
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 }
  1. Demo01.tsx显现count
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
  1. Demo02.tsx引进Button组件
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
  1. Button.tsx修正count值
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传下来的值

  1. App.tsx创立并运用ThemeContext和一个UsersContext
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
}
  1. Demo01.tsx获取context值
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中运用。

  1. 抽取context到AppContext公共组件中
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
}
  1. 在App.tsx中运用AppContext组件
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的值

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时,还能够传入个目标,其间包括操作类型和参数,例如上述事例,咱们能够指定加减的数字值是多少

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显现最新修正的值,功用图解如下:

一篇搞懂所有React Hooks

创立AppContext公共组件

在这个组件中,咱们界说了两个reducer函数,别离用来修正count和user,创立了一个AppContextProvider组件,而且在组件中运用useReducer注册了count和user两个状况,并将count和user两个状况和修正状况的两个派发函数经过context注入组件树供组件中的一切子组件运用。
最终咱们运用自界说hooks简化了useContext的运用,运用自界说hooks能够防止子组件在运用useContext时,需求导入AppContext和useContext,只需求导入一个useAppContext就能够直接运用

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

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中运用这两个状况

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中修正这两个状况

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输入框的特点内

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承受最新值并显现

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,装置和运用办法不做介绍,可自行查询教程)看到字符串的长度

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

react的开发者东西中显现的成果如下图:

一篇搞懂所有React Hooks

useDeferredValue

请查阅独自文章:彻底搞懂useDeferredValue

useId

useId 是 React 18 中新增的一个 hook,它能够用来生成一个仅有且安稳的 id。生成的id在整个react的声明周期都是安稳的,而且是仅有的,就算组件被屡次调用,每个组件内部的id也是仅有的,能够用于咱们之前自界说的字符串id所用到的当地。
咱们就看一个同一组件内部生成id后屡次调用会有什么成果

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>
    )
}

页面输出成果:

一篇搞懂所有React Hooks

所以运用useId生成的id在组件中运用时,不必忧虑组件被屡次引证形成的id不仅有的状况。

useImperativeHandle

useImperativeHandle是React中的一个自界说Hook,它答应您向父组件露出子组件的实例或某些特定的功用,使得在父组件中能够直接调用子组件上的办法或特点。一般状况下,子组件的实例和办法对父组件是不行见的,但运用useImperativeHandle,您能够选择性地将子组件的一部分公开给父组件。
当咱们想在父组件拜访子组件的一些办法或许特点时,react是没有供给直接的办法的,此刻咱们就能够运用useImperativeHandle钩子函数,配合react内置api:forwardRef来完结。

forwardRef介绍

forwardRef 答应你的组件运用 ref 将一个 DOM 节点露出给父组件。也便是说当咱们直接在组件上运用ref时,react会有以下报错,例如如下代码:

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

一篇搞懂所有React Hooks

咱们能够把上述代码做如下改造:这样改造之后就不会有报错,然后咱们就能够在子组件的参数中拿到这个ref,而且能够把它绑定到任何dom元素上,这样父组件就能够获取到子组件的元素

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
父组件代码

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)

子组件代码

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上现已能够读取到子组件露出出的特点和办法了。

一篇搞懂所有React Hooks

useInsertionEffect

useInsertionEffect 是为 CSS-in-JS 库的作者特意打造的。除非你正在运用 CSS-in-JS 库而且需求注入款式,否则你应该运用 useEffect 或许 useLayoutEffect。这是官方的介绍,咱们简直很少运用CSS-in-JS的写法,因而在这儿就不做介绍了,想要了解的能够自行查阅材料。

useSyncExternalStore

请查阅独自文章:详解useSyncExternalStore

useTransition

useTransition 是 React 18 中新增的 Hook,它能够让组件在状况更新时滑润过渡而不是忽然从头烘托。useTransition 的首要效果有两个:

  1. 处理组件从头烘托时的界面跳变问题。经过 useTransition 你能够让组件在状况更新时,先保存当时界面,等候数据准备好后再过渡到新界面。
  2. 处理组件从头烘托时的加载卡顿问题。useTransition 答应你把状况更新和组件烘托别脱离,先更新状况,等数据准备好后再过渡到新界面。这样能够防止每次状况改动都从头烘托组件带来的卡顿。

useTransition最首要的特点是它能够先更新状况,再等候ui的烘托。例如完结如下官方文档的事例,在一个页面中有许多的选项卡,点击选项卡显现选项对应内容,其间一个选项卡组件加载十分缓慢,当咱们快速点击选项卡时,假如不运用useTransition状况下,当咱们点击了加载缓慢的选项卡后,立马点击其他的选项卡,会由于加载缓慢的组件还没有加载完结而导致卡顿。而运用了useTransition之后,当正在加载缓慢组件时若又更新了状况,**useTransition能够中止之前的烘托,直接进行下次烘托。**这便是useTransition的效果关键所在。

不运用useTransition完结事例

完结模拟加载缓慢list组件

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)

完结其他组件

import React, { memo, useState, useTransition } from 'react'
import List from './list'
function ContactTab() {
    return (
        <>
            <p>
                You can find me online here:
            </p>
            <ul>
                <li>admin@mysite.com</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)

页面如下:

一篇搞懂所有React Hooks

当咱们快速点击按钮时,有显着的卡顿,这是由于咱们修正了tab之后,List组件加载缓慢,再点击其他按钮时,会等候List组件加载完结再更新状况,去加载其他组件。

运用useTransition完结

咱们只修正Demo08组件代码即可

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之后,遇到加载缓慢的组件,假如立马更新状况,他会体制加载缓慢组件,直接更新状况,防止卡顿。