React-Hook 應(yīng)用

Hook 是 react 16.8 推出的新特性,具有如下優(yōu)點(diǎn):

  • Hook 使你在無需修改組件結(jié)構(gòu)的情況下復(fù)用狀態(tài)邏輯?!远x hook
  • Hook 將組件中相互關(guān)聯(lián)的部分拆分成更小的函數(shù)(比如設(shè)置訂閱或請求數(shù)據(jù))——Effect
  • Hook 使你在非 class 的情況下可以使用更多的 React 特性?!獡肀Ш瘮?shù)式組件

1. 簡介

Hook 就是 JavaScript 函數(shù),但是使用它們會有兩個(gè)額外的規(guī)則:

  • 只能在函數(shù)最外層調(diào)用 Hook,也即Hook 需要在我們組件的最頂層調(diào)用。不要在循環(huán)、條件判斷或者子函數(shù)中調(diào)用,這會破壞更新時(shí) hook 順序的一致性,造成數(shù)據(jù)讀取錯(cuò)誤。
  • 只能在 React 的函數(shù)組件中調(diào)用 Hook。不要在其他 JavaScript 函數(shù)中調(diào)用。(還有一個(gè)地方可以調(diào)用 Hook —— 就是自定義的 Hook 中)

可以通過 ESLint 配置來提醒自己遵循 Hook 開發(fā)規(guī)則:

  • 安裝插件 npm install eslint-plugin-react-hooks --save-dev

  • 配置 package.json 中的eslint-config:

// 你的 ESLint 配置
"eslintConfig": {
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // 檢查 Hook 的規(guī)則
    "react-hooks/exhaustive-deps": "warn" // 檢查 effect 的依賴
  }
}

2. React Hook 的基礎(chǔ) API (附實(shí)例)

具體 API 介紹可以查閱官網(wǎng) Hook API Reference

2.1 組件中的狀態(tài)——useState

在使用 class 定義組件時(shí),可以通過 this.state 定義組件內(nèi)的狀態(tài)屬性,在 render 時(shí)使用 this.state[key] 獲取狀態(tài)值,使用 this.setState 來修改狀態(tài)。Hook 中提供了 useState 方法,用于快速定義函數(shù)組件內(nèi)的狀態(tài)和對應(yīng)的更改函數(shù)。

使用方法:const [state, setState] = useState(defaultValue)

實(shí)例:

import React, { useState } from 'react'
function Counter() {
  // 初始化 count = 0
  const [count, setCount] = useState(0)
  return (<div>
    <p>你點(diǎn)擊了 {count} 次</p>  
    {/* 直接調(diào)用 setCount,傳入新的值,賦值給 count */}
    <button onClick={()=> setCount(count+1)}>點(diǎn)擊</button>
  </div>)
}
2.2 組件中的生命周期——useEffect

函數(shù)式組件在發(fā)生更新時(shí),都會順序執(zhí)行函數(shù)主體,相當(dāng)于類組件中的 render 函數(shù)。而在 render 過程中,是不允許執(zhí)行改變 DOM、添加訂閱、設(shè)置定時(shí)器、記錄日志等包含副作用的操作,因此在類組件中,我們通常在生命周期函數(shù)中執(zhí)行必要的包含副作用操作。在函數(shù)式組件中,useEffect 提供了執(zhí)行副作用操作的支持,當(dāng) React 渲染組件時(shí),會保存已使用的 effect,并在更新完 DOM 后執(zhí)行它。useEffect ≈ componentWillMount + componentDidUpdate + componentWillUnmount,其內(nèi)部可以訪問到組件的 propsstate。

使用方法:useEffect( didUpdateFn )

實(shí)例:

import React, { useState, useEffect } from 'react'
function Counter() {
  // 初始化 count = 0
  const [count, setCount] = useState(0)
  
  // 如果第二個(gè)參數(shù)為空,則只有在組件被銷毀時(shí)才解綁,也就是 副作用 等價(jià)于 componentDidMount,解綁 等價(jià)于 componentWillUnMount
  useEffect(() => {
      console.log('useEffect => 組件掛載')
      // 返回一個(gè)清除函數(shù),當(dāng)副作用中有定時(shí)器或監(jiān)聽事件時(shí)清除
      return () => {
          console.log('useEffect => 組件被銷毀')
      };
  }, [])
  // 第二個(gè)參數(shù),每次當(dāng) count 發(fā)生變化就執(zhí)行解綁原來的數(shù)據(jù)并重新執(zhí)行副作用
  useEffect(() => {
      console.log('useEffect => count 數(shù)據(jù)掛載')
      return () => {
          console.log('useEffect => count 數(shù)據(jù)解綁')
      };
  }, [count])
  
  return (<div>
    <p>你點(diǎn)擊了 {count} 次</p>  
    <button onClick={()=> setCount(count+1)}>點(diǎn)擊</button>
  </div>)
}

React 會在調(diào)用一個(gè)新的 effect 之前對前一個(gè) effect 進(jìn)行清理。在上述程序中,當(dāng) count 值更新時(shí),會先輸出 ’useEffect => count 數(shù)據(jù)掛載‘,再輸出 ’useEffect => count 數(shù)據(jù)掛載‘。因此當(dāng)我們在 useEffect 中設(shè)置定時(shí)器/事件時(shí),通過返回一個(gè)清除函數(shù),使得在下一次依賴發(fā)生更新時(shí),能夠清除上一次的定時(shí)器/事件,以此避免內(nèi)存泄露。否則每次依賴更新時(shí),都會增加一個(gè)定時(shí)器/事件。

2.3 跨組件通信——useContext

在跨組件通信時(shí),可以借助 context 實(shí)現(xiàn)組件間的傳值。useContext 用于快速獲取組件上層最近的 contextObj.Provider 所提供的 value 值,等價(jià)于 contextObj.Consumer。

使用方法:useContext(ContextObj)contextObjReact.createContext 返回的 context 對象

實(shí)例:

import React, { useState, useEffect, useContext, createContext } from 'react'

const ColorContext = createContext()

function Container() {
  const [color, setColor] = useState('#ffff00')
  const toggleColor = () => {
      const saturation = () => Math.random() * 255
      setColor(`rgb(${saturation()}, ${saturation()}, ${saturation()})`)
  }
  // 1. 使用 ColorContext.Provider 將 color 值傳給內(nèi)部的組件
  // 3. 當(dāng)按鈕點(diǎn)擊切換背景色時(shí), color 值發(fā)生變化,將通知到 Counter 組件中
  return (<ColorContext.Provider value={color}>
     <Counter />
     <button onClick={toggleColor}>切換背景色</button>
  </ColorContext.Provider>)
}

function Counter() {
  // ...
  // 2. 使用 useContext 返回最近的 Provider 提供的 value 值,本例中也即 color 值,并訂閱 color 值的變化
  const color = useContext(ColorContext)
  return (<div>
    <p style={{backgroundColor: color}}>你點(diǎn)擊了 {count} 次</p>  
    <button onClick={()=> setCount(count+1)}>點(diǎn)擊</button>
  </div>)
}
2.4 復(fù)雜狀態(tài)管理——useReducer

在 redux 狀態(tài)管理中,使用 reducer 根據(jù) action 的不同,對 state 執(zhí)行不同的操作。在 hook 中,useState 支持直接修改 state,但是當(dāng)修改邏輯較為復(fù)雜時(shí),可以改用 useReducer 來定義不同的更改行為。通過傳入一個(gè)形如 (state, action) => {} 的 reducer,返回狀態(tài)及其 dispatch 函數(shù)。還可以使用后面的兩個(gè)參數(shù)對 state 執(zhí)行初始化操作,initialArg 將作為 init 函數(shù)的參數(shù)傳入。

使用方法:const [state, dispatch] = useReducer(reducer, initialArg, init)

實(shí)例:

import React, { useState, useEffect, useContext, createContext, useReducer } from 'react'

function Container() {
  // ...
  return (<ColorContext.Provider value={color}>
     <Counter initialCount={0}/>
     <button onClick={toggleColor}>切換背景色</button>
  </ColorContext.Provider>)
}
function Counter() {
  // ...
  const init = initialCount => ({count: initialCount})
  const [state, dispatch] = useReducer((state, action) => {
        switch(action.type) {
            case 'add': 
                return {count: state.count + 1}
            case 'sub':
                return {count: state.count - 1}
            case 'reset':
                    return init(action.payload)
            default:
                return state
        }
  }, initialCount, init)
  return (<div>
    <p style={{backgroundColor: color}}>你點(diǎn)擊了 {state.count} 次</p> 
    <button onClick={() => dispatch({type: 'add'})}>+</button>
    <button onClick={() => dispatch({type: 'sub'})}>-</button>
    <button onClick={() => dispatch({type: 'reset', payload: initialCount})}>Reset</button>
  </div>)
}
2.5 組件性能優(yōu)化——useCallback / useMemo

在組件生命周期的應(yīng)用中,常常有利用 shouldCompnentUpdate 判斷參數(shù)/狀態(tài)的相等性,避免不必要的組件渲染。 useCallback 也是一種類似的組件優(yōu)化手段,其返回一個(gè) memoized 函數(shù),僅在依賴項(xiàng)發(fā)生變化時(shí),函數(shù)體才會更新。useCallback(fn, deps) 相當(dāng)于 useMemo(() => fn, deps),不同的是 useMemo 返回的是一個(gè) memoized 值,當(dāng)依賴項(xiàng)發(fā)生變化時(shí),fn 才會執(zhí)行,該值才會發(fā)生更新。傳入 useMemo 的函數(shù)會在渲染期間執(zhí)行,因此useMemo 內(nèi)部,不要執(zhí)行與渲染無關(guān)的操作。依賴項(xiàng)并不會作為參數(shù)傳入回調(diào)函數(shù)中,但內(nèi)部執(zhí)行函數(shù)可以直接使用依賴項(xiàng),如 fn(deps) 。

使用方法:useCallback(() => { doSomething() }, depsArr) / useMemo(() => doSomething(), depsArr)

實(shí)例:

import React, { useState, useEffect, useContext, createContext, useReducer, useMemo } from 'react'
// ...
function Counter() {
    // ...
    const [asyncName, setName] = useState('')
    function sayName(name) {
        setTimeout(() => {
            // 模擬異步請求,并根據(jù)請求結(jié)果設(shè)置狀態(tài)值
            console.log(`${name} 正在操作`)
            setName(`user_${name}`)
        }, 1000)  
    }
  
    // 直接調(diào)用 sayName 的話,每次 state.count 發(fā)生變化時(shí),雖然 asyncName 并不會變化,但 sayName 每次都會被執(zhí)行。如果是一個(gè)比較耗時(shí)的異步請求,將降低組件的性能
    // sayName(name) 
  
    // 對比直接調(diào)用,使用 useMemo,能夠避免 count 變化時(shí) sayName 的頻繁調(diào)用,從而優(yōu)化組件性能
    // 僅在依賴項(xiàng) name 值發(fā)生變化時(shí),sayName 方法才會被執(zhí)行
    useMemo(() => sayName(name), [name])
    return (<div>
        <h1>用戶名:{asyncName}</h1>
        {/* ... */}
    </div>)
}

效果:

  • 直接調(diào)用 sayName
每次點(diǎn)擊 count 的按鈕都會打印 xxx 正在操作
  • 使用 useMemo
    僅有更新 name 時(shí)才會打印 xxx 正在操作
2.6 組件內(nèi)值的保存——useRef

ref 是一種訪問 DOM 的方式,useRef 返回一個(gè)“盒子”,可以在其 current 屬性中保存一個(gè)任何類型的可變值,如 DOM 元素、定時(shí)器、訂閱器等。useRef 在每次渲染時(shí)返回同一個(gè) ref 對象,當(dāng) ref 對象內(nèi)容發(fā)生變化時(shí),useRef不會通知你。變更 .current 屬性不會引發(fā)組件重新渲染。

使用方法:const oRef = useRef(initialValue)

實(shí)例:

import React, {useRef, useEffect} from 'react'
function InputItem() {
    const inputEle = useRef(null)
    const timerId = useRef(null)
    const [time, setTime] = useState(0)
    
    useEffect(() => {
        const id = setInterval(() => {
            // 使用函數(shù)更新的方式,避免依賴項(xiàng)
            setTime(t => t + 1)
        }, 1000)
        timerId.current = id
        return () => clearInterval(id)
    }, [])

    const focusBtnClick = () => {
        // inputEle.current 已經(jīng)掛載到 DOM 中的文本輸入框元素上
        inputEle.current.focus()
    }
    const clearBtnClick = () => {
        // timerId.current 已經(jīng)被寫入了定時(shí)器的 id,可以在 click 事件中中止定時(shí)器
        clearInterval(timerId.current)
    }

    return (<div>
        <h2>定時(shí)器數(shù)值為:{time}</h2>
        <input type='text' ref={inputEle} />
        <button onClick={focusBtnClick}>focus</button>
        <button onClick={clearBtnClick}>stop</button>
    </div>)
}

效果:

useRef.gif
2.7 其它

以下三個(gè) hook 都是極少使用的方法,簡單介紹其應(yīng)用,有必要時(shí)可以查閱官方文檔

  • useImperativeHandle :用于自定義暴露給父組件的子組件內(nèi)部某一 ref 實(shí)例值,與 forwardRef(將內(nèi)部某一 ref 實(shí)例值全部暴露給父組件) 配合使用。
  • useLayoutEffect :作用同 useEffect,不同的是 useEffect 是在 DOM 元素渲染完成后執(zhí)行,而 useLayoutEffect 是與 DOM 更新同步執(zhí)行。
  • useDebugValue :用于在 React 開發(fā)者工具中顯示自定義 hook 的標(biāo)簽。

3. 自定義 Hook

在函數(shù)化開發(fā)時(shí),我們常常將多個(gè)函數(shù)間共用的邏輯抽離為某一功能函數(shù),增強(qiáng)代碼的復(fù)用性。而在組件化開發(fā)過程中,兩個(gè)組件之間也可能存在同樣的功能邏輯,比如需求列表和詳情頁都需要獲取需求項(xiàng)的狀態(tài)(規(guī)劃中/進(jìn)行中/已完成),此時(shí)可以把 “查詢需求項(xiàng)狀態(tài)” 這一功能用自定義 hook 抽離出來,不僅能夠提高代碼復(fù)用性和可讀性,還能方便測試。自定義 hook 命名需要以 use 開頭,以方便 react 自動(dòng)檢查是否違反了 hook 規(guī)則。目前,也有很多第三方 hook 實(shí)現(xiàn):https://github.com/streamich/react-use

實(shí)例:

import React, { useState, useEffect } from 'react'

// 自定義 hook : 根據(jù)需求項(xiàng)的 id 值查詢狀態(tài)
function useStatus(demandId) {
    // 需求項(xiàng)狀態(tài),0 - 規(guī)劃中, 1 - 進(jìn)行中, 2 - 已完成
    const [status, setStatus] = useState(0)
    useEffect(() => {
        // 模擬一下異步請求
        const getStatus = setTimeout(() => {
            setStatus(demandId % 3)
        }, 200)
        return () => {
            clearTimeout(getStatus)
        }
    }, [demandId])
    const description = ['規(guī)劃中', '進(jìn)行中', '已完成']
    return {code: status, status: description[status]}
}

// 需求列表組件
function DemandList() {
    const [list, setList] = useState([])
    const [selectedId, setSelectedId] = useState(null)

    useEffect(() => {    
        // 模擬一下數(shù)據(jù)
        let mockList = []
        for(let i = 0; i <= 9; i++) {
            const id = i + Math.round(Math.random() * 100)
            mockList.push({
                id,
                name: `需求${id}`
            })
        }
        setList(mockList)
    }, [])

    return (<div>
        <ul>
            {list.map( demand => (
                <DemandItem 
                    demandId = {demand.id}
                    selectedMethod = {setSelectedId}
                >
                    {demand.name}
                </DemandItem>
            ) )}
        </ul>
        <hr />
        {/* 這里為了偷懶,就沒有用路由,而是直接顯示在下面 */}
        { selectedId && <DemandDetail demandId = {selectedId} />}
    </div>)
    
}

// 需求項(xiàng)組件
function DemandItem({demandId, selectedMethod, children}) {
    // 調(diào)用自定義 hook 獲取需求項(xiàng)狀態(tài)
    const {code, status} = useStatus(demandId)
    const color = ['#F4A460', '#FFD700', '#32CD32']

    return (<li>
        <span style = {{display: 'inline-block', width: '100px'}}>{children}</span>
        <span 
            style = {{backgroundColor: color[code], cursor: 'pointer'}}
            onClick = {() => {selectedMethod(demandId)}}
        >{status}</span>
    </li>)
}

// 需求詳情頁組件
function DemandDetail({demandId}) {
    // 調(diào)用自定義 hook 獲取需求項(xiàng)狀態(tài)
    const {status} = useStatus(demandId)
    const [info, setInfo] = useState({})

    useEffect(() => {
        // 根據(jù) id 查詢需求的詳細(xì)信息
        setInfo({
            name: `需求${demandId}`,
            detail: '這是需求詳情信息呀~這是需求詳情信息呀~這是需求詳情信息呀~這是需求詳情信息呀~這是需求詳情信息呀~'
        })
    }, [demandId])

    return (<div>
        <h3>項(xiàng)目名稱為:{info.name}({status})</h3>
        <p>{info.detail}</p>
    </div>)
}

export default DemandList

效果:

自定義 hook
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 概述 Hook 使你在非 class 的情況下可以使用更多的 React 特性。Hook 是一些可以在函數(shù)組件里鉤...
    bowen_wu閱讀 439評論 0 0
  • Hooks是 React v16.8 的新特性,可以在不使用類組件的情況下,使用 state 以及其他的React...
    hellomyshadow閱讀 13,703評論 0 5
  • Hook Hook 是 React 16.8.0 的新增特性。 Hook 使你在非 class 的情況下可以使用更...
    tigerHee閱讀 430評論 0 0
  • 你還在為該使用無狀態(tài)組件(Function)還是有狀態(tài)組件(Class)而煩惱嗎?——擁有了hooks,你再也不需...
    水落斜陽閱讀 82,493評論 11 100
  • Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他...
    Oldboyyyy閱讀 4,927評論 0 3

友情鏈接更多精彩內(nèi)容