React Hooks使用一年后,個人使用心得!

原文:juejin.cn/post/6912309761066729485#heading-9
關(guān)注公眾號 前端人,回復(fù)“加群”
添加無廣告優(yōu)質(zhì)學(xué)習(xí)群
前言
分享一下用了將近一年hooks使用心得
動機(jī)(官方)
組件之間很難重用有狀態(tài)邏輯 復(fù)雜的組件變得難以理解 類 class 混淆了人和機(jī)器 更符合 FP 的理解, React 組件本身的定位就是函數(shù),一個吃進(jìn)數(shù)據(jù)、吐出 UI 的函數(shù) 
常用 hook
useState
const [state, setState] = useState(initialState)
useState 有一個參數(shù),該參數(shù)可以為任意數(shù)據(jù)類型,一般用作默認(rèn)值 useState 返回值為一個數(shù)組,數(shù)組的第一個參數(shù)為我們需要使用的 state,第二個參數(shù)為一個 setFn。 完整例子
function Love() {
const [like, setLike] = useState(false)
const likeFn = () => (newLike) => setLike(newLike)
return (
<>
你喜歡我嗎: {like ? 'yes' : 'no'}
<button onClick={likeFn(true)}>喜歡</button>
<button onClick={likeFn(false)}>不喜歡</button>
</>
)
}
關(guān)于使用規(guī)則:
只在 React 函數(shù)中調(diào)用 Hook; 不要在循環(huán)、條件或嵌套函數(shù)中調(diào)用 Hook。 讓我們來看看規(guī)則 2 為什么會有這個現(xiàn)象, 先看看 hook 的組成
function mountWorkInProgressHook() {
// 注意,單個 hook 是以對象的形式存在的
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
if (workInProgressHook === null) {
firstWorkInProgressHook = workInProgressHook = hook;
/* 等價
let workInProgressHook = hooks
firstWorkInProgressHook = workInProgressHook
*/
} else {
workInProgressHook = workInProgressHook.next = hook;
}
// 返回當(dāng)前的 hook
return workInProgressHook;
}
每個 hook 都會有一個 next 指針,hook 對象之間以單向鏈表的形式相互串聯(lián), 同時也能發(fā)現(xiàn) useState 底層依然是 useReducer 再看看更新階段發(fā)生了什么
// ReactFiberHooks.js
const HooksDispatcherOnUpdate: Dispatcher = {
// ...
useState: updateState,
}
function updateState(initialState) {
return updateReducer(basicStateReducer, initialState);
}
function updateReducer(reducer, initialArg, init) {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
if (numberOfReRenders > 0) {
const dispatch = queue.dispatch;
if (renderPhaseUpdates !== null) {
// 獲取Hook對象上的 queue,內(nèi)部存有本次更新的一系列數(shù)據(jù)
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate !== undefined) {
renderPhaseUpdates.delete(queue);
let newState = hook.memoizedState;
let update = firstRenderPhaseUpdate;
// 獲取更新后的state
do {
// useState 第一個參數(shù)會被轉(zhuǎn)成 useReducer
const action = update.action;
newState = reducer(newState, action);
//按照當(dāng)前鏈表位置更新數(shù)據(jù)
update = update.next;
} while (update !== null);
hook.memoizedState = newState;
// 返回新的 state 以及 dispatch
return [newState, dispatch];
}
}
}
// ...
}
結(jié)合實際讓我們看下面一組 hooks
let isMounted = false
if(!isMounted) {
[name, setName] = useState("張三");
[age] = useState("25");
isMounted = true
}
[sex, setSex] = useState("男");
return (
<button
onClick={() => {
setName(李四");
}}
>
修改姓名
</button>
);
首次渲染時 hook 順序為
name => age => sex
二次渲染的時根據(jù)上面的例子,調(diào)用的 hook 的只有一個
setSex
所以總結(jié)一下初始化階段構(gòu)建鏈表,更新階段按照順序去遍歷之前構(gòu)建好的鏈表,取出對應(yīng)的數(shù)據(jù)信息進(jìn)行渲染當(dāng)兩次順序不一樣的時候就會造成渲染上的差異。
為了避免出現(xiàn)上面這種情況我們可以安裝 eslint-plugin-react-hooks
// 你的 ESLint 配置
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // 檢查 Hook 的規(guī)則
"react-hooks/exhaustive-deps": "warn" // 檢查 effect 的依賴
}
}
useEffect
useEffect(effect, array)
effect 每次完成渲染之后觸發(fā), 配合 array 去模擬類的生命周期 如果不傳,則每次 componentDidUpdate 時都會先觸發(fā) returnFunction(如果存在),再觸發(fā) effect [] 模擬 componentDidMount [id] 僅在 id 的值發(fā)生變化以后觸發(fā) 清除 effect
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
};
});
useLayoutEffect
跟 useEffect 使用差不多,通過同步執(zhí)行狀態(tài)更新可解決一些特性場景下的頁面閃爍問題 useLayoutEffect 會阻塞渲染,請謹(jǐn)慎使用
import React, { useLayoutEffect, useEffect, useState } from 'react';
import './App.css'
function App() {
const [value, setValue] = useState(0);
useEffect(() => {
if (value === 0) {
setValue(10 + Math.random() * 200);
}
}, [value]);
const test = () => {
setValue(0)
}
const color = !value ? 'red' : 'yellow'
return (
<React.Fragment>
<p style={{ background: color}}>value: {value}</p>
<button onClick={test}>點我</button>
</React.Fragment>
);
}
export default App;
useContext
const context = useContext(Context)
useContext 從名字上就可以看出,它是以 Hook 的方式使用 React Context, 先簡單介紹 Context 的概念和使用方式
import React, { useContext, useState, useEffect } from "react";
const ThemeContext = React.createContext(null);
const Button = () => {
const { color, setColor } = React.useContext(ThemeContext);
useEffect(() => {
console.info("Context changed:", color);
}, [color]);
const handleClick = () => {
console.info("handleClick");
setColor(color === "blue" ? "red" : "blue");
};
return (
<button
type="button"
onClick={handleClick}
style={{ backgroundColor: color, color: "white" }}
>
toggle color in Child
</button>
);
};
// app.js
const App = () => {
const [color, setColor] = useState("blue");
return (
<ThemeContext.Provider value={{ color, setColor }}>
<h3>
Color in Parent: <span style={{ color: color }}>{color}</span>
</h3>
<Button />
</ThemeContext.Provider>
);
};
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init)
語法糖跟 redux 差不多,放個基礎(chǔ) ??
function init(initialCount) {
return {count: initialCount};
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
useRef
const refContainer = useRef(initialValue);
useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化為傳入的參數(shù)(initialValue)。返回的 ref 對象在組件的整個生命周期內(nèi)保持不變
解決引用問題--useRef 會在每次渲染時返回同一個 ref 對象
解決一些 this 指向問題
對比 createRef -- 在初始化階段兩個是沒區(qū)別的,但是在更新階段兩者是有區(qū)別的。
我們知道,在一個局部函數(shù)中,函數(shù)每一次 update,都會在把函數(shù)的變量重新生成一次。
所以我們每更新一次組件, 就重新創(chuàng)建一次 ref, 這個時候繼續(xù)使用 createRef 顯然不合適,所以官方推出 useRef。
useRef 創(chuàng)建的 ref 仿佛就像在函數(shù)外部定義的一個全局變量,不會隨著組件的更新而重新創(chuàng)建。但組件銷毀,它也會消失,不用手動進(jìn)行銷毀
總結(jié)下就是 ceateRef 每次渲染都會返回一個新的引用,而 useRef 每次都會返回相同的引用
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
一個常用來做性能優(yōu)化的 hook,看個 ??
const MemoDemo = ({ count, color }) => {
useEffect(() => {
console.log('count effect')
}, [count])
const newCount = useMemo(() => {
console.log('count 觸發(fā)了')
return Math.round(count)
}, [count])
const newColor = useMemo(() => {
console.log('color 觸發(fā)了')
return color
}, [color])
return <div>
<p>{count}</p>
<p>{newCount}</p>
{newColor}</div>
}
我們這個時候?qū)魅氲?count 值改變 的,log 執(zhí)行循序
count 觸發(fā)了
count effect
可以看出有點類似 effect, 監(jiān)聽 a、b 的值根據(jù)值是否變化來決定是否更新 UI memo 是在 DOM 更新前觸發(fā)的,就像官方所說的,類比生命周期就是 shouldComponentUpdate 對比 React.Memo 默認(rèn)是是基于 props 的淺對比,也可以開啟第二個參數(shù)進(jìn)行深對比。在最外層包裝了整個組件,并且需要手動寫一個方法比較那些具體的 props 不相同才進(jìn)行 re-render。使用 useMemo 可以精細(xì)化控制,進(jìn)行局部 Pure
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
useCallback 的用法和上面 useMemo 差不多,是專門用來緩存函數(shù)的 hooks
下面的情況可以保證組件重新渲染得到的方法都是同一個對象,避免在傳給onClick的時候每次都傳不同的函數(shù)引用
import React, { useState, useCallback } from 'react'
function MemoCount() {
const [value, setValue] = useState(0)
memoSetCount = useCallback(()=>{
setValue(value + 1)
},[])
return (
<div>
<button
onClick={memoSetCount}
>
Update Count
</button>
<div>{value}</div>
</div>
)
}
export default MemoCount
自定義 hooks
自定義 Hook 是一個函數(shù),其名稱以 “use” 開頭,函數(shù)內(nèi)部可以調(diào)用其他的 Hook 一般我將 hooks 分為這幾類
util
顧名思義工具類,比如 useDebounce、useInterval、useWindowSize 等等。例如下面 useWindowSize
import { useEffect, useState } from 'react';
export default function useWindowSize(el) {
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined,
});
useEffect(
() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
window.addEventListener('resize', handleResize);
handleResize();
return () => window.removeEventListener('resize', handleResize);
},
[el],
);
return windowSize;
}
API
像之前的我們有一個公用的城市列表接口,在用 redux 的時候可以放在全局公用,不用的話我們就可能需要復(fù)制粘貼了。有了 hooks 以后我們只需要 use 一下就可以在其他地方復(fù)用了
import { useState, useEffect } from 'react';
import { getCityList } from '@/services/static';
const useCityList = (params) => {
const [cityList, setList] = useState([]);
const [loading, setLoading] = useState(true)
const getList = async () => {
const { success, data } = await getCityList(params);
if (success) setList(data);
setLoading(false)
};
useEffect(
() => {getList();},
[],
);
return {
cityList,
loading
};
};
export default useCityList;
// bjs
function App() {
// ...
const { cityList, loading } = useCityList()
// ...
}
logic
邏輯類,比如我們有一個點擊用戶頭像關(guān)注用戶或者取消關(guān)注的邏輯,可能在評論列表、用戶列表都會用到,我們可以這樣做
import { useState, useEffect } from 'react';
import { followUser } from '@/services/user';
const useFollow = ({ accountId, isFollowing }) => {
const [isFollow, setFollow] = useState(false);
const [operationLoading, setLoading] = useState(false)
const toggleSection = async () => {
setLoading(true)
const { success } = await followUser({ accountId });
if (success) {
setFollow(!isFollow);
}
setLoading(false)
};
useEffect(
() => {
setFollow(isFollowing);
},
[isFollowing],
);
return {
isFollow,
toggleSection,
operationLoading
};
};
export default useFollow;
只需暴露三個參數(shù)就能滿足大部分場景
UI
還有一些和 UI 一起綁定的 hook, 但是這里有點爭議要不要和 ui 一起混用。就我個人而言一起用確實幫我解決了部分復(fù)用問題,我還是分享出來。
import React, { useState } from 'react';
import { Modal } from 'antd';
// TODO 為了兼容一個頁面有多個 modal, 目前想法通過唯一 key 區(qū)分,后續(xù)優(yōu)化
export default function useModal(key = 'open') {
const [opens, setOpen] = useState({
[key]: false,
});
const onCancel = () => {
setOpen({ [key]: false });
};
const showModal = (type = key) => {
setOpen({ [type]: true });
};
const MyModal = (props) => {
return <Modal key={key} visible={opens[key]} onCancel={onCancel} {...props} />;
};
return {
showModal,
MyModal,
};
}
// 使用
function App() {
const { showModal, MyModal } = useModal();
return <>
<button onClick={showModal}>展開</button>
<MyModal onOk={console.log} />
</>
}
總結(jié)
越來越多的 react 配套的三方庫都上了 hooks 版,像 react-router、redux 都出了 hooks。
同時也出現(xiàn)了一些好用的 hooks 庫,比如 ahooks 這種。自從用了 hooks 以后我就兩個字,真香
回復(fù) 資料包領(lǐng)取我整理的進(jìn)階資料包回復(fù) 加群,加入前端進(jìn)階群console.log("文章點贊===文章點在看===你我都快樂")Bug離我更遠(yuǎn)了,下班離我更近了

