React Hook 不徹底指南

前言

本文內容大部分參考了 overreacted.io 博客一文,同時結合 React Hook 官方 文章,整理並概括一些筆記和輸出我的的一些理解

什麼是 Hook ?

官方介紹:Hook 是 React 16.8 的新增特性。它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。

React 中內置的 Hook API

  • 基礎 Hookjavascript

    • useStatehtml

      // 傳入初始值,做爲 state
      const [state, setState] = useState(initialState)
      
      //  `惰性初始 state`;傳入函數,由函數計算出的值做爲 state
      // 此函數只在初始渲染時被調用
      const [state, setState] = useState(() => {
        const initialState = someExpensiveComputation(props)
        return initialState
      })
    • useEffectjava

      • 該 Hook 接收一個包含命令式、且可能有反作用代碼的函數.
      • 在函數組件主體內(這裏指在 React 渲染階段)改變 DOM、添加訂閱、設置定時器、記錄日誌以及執行其餘包含反作用的操做都是不被容許的,由於這可能會產生莫名其妙的 bug 並破壞 UI 的一致性。
      • 使用 useEffect 完成反作用操做,賦值給 useEffect 的函數會在組件渲染到屏幕以後。你能夠把 effect 看做從 React 的純函數式世界通往命令式世界的逃生通道。
      • 默認狀況下,effect 將在每輪渲染結束後執行,但你能夠選擇讓它 在只有某些值改變的時候才執行。詳情見後面。
      • 清除 effect
        一般,組件卸載時須要清除 effect 建立的諸如訂閱或計時器 ID 等資源。要實現這一點,useEffect 函數需返回一個清除函數。如下就是一個建立訂閱的例子:react

        useEffect(() => {
          const subscription = props.source.subscribe()
          return () => {
            // 清除訂閱
            subscription.unsubscribe()
          }
        }, [依賴])
    • useContext
  • 額外的 Hookios

    • useReducer
    • useCallback
    • useMemo
    • useRef
    • useImperativeHandle
    • useLayoutEffect
    • useDebugValue

咱們爲何選擇使用 Hook ?

1. 在組件之間複用狀態邏輯很難

React 沒有提供將可複用性行爲「附加」到組件的途徑(例如,把組件鏈接到 store)。若是你使用過 React 一段時間,你也許會熟悉一些解決此類問題的方案,好比 render props 和 高階組件。可是這類方案須要從新組織你的組件結構,這可能會很麻煩,使你的代碼難以理解。若是你在 React DevTools 中觀察過 React 應用,你會發現由 providers,consumers,高階組件,render props 等其餘抽象層組成的組件會造成「嵌套地獄」。儘管咱們能夠在 DevTools 過濾掉它們,但這說明了一個更深層次的問題:React 須要爲共享狀態邏輯提供更好的原生途徑。

你可使用 Hook 從組件中提取狀態邏輯,使得這些邏輯能夠單獨測試並複用。Hook 使你在無需修改組件結構的狀況下複用狀態邏輯。 這使得在組件間或社區內共享 Hook 變得更便捷。ajax

2. 複雜組件變得難以理解

咱們常常維護一些組件,組件起初很簡單,可是逐漸會被狀態邏輯和反作用充斥。每一個生命週期經常包含一些不相關的邏輯。例如,組件經常在 componentDidMount 和 componentDidUpdate 中獲取數據。可是,同一個 componentDidMount 中可能也包含不少其它的邏輯,如設置事件監聽,而以後需在 componentWillUnmount 中清除。相互關聯且須要對照修改的代碼被進行了拆分,而徹底不相關的代碼卻在同一個方法中組合在一塊兒。如此很容易產生 bug,而且致使邏輯不一致。

在多數狀況下,不可能將組件拆分爲更小的粒度,由於狀態邏輯無處不在。這也給測試帶來了必定挑戰。同時,這也是不少人將 React 與狀態管理庫結合使用的緣由之一。可是,這每每會引入了不少抽象概念,須要你在不一樣的文件之間來回切換,使得複用變得更加困難。npm

爲了解決這個問題,Hook 將組件中相互關聯的部分拆分紅更小的函數(好比設置訂閱或請求數據),而並不是強制按照生命週期劃分。你還可使用 reducer 來管理組件的內部狀態,使其更加可預測。json

3. 用更少的代碼,實現一樣的效果

下面的代碼能夠直觀的體現出來,在某些場景下,使用 hook 來實現對應的功能,能夠節省大部分的代碼axios

Edit CounterHook vs CounterClass

3.1 清除反作用更加緊湊

對比 Class 組件來講,清除反作用要簡單的多,以下代碼,在 useEffect hook 裏面返回一個函數,當咱們的函數組件卸載的時候,就會自動執行這個函數,從而來清除反作用。想一想咱們在 Class 組件裏面須要在 componentWillUnmount 生命週期裏面去編寫對應的代碼。windows

對比二者咱們發現,使用 useEffect 的方式,可以將掛載和卸載的邏輯更加緊密的耦合在一塊兒,從而減小 BUG 的發生

useEffect(() => {
  const id = setInterval(() => {
    setCount(count => count + 1)
  }, 1000)
  return () => clearInterval(id)
}, [])

// 好比給 windows 掛載監聽函數
useEffect(() => {
  window.addEventListener('reszie', handleRezie)

  return () => {
    window.removeEventListener('resize', handleRezie)
  }
}, [])

如何正確的使用 Hook ?

1. 使用規則

  1. 只在最頂層使用 Hook:不要在循環,條件或嵌套函數中調用 Hook, 確保老是在你的 React 函數的最頂層調用他們。
  2. 不要在普通的 JavaScript 函數中調用 Hook。你能夠

    • [x] 在 React 的函數組件中調用 Hook
    • [x] 在自定義 Hook 中調用其餘 Hook

2. 只有在本身依賴更新時才執行 effect

使用 useEffect 完成反作用操做,賦值給 useEffect 的函數會在組件渲染到屏幕以後;牢記這句話。

仔細觀察以下代碼,當函數組件裏面,有多個 effect 的時候,默認的 effect 將在每次 UI render 以後被調用。當咱們經過 useEffect 的第二個數組類型參數,指明當前 effect 的依賴,就能避免不相關的執行開銷了。

經過啓用 eslint-plugin-react-hooks 插件,來強制提醒咱們在使用 effect 的時候,申明所須要的依賴

{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}
const CounterHook = () => {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('heaven')

  useEffect(() => {
    document.title = `counterWithHook ${count}`
  }, [count])

  useEffect(() => {
    console.log('you name is', name)
  }, [name])

  return (
    <div>
      <h3>Counter with Hook</h3>
      <p>You click {count} times</p>
      <button onClick={e => setCount(count => count + 1)}>Click me</button>
      <p>
        <input placeholder="輸入姓名" onChange={e => setName(e.target.value)} />
        <br />
        your name is {name}
      </p>
    </div>
  )
}

2.1 不要忘記函數依賴

對於 useEffect 內部方法,一旦引用外部的函數,那麼這個時候須要注意了:
須要把 useEffect 內部引用到的方式,聲明爲當前 effect 的依賴
在下圖的代碼中,咱們能夠看到,在 effect 函數內部,引入外部的函數,咱們的 eslint-plugin-react-hooks 插件會自動提示咱們須要把對應的函數做爲依賴添加進去

不規範示例:這裏在安裝了插件的狀況下,會自動提示咱們將 fetchData 函數移入 effect 內部

const getFetchUrl = () => {
  return `https://hn.algolia.com/api/v1/search?query=${query}`
}

const fetchData = async () => {
  return axios.get(getFetchUrl())
}

useEffect(() => {
  fetchData().then(resp => {
    console.log(resp)
    setData(resp.data)
  })
}, [])

正確的寫法:

useEffect(() => {
  const getFetchUrl = () => {
    return `https://hn.algolia.com/api/v1/search?query=${query}`
  }

  const fetchData = async () => {
    return axios.get(getFetchUrl())
  }

  fetchData().then(resp => {
    console.log(resp)
    setData(resp.data)
  })
}, [query])

三、理解每一次的 Rendering

每一次渲染都有它本身的 Props and State
每一次渲染都有它本身的事件處理函數
每次渲染都有它本身的 Effects

運行以下代碼以後,在咱們點擊 Show alert 按鈕以後,而後點擊 Click me 按鈕,alert 輸出的永遠是在點擊的那個時刻的 count;

換句話來講;在 hooks 組件裏面,每一次的渲染,都至關於記錄當前次的『快照』

import React, { useEffect, useState } from 'react'
const Counter = () => {
  const [count, setCount] = useState(0)

  const handleAlertClick = () => {
    setTimeout(() => {
      alert(`Yout clicked me: ${count}`)
    }, 3000)
  }

  useEffect(() => {
    setTimeout(() => {
      console.log(`Yout clicked ${count} times`)
    }, 3000)
  })

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  )
}

export default Counter

使用自定義 Hook

經過自定義 Hook,能夠將組件邏輯提取到可重用的函數中。

當咱們想在兩個函數之間共享邏輯時,咱們會把它提取到第三個函數中。而組件和 Hook 都是函數,因此也一樣適用這種方式。

自定義 Hook 是一個函數,其名稱以 「use」 開頭,函數內部能夠調用其餘的 Hook。

自定義 useService hook

useService.js 自定義的一個 server hook,該 hook 封裝了 ajax 請求中的 { loading, error, response } 三個基礎邏輯;有了這個 hook 咱們就能很輕鬆的在每次網絡請求裏面去處理各類異常邏輯了;詳細用法看文章最後的 Table 分頁操做實例

import { useEffect, useRef, useState, useCallback } from 'react'
import { isEqual } from 'lodash'

const useService = (service, params) => {
  const prevParams = useRef(null)
  const [callback, { loading, error, response }] = useServiceCallback(service)

  useEffect(() => {
    if (!isEqual(prevParams.current, params)) {
      prevParams.current = params
      callback(params)
    }
  })

  return { loading, error, response }
}

const useServiceCallback = service => {
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(null)
  const [response, setResponse] = useState(null)

  // 使用 useCallback,來判斷 service 是否改變
  const callback = useCallback(
    params => {
      setLoading(true)
      setError(null)
      service(params)
        .then(response => {
          console.log(response)
          setLoading(false)
          setResponse(response)
        })
        .catch(error => {
          setLoading(false)
          setError(error)
        })
    },
    [service]
  )

  return [callback, { loading, error, response }]
}

實例剖析

Table 分頁操做

以下代碼,使用 hook 的方式來實現表格的分頁,數據請求操做,

Edit 0o8169rx4w

跑馬燈中獎

使用 hook 實現一個簡易版的跑馬燈抽獎邏輯

Edit react-prize

參考資料

官方 Hook 介紹
Hook 規則
Hook API 索引
如何在 Hook 中發起請求
useEffect 詳解

相關文章
相關標籤/搜索