本文內容大部分參考了 overreacted.io 博客一文,同時結合 React Hook 官方 文章,整理並概括一些筆記和輸出我的的一些理解javascript
官方介紹:Hook 是 React 16.8 的新增特性。它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。html
// 傳入初始值,做爲 state
const [state, setState] = useState(initialState)
// `惰性初始 state`;傳入函數,由函數計算出的值做爲 state
// 此函數只在初始渲染時被調用
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props)
return initialState
})
複製代碼
該 Hook 接收一個包含命令式、且可能有反作用代碼的函數.java
在函數組件主體內(這裏指在 React 渲染階段)改變 DOM、添加訂閱、設置定時器、記錄日誌以及執行其餘包含反作用的操做都是不被容許的,由於這可能會產生莫名其妙的 bug 並破壞 UI 的一致性。react
使用 useEffect 完成反作用操做,賦值給 useEffect 的函數會在組件渲染到屏幕以後。你能夠把 effect 看做從 React 的純函數式世界通往命令式世界的逃生通道。ios
默認狀況下,effect 將在每輪渲染結束後執行,但你能夠選擇讓它 在只有某些值改變的時候才執行。詳情見後面。ajax
清除 effect 一般,組件卸載時須要清除 effect 建立的諸如訂閱或計時器 ID 等資源。要實現這一點,useEffect 函數需返回一個清除函數。如下就是一個建立訂閱的例子:npm
useEffect(() => {
const subscription = props.source.subscribe()
return () => {
// 清除訂閱
subscription.unsubscribe()
}
}, [依賴])
複製代碼
React 沒有提供將可複用性行爲「附加」到組件的途徑(例如,把組件鏈接到 store)。若是你使用過 React 一段時間,你也許會熟悉一些解決此類問題的方案,好比 render props 和 高階組件。可是這類方案須要從新組織你的組件結構,這可能會很麻煩,使你的代碼難以理解。若是你在 React DevTools 中觀察過 React 應用,你會發現由 providers,consumers,高階組件,render props 等其餘抽象層組成的組件會造成「嵌套地獄」。儘管咱們能夠在 DevTools 過濾掉它們,但這說明了一個更深層次的問題:React 須要爲共享狀態邏輯提供更好的原生途徑。json
你可使用 Hook 從組件中提取狀態邏輯,使得這些邏輯能夠單獨測試並複用。Hook 使你在無需修改組件結構的狀況下複用狀態邏輯。 這使得在組件間或社區內共享 Hook 變得更便捷。axios
咱們常常維護一些組件,組件起初很簡單,可是逐漸會被狀態邏輯和反作用充斥。每一個生命週期經常包含一些不相關的邏輯。例如,組件經常在 componentDidMount 和 componentDidUpdate 中獲取數據。可是,同一個 componentDidMount 中可能也包含不少其它的邏輯,如設置事件監聽,而以後需在 componentWillUnmount 中清除。相互關聯且須要對照修改的代碼被進行了拆分,而徹底不相關的代碼卻在同一個方法中組合在一塊兒。如此很容易產生 bug,而且致使邏輯不一致。windows
在多數狀況下,不可能將組件拆分爲更小的粒度,由於狀態邏輯無處不在。這也給測試帶來了必定挑戰。同時,這也是不少人將 React 與狀態管理庫結合使用的緣由之一。可是,這每每會引入了不少抽象概念,須要你在不一樣的文件之間來回切換,使得複用變得更加困難。
爲了解決這個問題,Hook 將組件中相互關聯的部分拆分紅更小的函數(好比設置訂閱或請求數據),而並不是強制按照生命週期劃分。你還可使用 reducer 來管理組件的內部狀態,使其更加可預測。
下面的代碼能夠直觀的體現出來,在某些場景下,使用 hook 來實現對應的功能,能夠節省大部分的代碼
對比 Class
組件來講,清除反作用要簡單的多,以下代碼,在 useEffect
hook 裏面返回一個函數,當咱們的函數組件卸載的時候,就會自動執行這個函數,從而來清除反作用。想一想咱們在 Class
組件裏面須要在 componentWillUnmount
生命週期裏面去編寫對應的代碼。
對比二者咱們發現,使用 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)
}
}, [])
複製代碼
使用 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> ) } 複製代碼
對於 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])
複製代碼
每一次渲染都有它本身的 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 是一個函數,其名稱以 「use」 開頭,函數內部能夠調用其餘的 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 }]
}
複製代碼
以下代碼,使用 hook
的方式來實現表格的分頁,數據請求操做,
使用 hook
實現一個簡易版的跑馬燈抽獎邏輯