
一文詳解react-hooks
- 🎙?前言
- 一、📻概述
- 1、關于React Hooks
- 2、認識React Hooks
- (1)回顧React函式式組件
- (2)函陣列件的特點
- (3)class組件的問題
- (4)React 組件
- 二、🪕幾種 Hooks
- 1、State Hook🗞?
- (1)讓函陣列件實作state和setState
- (2)舉例闡述
- (3)useState使用總結
- (4)Hooks命名規范
- 2、Effect Hook🗞?
- (1)讓函陣列件模擬生命周期
- (2)舉例闡述
- (3)useEffect使用總結
- (4)useEffect讓純函式有了副作用
- (5)useEffect中回傳函式 fn
- 3、其他Hooks🗞?
- (1)useRef
- (2)useContext
- (3)useReducer
- (4)useMemo
- (5)useCallback
- 4、自定義Hook🗞?
- (1)為什么要使用自定義Hook
- (2)舉例闡述
- (3)總結
- 5、Hooks的兩條重要規則🗞?
- (1)Hooks使用規范
- (2)Hooks 呼叫順序必須保持一致
- 三、??React-Hooks組件邏輯復用
- 1、class組件的邏輯復用
- (1)Mixin
- (2)高階組件 HOC
- (3)Render Prop
- 2、使用 Hooks 做組件邏輯復用
- (1)例子闡述
- (2)好處
- 四、📀React Hooks 注意事項
- 1、useState初始化值,只有第一次有效
- 2、useEffect內部不能修改state
- 3、useEffect可能出現死回圈
- 五、🔍結束語
- 🐣彩蛋 One More Thing
- (:往期推薦
- (:叮
🎙?前言
相傳, react 17 出了一個很強大的功能, 也就是 react hooks ,實際上, react hooks 有點類似與 vue3 的 composition API ,都是為了提升開發效率而出現,
那么,在下面的文章中,將從 0到1 開始,帶大家了解 react hooks ,以及一些常用的 API ,
廢話不多說,開始 react-hook 之旅叭~
一、📻概述
1、關于React Hooks
React Hooks是一個可選功能,通常用class組件 來和它做比較;100%向后兼容,沒有破壞性改動;- 不會取代
class組件,尚無計劃要移除class組件,
2、認識React Hooks
(1)回顧React函式式組件
我們先來回顧下 class 組件 和 函陣列件,具體如下:
class組件:
// class組件
class List extends React.Component {
constructor() {
super(props)
}
render() {
const { List } = this.props
return <ul>{list.map((item, index) => {
return <li key={item.id}>
<span>{item.title}</span>
</li>
})}</ul>
}
}
函陣列件:
// 函陣列件
function List(props) {
const { list } = props
return <ul>{list.map((item, index) => {
return <li key={item.id}>
<span>{item.title}</span>
</li>
})}</ul>
}
(2)函陣列件的特點
函陣列件的特點是:
- 沒有組件實體;
- 沒有生命周期;
- 沒有
state和setState,只能接收props,
(3)class組件的問題
上面我們說到了函陣列件是一個純函式,只能接收 props ,沒有任何其他功能,而 class 組件擁有以上功能,但是呢,class 組件會存在以下問題:
- 大型組件很難拆分和重構,很難測驗(即
class不易拆分); - 相同業務邏輯,分散到各個方法中,邏輯混亂;
- 復用邏輯變得復雜,如
Mixins、HOC、Render Props,
因此,有了以上問題的出現,也就有了 React Hooks ,
(4)React 組件
-
React組件更易于用函式來表達: -
React提倡函式式編程,即view=fn(props); -
函式更靈活,更易拆分,更易測驗;
-
但函陣列件太簡單,需要增強能力 ——因此,有了
React Hooks,
二、🪕幾種 Hooks
1、State Hook🗞?
(1)讓函陣列件實作state和setState
- 默認函陣列件是沒有
state的; - 函陣列件是一個純函式,執行完即銷毀,無法存盤
store; - 需要
State Hook,即把store功能 “鉤” 到純函式中,
(2)舉例闡述
假設我們現在要通過點擊按鈕,來修改設定的某個值,現在我們用 hooks 中的 useState 來處理,具體代碼如下:
import React, { useState } from 'react'
function ClickCounter() {
// 陣列的解構
// useState 就是一個 Hook “鉤”,最基本的一個 Hook
const [count, setCount] = useState(0) // 傳入一個初始值,初始值可以是數值/字串/陣列/物件等型別
const [name, setName] = useState('星期一研究室')
// const arr = useState(0)
// const count = arr[0]
// const setCount = arr[1]
function clickHandler() {
setCount(count + 1)
setName(name + '2021')
}
return <div>
<p>你點擊了 {count} 次 {name}</p>
<button onClick={clickHandler}>點擊</button>
</div>
}
export default ClickCounter
此時瀏覽器的顯示效果如下:

在上面的代碼中, count 是一個 state 的值, setCount 是修改 state 的一個函式,同理, name 也是一個 state 的值, setName 是修改 name 值得一個函式,
如果使用 hooks 來修改 state 值的話,那么我們只需要以 const [count, setCount] = useState(0) 這種形式去進行,便可修改最后的值,而不需要像 class 組件那么復雜的去使用,
對于上面這個功能來說,如果使用 class 組件來實作的話,具體代碼如下:
import React from 'react'
class ClickCounter extends React.Component {
constructor() {
super()
// 定義 state
this.state = {
count: 0,
name: '星期一研究室'
}
this.clickHandler = this.clickHandler.bind(this)
}
render() {
return <div>
<p>你點擊了 {this.state.count} 次 {this.state.name}</p>
<button onClick={this.clickHandler}>點擊</button>
</div>
}
clickHandler() {
// 修改 state
this.setState({
count: this.state.count + 1,
name: this.state.name + '2021'
})
}
}
export default ClickCounter
大家可以看到,如果使用 class 組件來解決的話,那么需要先定義 state ,然后再定義一個函式,再使用 setState 才能去修改值,這樣似憾訓麻煩了點,
相信到這里,大家已經感受到 hooks 的快樂之處了,
下面,我們來總結關于 useState 的一些知識點,
(3)useState使用總結
useState(xxx)傳入初始值,回傳陣列[state, setState];- 通過
state獲取值; - 通過
setState(xxx)修改值,
(4)Hooks命名規范
- 規定所有的
Hooks都要使用use開頭,如useXxx; - 自定義
Hook也要以use開頭; - 非
Hooks的地方,盡量不要使用useXxx寫法,不然容易造成誤解,
2、Effect Hook🗞?
(1)讓函陣列件模擬生命周期
- 默認函陣列件沒有生命周期;
- 函陣列件是一個純函式,執行完即銷毀,自己無法實作生命周期;
- 使用
Effect Hook把生命周期 “鉤” 到純函式中,
(2)舉例闡述
同樣地,我們用于 useState 相同的例子,來體驗下 useEffect ,
我們先在 src|components 下建立一個檔案,命名為 LiftCycles.js , 具體代碼如下:
import React, { useState, useEffect } from 'react'
function LifeCycles() {
const [count, setCount] = useState(0)
const [name, setName] = useState('星期一研究室')
// 模擬 class 組件的 DidMount 和 DidUpdate
useEffect(() => {
console.log('在此發送一個 ajax 請求')
})
function clickHandler() {
setCount(count + 1)
setName(name + '2020')
}
return <div>
<p>你點擊了 {count} 次 {name}</p>
<button onClick={clickHandler}>點擊</button>
</div>
}
export default LifeCycles
現在,我們在 App.js 中注冊該組件,具體代碼如下:
import React, { useState } from 'react';
import LifeCycles from './components/LifeCycles'
function App() {
const [flag, setFlag] = useState(true)
return (
<div
{flag && <LifeCycles/>}
</div>
);
}
export default App;
此時,瀏覽器的顯示效果如下:

大家可以看到, useEffect 如果只傳入一個函式的話,那么它模擬的是 DidMount 和 DidUpdate 這兩個生命周期的功能,每當我們點擊一次的時候,瀏覽器就會列印一次,也就是組件加載和組件更新是在一起進行的,
那如果我們想要讓組件加載和組件更新分開實作呢,同樣有辦法,我們來改造下 LiftCycles.js 的代碼,具體代碼如下:
import React, { useState, useEffect } from 'react'
function LifeCycles() {
const [count, setCount] = useState(0)
const [name, setName] = useState('星期一研究室')
// 模擬 class 組件的 DidMount
useEffect(() => {
console.log('加載完了')
}, []) // 第二個引數是 [] (不依賴于任何 state)
// 模擬 class 組件的 DidUpdate
useEffect(() => {
console.log('更新了')
}, [count, name]) // 第二個引數就是依賴的 state
function clickHandler() {
setCount(count + 1)
setName(name + '2020')
}
return <div>
<p>你點擊了 {count} 次 {name}</p>
<button onClick={clickHandler}>點擊</button>
</div>
}
export default LifeCycles
此時瀏覽器的顯示效果如下:

大家可以看到,第一次的時候分別加載和更新了 1 次,等到我們去點擊的時候,因為已經加載完了,所以這個時候就是更新了,
那我們來梳理一下, useEffect 在加載和更新的時候,分別是怎么進行處理的?
看上面的代碼中我們可以發現,useEffect 還可以接收第二個引數,第二個引數如果空值,那么它不依賴于任何 state ,表示 componentDidMount 這個生命周期,相反,如果第二個引數沒有依賴值或者接收依賴于 state 的值這兩種情況時,那么它模擬 componentDidUpdate 這個生命周期,
說到這里,可能有的小伙伴還想問,那跟銷毀相關的生命周期,又怎么處理呢?我們繼續來對 LiftCycles.js 的代碼進行改造,具體如下:
import React, { useState, useEffect } from 'react'
function LifeCycles() {
const [count, setCount] = useState(0)
const [name, setName] = useState('星期一研究室')
// 模擬 class 組件的 DidMount
useEffect(() => {
let timerId = window.setInterval(() => {
console.log(Date.now())
}, 1000)
// 回傳一個函式
// 模擬 WillUnMount
return () => {
window.clearInterval(timerId)
}
}, [])
function clickHandler() {
setCount(count + 1)
setName(name + '2020')
}
return <div>
<p>你點擊了 {count} 次 {name}</p>
<button onClick={clickHandler}>點擊</button>
</div>
}
export default LifeCycles
大家可以看以上代碼,在這里,我們通過 return 一個函式,去模擬 componentWillUnMount 這個生命周期,來銷毀每一次定時器中執行的任務,
到這里,相信大家對 useEffect 已經有了一定的了解,現在,我們來對 useEffect 做個小結,
(3)useEffect使用總結
- 模擬
componentDidMount - useEffect依賴[]; - 模擬
componentDidUpdate - useEffect無依賴,或者依賴[a, b]; - 模擬
componentWillUnMount - useEffect中回傳一個函式,
(4)useEffect讓純函式有了副作用
- 默認情況下,執行純函式的時候,只需要輸入引數并回傳結果即可,是沒有任何副作用的;
- 所謂副作用,就是對函式之外造成影響,如設定全域定時任務;
- 而組件需要副作用,所以需要
useEffect“鉤” 到純函式中; - 因此,
useEffect這個副作用并不是一件壞事,
(5)useEffect中回傳函式 fn
有一種值得注意的情況是,在上面我們通過回傳一個函式來模擬 WillUnMount ,但這個模擬后的結果還不完全等于 WillUnMount ,現在,我們在 src|components 下建立一個檔案,命名為 FriendStatus.js ,具體代碼如下:
import React, { useState, useEffect } from 'react'
function FriendStatus({ friendId }) {
const [status, setStatus] = useState(false)
// DidMount 和 DidUpdate
useEffect(() => {
console.log(`開始監聽 ${friendId} 在線狀態`)
// 【特別注意】
// 此處并不完全等同于 WillUnMount
// props 發生變化,即更新,也會執行結束監聽
// 準確的說:回傳的函式,會在下一次 effect 執行之前,被執行
return () => {
console.log(`結束監聽 ${friendId} 在線狀態`)
}
})
return <div>
好友 {friendId} 在線狀態:{status.toString()}
</div>
}
export default FriendStatus
App.js 的代碼如下:
import React, { useState } from 'react';
import FriendStatus from './components/FriendStatus'
function App() {
const [flag, setFlag] = useState(true)
const [id, setId] = useState(1)
return (
<div>
<div>
<button onClick={() => setFlag(false)}>flag = false</button>
<button onClick={() => setId(id + 1)}>id++</button>
</div>
{flag && <FriendStatus friendId={id}/>}
</div>
);
}
export default App;
此時,瀏覽器的顯示效果如下:

大家可以看到,當開始下一個監聽時,會先結束掉上一個監聽再開始下一個,正如圖中所看到的,一開始我們正在監聽 id 為 1 的好友,那當我們想要點擊 id++ 按鈕去監聽好友 2 時,useEffect 會先去結束掉 1 的狀態,再讓好友 2 上線,
此時,我們要注意的一個點是,執行結束監聽的這個函式,執行的是 DidUpdate 生命周期,而不是 WillUnMount 生命周期,函式在這個時候是屬于更新狀態而不是銷毀狀態,
依據以上內容,我們來做個小結,具體如下:
- 當
useEffect依賴于[]的時候,在組件銷毀時執行return的函式fn,等于WillUnMount生命周期; - 當
useEffect無依賴或依賴于[a, b]的時候,組件是在更新時執行return的函式fn;即在下一次執行useEffect之前,就會先執行fn,此時模擬的是DidUpdate生命周期,
3、其他Hooks🗞?
上面我們講了兩個比較常用的 hooks → useState 和 useEffect ,接下來,我們來了解其他幾個比較不常用的 hooks ,
(1)useRef
Ref 是用于對 值的修改 和 DOM 元素的獲取 ,先來看一段代碼:
import React, { useRef, useEffect } from 'react'
function UseRef() {
const btnRef = useRef(null) // 初始值
const numRef = useRef(0)
numRef.current = 2;
useEffect(() => {
console.log(btnRef.current) // DOM 節點
console.log(numRef.current); // 得到值
}, [])
return <div>
<button ref={btnRef}>click</button>
</div>
}
export default UseRef
此時,瀏覽器的顯示效果為:

大家可以看到,如果將 btnRef 系結到 ref={btnRef} 上,那么 btnRef.current 獲取到的是當前的 DOM 節點,
另外一種情況是,大家定位到 numRef ,如果我們給其進行賦值并修改,那么 numRef.current 最終得到的值是修改后的值,
(2)useContext
很多時候,我們經常會有一些比較靜態的屬性需要做切換,比如:主題顏色切換,這個時候就需要用到 context 背景關系來處理,那在 hook 中,就有 useContext 可以處理這件事情,先來看一段演示代碼:
import React, { useContext } from 'react'
// 主題顏色
const themes = {
light: {
foreground: '#000',
background: '#eee'
},
dark: {
foreground: '#fff',
background: '#222'
}
}
// 創建 Context
const ThemeContext = React.createContext(themes.light) // 初始值
function ThemeButton() {
const theme = useContext(ThemeContext)
return <button style={{ background: theme.background, color: theme.foreground }}>
hello world
</button>
}
function Toolbar() {
return <div>
<ThemeButton></ThemeButton>
</div>
}
function App() {
return <ThemeContext.Provider value={themes.dark}>
<Toolbar></Toolbar>
</ThemeContext.Provider>
}
export default App
此時瀏覽器的顯示效果如下:

現在屬于黑色背景的主題,如果我們要切換為白色背景的主題,那么只需要把最底下的代碼 value={themes.dark} 改為 value={themes.light} 即可,
(3)useReducer
在 redux 中,我們會用到 reducer ,但 useReducer 跟 redux 還有一點區別, useReducer 是對單個組件進行狀態
先來看一段代碼:
import React, { useReducer } from 'react'
const initialState = { count: 0 }
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
default:
return state
}
}
function App() {
// 很像 const [count, setCount] = useState(0)
const [state, dispatch] = useReducer(reducer, initialState)
return <div>
count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>decrement</button>
</div>
}
export default App
此時瀏覽器的顯示效果如下:

我們將要改變的值放到 reducer 當中,之后再將 reducer 傳遞給 useRouter ,最后,通過 dispatch 來對值進行系結,
下面我們來梳理一下 useRuducer 和 redux 的區別:
useReducer是useState的代替方案,用于處理state的復雜變化;useReducer是單個組件狀態管理,組件通訊還需要props;redux是全域的狀態管理,多組件共享資料,
(4)useMemo
useMemo 是 hooks 中性能優化的一部分,當我們不使用 useMemo 時,狀態時這樣的,看以下這段代碼:
import React, { useState } from 'react'
// 子組件
function Child({ userInfo }) {
console.log('Child render...', userInfo)
return <div>
<p>This is Child {userInfo.name} {userInfo.age}</p>
</div>
}
// 父組件
function App() {
console.log('Parent render...')
const [count, setCount] = useState(0)
const [name, setName] = useState('星期一研究室')
const userInfo = { name, age: 20 }
return <div>
<p>
count is {count}
<button onClick={() => setCount(count + 1)}>click</button>
</p>
<Child userInfo={userInfo}></Child>
</div>
}
export default App
此時,控制臺的列印效果如下:

大家可以看到,每每我們點擊一次,不管 state 的值是否更新時,子組件 Child 都會重新列印一次, 這對于程式來說,是比較耗費性能的,因此,我們要用 useMemo 來阻止 Child 事件頻繁列印,提高程式的性能,代碼改造如下:
import React, { useState, memo, useMemo } from 'react'
// 子組件
// 類似 class PureComponent ,對 props 進行淺層比較
const Child = memo(({ userInfo }) => {
console.log('Child render...', userInfo)
return <div>
<p>This is Child {userInfo.name} {userInfo.age}</p>
</div>
})
// 父組件
function App() {
console.log('Parent render...')
const [count, setCount] = useState(0)
const [name, setName] = useState('星期一研究室')
// 用 useMemo 快取資料,依賴于 [name]
// 當name有更新時,{ name, age: 18 } 這個快取就會失效
const userInfo = useMemo(() => {
return { name, age: 18 }
}, [name])
return <div>
<p>
count is {count}
<button onClick={() => setCount(count + 1)}>click</button>
</p>
<Child userInfo={userInfo}></Child>
</div>
}
export default App
此時,瀏覽器的顯示效果如下:

大家可以看到, Child 組件除了第一次更新有列印之外,之后的沒有更新就不會再進行列印,那它是怎么使用的呢,首先,我們要用 memo 把子組件給包裹下來,之后呢,通過 useMemo 去快取資料,同時這個資料要依賴于 [name] ;值得注意的是,如果不依賴于 [name] ,它是不會生效的,
現在,我們來對 useMemo 做個總結,具體如下:
React默認會更新所有子組件;class組件使用shouldComponentUpdate和PureComponent做優化;Hooks中使用useMemo做優化,但優化原理和class組件是相同的,
(5)useCallback
上面我們說到的是使用 useMemo 來快取資料,現在我們來講下 useCallback , useCallback 主要是用來快取函式,
假設我們現在不使用 useCallback ,來運行下面這段代碼:
import React, { useState, memo, useMemo, useCallback } from 'react'
// 子組件,memo 相當于 PureComponent
const Child = memo(({ userInfo, onChange }) => {
console.log('Child render...', userInfo)
return <div>
<p>This is Child {userInfo.name} {userInfo.age}</p>
<input onChange={onChange}></input>
</div>
})
// 父組件
function App() {
console.log('Parent render...')
const [count, setCount] = useState(0)
const [name, setName] = useState('星期一研究室')
// 用 useMemo 快取資料
const userInfo = useMemo(() => {
return { name, age: 21 }
}, [name])
function onChange(e) {
console.log(e.target.value)
}
return <div>
<p>
count is {count}
<button onClick={() => setCount(count + 1)}>click</button>
</p>
<Child userInfo={userInfo} onChange={onChange}></Child>
</div>
}
export default App
此時瀏覽器的列印效果如下:

大家可以看到,如果沒有使用 useCallback ,那么其會一直將子組件列印出來,也就是 onChange 事件會一直運轉,現在,我們將 onChange 事件改造一下,具體如下:
// 用 useCallback 快取函式
const onChange = useCallback(e => {
console.log(e.target.value)
}, [])
此時我們來看下瀏覽器運行的效果:

大家可以看到,加上 useCallback 之后,且 Child 沒有更新時,也就不會再更新啦!
接下來我們對 useMemo 和 useCallback 做個小結:
useMemo快取資料;useCallback快取函式;- 兩者都是
React Hooks的常見優化策略,
4、自定義Hook🗞?
(1)為什么要使用自定義Hook
- 用來封裝通用的功能;
- 可以開發和使用第三方
Hooks; - 自定義
Hook帶來了無限的擴展性,解耦代碼,
(2)舉例闡述
假設我們現在想要封裝一個 useAxios ,那怎么實作呢?
首先我們需要先在專案中安裝 axios ,具體代碼如下:
npm i axios --save
接著,我們在專案的 src|customHooks 檔案夾下新建一個檔案,命名為 useAxios.js ,具體代碼如下:
import { useState, useEffect } from 'react'
import axios from 'axios'
// 封裝 axios 發送網路請求的自定義 Hook
function useAxios(url) {
// loading 模擬當前是否再等待中
const [loading, setLoading] = useState(false)
// 請求成功時回傳的資料
const [data, setData] = useState()
// 請求失敗時回傳的一些資訊
const [error, setError] = useState()
useEffect(() => {
// 利用 axios 發送網路請求
setLoading(true)
axios.get(url) // 發送一個 get 請求
.then(res => setData(res))
.catch(err => setError(err))
.finally(() => setLoading(false))
}, [url])
return [loading, data, error]
}
export default useAxios
繼續,我們在專案的 src|components 檔案夾,新建一個檔案,命名為 CustomHookUsage.js ,這個組件主要用來使用上面所自定義的 useAxios 這個 hook ,具體代碼如下:
import React from 'react'
import useAxios from '../customHooks/useAxios'
function App() {
const url = 'http://localhost:3000/'
// 陣列解構
const [loading, data, error] = useAxios(url)
if (loading) return <div>loading...</div>
return error
? <div>{JSON.stringify(error)}</div>
: <div>{JSON.stringify(data)}</div>
}
export default App
最后,我們在專案的 App.js 中來使用它,具體代碼如下:
import React, { useState } from 'react';
import CustomHookUsage from './components/CustomHookUsage'
function App() {
return (
<div>
{<CustomHookUsage/>}
</div>
);
}
export default App;
此時,我們來看下瀏覽器的顯示效果:

大家可以看到,首先是在等待的時候,它會先出現 loading ,之后呢,如果在等待結束并請求成功后,會回傳 data ,當然,我們這里沒有演示 error 的情況,大家可以把 url 修改為不存在的請求地址,即可測驗 error 的情況,
(3)總結
看完上面的演示代碼以后,我們來做個小結:
- 自定義
hook本質是一個函式,以use開頭; - 內部可以正常使用
useState、useEffect或者其他Hooks; - 自定義回傳結果,格式不限;
這里給大家推薦兩個第三方自定義 Hook 的庫:
- react-hooks: https://nikgraf.github.io/react-hooks/
- hooks: https://github.com/umijs/hooks
5、Hooks的兩條重要規則🗞?
(1)Hooks使用規范
-
Hooks只能用于react函陣列件和自定義Hooks中,其他地方都不可以,比如:class組件 和 普通函式; -
只能用于頂層代碼,不能在回圈、判斷中使用
Hooks; -
eslint中的插件 eslint-plugin-react-hooks ,可以幫到你使用react-hooks,如下配置所示:// ESLint 組態檔 { "plugins": [ // ...此處省略 N 行... "react-hooks" ], "rules": { // ...此處省略 N 行 ... "react-hooks/rules-of-hooks": "error", // 檢查 Hook 的規則 "react-hooks/exhaustive-deps": "warn" // 檢查 effect 的依賴 } }
(2)Hooks 呼叫順序必須保持一致
在 react-hooks 中,呼叫順序是尤為需要注意的一個關鍵點,為什么呢?
下面我們用一段代碼來演示:
import React, { useState, useEffect } from 'react'
function Teach({ couseName }) {
// 函陣列件,純函式,執行完即銷毀
// 所以,無論組件初始化(render)還是組件更新(re-render)
// 都會重新執行一次這個函式,獲取最新的組件
// 這一點和 class 組件不一樣
// render: 初始化 state 的值 '張三'
// re-render: 讀取 state 的值 '張三'
const [studentName, setStudentName] = useState('張三')
// if (couseName) {
// const [studentName, setStudentName] = useState('張三')
// }
// render: 初始化 state 的值 '雙越'
// re-render: 讀取 state 的值 '雙越'
const [teacherName, setTeacherName] = useState('星期一研究室')
// if (couseName) {
// useEffect(() => {
// // 模擬學生簽到
// localStorage.setItem('name', studentName)
// })
// }
// render: 添加 effect 函式
// re-render: 替換 effect 函式(內部的函式也會重新定義)
useEffect(() => {
// 模擬學生簽到
localStorage.setItem('name', studentName)
})
// render: 添加 effect 函式
// re-render: 替換 effect 函式(內部的函式也會重新定義)
useEffect(() => {
// 模擬開始上課
console.log(`${teacherName} 開始上課,學生 ${studentName}`)
})
return <div>
課程:{couseName},
講師:{teacherName},
學生:{studentName}
</div>
}
export default Teach
上面的代碼演示的是一個函陣列件,那對于函陣列件來說,它是一個純函式,一般執行完就會銷毀,因此,無論是組件初始化(render)還是組件更新(re-render),都會重新執行一次這個函式,獲取最新的組件,這一點和 class 組件不一樣,
因此,假設我們把上面的 const [studentName, setStudentName] = useState('張三') 給用 if 陳述句包裹起來,那么它在執行時會受到阻礙,同時,沿著底下的順序走下去,有可能就會把 張三 這個值,賦值給 teacherName ,因此,在 react-hooks 中,我們不能將 react-hooks 用在回圈和判斷里面,不然會很容易導致呼叫順序錯亂,
依據以上的決議,我們來對這個規則做個小結:
- 無論是
render還是re-render,Hooks呼叫順序必須是一致的; - 如果
Hooks出現在回圈、判斷里,則無法保證順序一致; Hooks嚴重依賴于呼叫順序!重要!
三、??React-Hooks組件邏輯復用
1、class組件的邏輯復用
class 組件有三種邏輯復用形式,分別是:
- Mixin
- 高階組件
HOC - Render Prop
下面說下它們三者各自的缺點,
(1)Mixin
- 變數作用域來源不清
- 屬性重名
mixins引入過多會導致順序沖突
(2)高階組件 HOC
- 組件層級嵌套過多,不易渲染,不易除錯
HOC會劫持props,必須嚴格規范,容易出現疏漏
(3)Render Prop
- 學習成本高,不易理解
- 只能傳遞純函式,而默認情況下純函式功能有限
了解了三種 class 組件的缺點之后,現在,我們來看下如何使用 Hooks 做組件邏輯復用,
2、使用 Hooks 做組件邏輯復用
使用 hooks 來使得組件可以進行邏輯復用的本質是:自定義 hooks ,下面我們用一個例子來展示,
(1)例子闡述
首先,我們在專案的 src|customHooks 檔案夾下新建一個檔案,命名為 useMousePosition.js ,具體代碼如下:
import { useState, useEffect } from 'react'
function useMousePosition() {
const [x, setX] = useState(0)
const [y, setY] = useState(0)
useEffect(() => {
function mouseMoveHandler(event) {
// event.clientX 可以拿到滑鼠橫坐標和縱坐標的位置
setX(event.clientX)
setY(event.clientY)
}
// 系結事件
document.body.addEventListener('mousemove', mouseMoveHandler)
// 解綁事件
return () => document.body.removeEventListener('mousemove', mouseMoveHandler)
}, [])
return [x, y]
}
export default useMousePosition
繼續,我們在專案的 src|components 檔案夾,新建一個檔案,命名為 CustomHookUsage.js ,這個組件主要用來使用上面 useMousePosition 中的邏輯,具體代碼如下:
import React from 'react'
import useMousePosition from '../customHooks/useMousePosition'
function App() {
const [x, y] = useMousePosition()
return <div style={{ height: '500px', backgroundColor: '#ccc' }}>
<p>滑鼠位置 {x} {y}</p>
</div>
}
export default App
最后,我們在專案的 App.js 中來使用它,具體代碼如下:
import React, { useState } from 'react';
import CustomHookUsage from './components/CustomHookUsage'
function App() {
return (
<div>
{<CustomHookUsage/>}
</div>
);
}
export default App;
此時,我們來看下瀏覽器的顯示效果:

使用 hooks 做組件邏輯復用,要比 class 組件要簡便和靈活許多,下面我們來梳理下 hooks 做組件邏輯復用的好處,
(2)好處
Hooks 做組件邏輯復用的好處:
- 完全符合
Hooks原有的規則,沒有其他要求,易理解和記憶; - 變數作用域很明確;
- 不會產生組件嵌套,
四、📀React Hooks 注意事項
1、useState初始化值,只有第一次有效
我們先來看一段代碼,具體如下:
import React, { useState } from 'react'
// 子組件
function Child({ userInfo }) {
// render: 初始化 state
// re-render: 只恢復初始化的 state 值,不會再重新設定新的值
// 只能用 setName 修改
const [ name, setName ] = useState(userInfo.name)
return <div>
<p>Child, props name: {userInfo.name}</p>
<p>Child, state name: {name}</p>
</div>
}
function App() {
const [name, setName] = useState('星期一')
const userInfo = { name }
return <div>
<div>
Parent
<button onClick={() => setName('星期二')}>setName</button>
</div>
<Child userInfo={userInfo}/>
</div>
}
export default App
此時,瀏覽器的顯示效果如下:

大家可以看到,當我們點擊的時候,只改變了 useInfo.name 的值,也就是第一個 Child 的值,而此時,可能有的小伙伴會產生疑惑說, useInfo.name 的值是賦值給 name 的,那為什么第二個 Child 不會改變呢,
原因在于,對于 useState 來說,剛開始我們總是會給 state 設定一個初始化的值,而這個初始化的值,即使是資料更新了,它只有第一次的值是有效的,更新的值不會被賦值到其中,所以下面第二個 Child 中 name 值就一直是星期一,
2、useEffect內部不能修改state
先來模擬一段代碼:
import React, { useState, useEffect } from 'react'
function UseEffectChangeState() {
const [count, setCount] = useState(0)
// 模擬 DidMount
useEffect(() => {
console.log('useEffect...', count)
// 定時任務
const timer = setInterval(() => {
console.log('setInterval...', count)
setCount(count + 1)
}, 1000)
// 清除定時任務
return () => clearTimeout(timer)
}, []) // 依賴為 []
// 依賴為 [] 時: re-render 不會重新執行 effect 函式
// 沒有依賴時:re-render 會重新執行 effect 函式
return <div>count: {count}</div>
}
export default UseEffectChangeState
此時我們來看下瀏覽器的效果:

大家可以看到,如果按照我們預期所想的,那么應該會遞增地列印出 1234 ,但是最終沒有這么顯示,timer 里面列印的一直是初始值 0 ,也就是說,這個 state 在 useEffect 里面并不起作用,
因此,這一點在我們日常的開發中需要尤為注意,有可能代碼寫著寫著 bug 就出現在這了,
那既然問題出現了,我們就需要來想一下它的解決方案了,改造如下:
import React, { useState, useRef, useEffect } from 'react'
function UseEffectChangeState() {
const [count, setCount] = useState(0)
// 模擬 DidMount
const countRef = useRef(0)
useEffect(() => {
console.log('useEffect...', count)
// 定時任務
const timer = setInterval(() => {
console.log('setInterval...', countRef.current)
// setCount(count + 1)
setCount(++countRef.current)
}, 1000)
// 清除定時任務
return () => clearTimeout(timer)
}, []) // 依賴為 []
// 依賴為 [] 時: re-render 不會重新執行 effect 函式
// 沒有依賴:re-render 會重新執行 effect 函式
return <div>count: {count}</div>
}
export default UseEffectChangeState
此時瀏覽器的顯示效果如下:

看上面的代碼我們可以發現,通過把 countRef 放到 useEffect 外部,并使用 useRef ,以此來對 state 的值進行修改,達到我們最終的目的,
3、useEffect可能出現死回圈
假設我們要利用 axios 發送網路請求,并且有想要給 url 加一些配置,那么我們有可能這么處理:
import { useState, useEffect } from 'react'
import axios from 'axios'
// 封裝 axios 發送網路請求的自定義 Hook
function useAxios(url, config = {}) {
const [loading, setLoading] = useState(false)
const [data, setData] = useState()
const [error, setError] = useState()
useEffect(() => {
// 利用 axios 發送網路請求
setLoading(true)
axios(url, config) // 發送一個 get 請求
.then(res => setData(res))
.catch(err => setError(err))
.finally(() => setLoading(false))
}, [url, config])
return [loading, data, error]
}
export default useAxios
看著是挺合理,但是呢,當config 使用的是參考資料型別時,也就是 config={} 或者 confgi=[] 時, useEffect 會出現死回圈,無限的發送請求,因此,當我們想要給 axios 傳值時,只能傳基本資料型別的,改造代碼如下:
import { useState, useEffect } from 'react'
import axios from 'axios'
// 封裝 axios 發送網路請求的自定義 Hook
function useAxios(url) {
const [loading, setLoading] = useState(false
const [data, setData] = useState()
const [error, setError] = useState()
useEffect(() => {
// 利用 axios 發送網路請求
setLoading(true)
axios.get(url) // 發送一個 get 請求
.then(res => setData(res))
.catch(err => setError(err))
.finally(() => setLoading(false))
}, [url])
return [loading, data, error]
}
export default useAxios
這樣,才不會出現死回圈地一直翻滾的對介面發送資料請求,
五、🔍結束語
在上面的文章中,我們首先認識了 hooks 的基本概念,接著,我們重點闡述了 state hook 和 effect hook ,同時,我們也簡單地了解了其他幾種 hooks ,最后,談到了 react-hooks 在組件邏輯復用方面的貢獻,以及 react-hooks 需要注意一些事項,
到這里,關于 react-hooks 的介紹就結束啦!不知道大家對 react-hooks 又有了一些新的了解呢?
🐣彩蛋 One More Thing
(:往期推薦
react專欄戳這里 https://juejin.cn/column/7018019097656950815
(:叮
- 關注公眾號星期一研究室,第一時間關注優質文章,更有「offer來了」面試專欄待你解鎖~
- 如果您覺得這篇文章有幫助到您的的話記得留個腳印jio再走喲~~😉
- 以上就是本文的全部內容!我們下期見!👋👋👋
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/395166.html
標籤:其他
上一篇:從VBIDE呼叫的ExcelVBA宏作業正常,但從按鈕呼叫時失敗,直到多次保存檔案
下一篇:【Kotlin 初學者】標準函式
