原文地址:Why Do React Hooks Rely on Call Order? - Dan Abramov
Hooks 重渲染時是依賴于固定順序調(diào)用的
- 請不要在循環(huán)、條件或者嵌套函數(shù)中調(diào)用 Hooks
- 都有在 React 函數(shù)中才去調(diào)用 Hooks
React提供了一個 linter
插件來強制執(zhí)行這些規(guī)則
只在最頂層使用Hook
不要在循環(huán),條件或嵌套函數(shù)中調(diào)用 Hook呻引, 確彼突冢總是在你的 React 函數(shù)的最頂層以及任何 return 之前調(diào)用他們
這條規(guī)則是為了確保Hook在每次渲染中都按照同樣的順序被調(diào)用盖彭,這讓React能夠在多次的useState
和useEffect
調(diào)用之間保持hook狀態(tài)正確
為什么一定要強調(diào)Hook按照順序調(diào)用
通常函數(shù)組件會有多個state
,讓我們通過一個例子來理解useState
可能是如何工作的
// 和useState一樣,myUseState接收一個初始值臀蛛,返回state和setState方法
const myUseState = initialValue => {
let state = initialValue
const setState = newValue => {
state = newValue
// 重新渲染
render()
}
return [state, setState]
}
const render = () => {
ReactDOM.render(<App />, document.getElementById('app'))
}
function App() {
const [n, setN] = myUseState(0)
...
return (
<div>
<p>{n}</p>
<button onClick={() => setN(n + 1)}>
+1
</button>
</div>
);
}
點擊button尿扯,n沒有任何變化
原來每次state
都變成了初始值0
求晶,因為myUseState
會將state
重置
我們需要一個不會被myUseState
重置的變量,那么這個變量只要聲明在myUseState
外面即可
let _state;
const myUseState = initialValue => {
// 如果state是undefined衷笋,則賦給初始值芳杏,否則就賦值為保存在外面的_state
_state = _state === undefined ? initialValue : _state;
const setState = newValue => {
_state = newValue;
render();
};
return [_state, setState];
};
還有問題,如果一個組件有倆state咋整辟宗?由于所有數(shù)據(jù)都放在_state爵赵,產(chǎn)生沖突:
function App() {
const [n, setN] = myUseState(0)
const [m, setM] = myUseState(0)
...
}
解決:
- 把_state做成對象(注意后面會對此種方案進行詳細討論)
- 不可行,沒有key泊脐,
useState(0)
只傳入了一個參數(shù)0
空幻,并不知道是n
還是m
- 不可行,沒有key泊脐,
- 把_state做成數(shù)組
- 可行,
_state = [0, 0]
- 可行,
let _state = [];
// 同樣需要把index聲明在myUseState外面容客,用來記錄調(diào)用順序
let index = 0;
const myUseState = (initialValue) => {
const currentIndex = index;
// 對應調(diào)用順序下的state有值嗎秕铛?
_state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex];
const setState = (newValue) => {
// 設置對應順序的state
_state[currentIndex] = newValue;
render();
};
// 下一個state的位置,使index加一位
index += 1;
return [_state[currentIndex], setState];
};
const render = () => {
// 重新渲染要重置index
// 注意觸發(fā)setter才會re-render
index = 0;
ReactDOM.render(<App />, document.getElementById('app'));
};
顯而易見的耘柱,因為數(shù)組根據(jù)調(diào)用順序存儲值如捅,每一個下標會對應其相應的state棍现,所以useState
調(diào)用順序必須一致调煎!
re-render時會從第一行代碼開始重新執(zhí)行整個組件,所以調(diào)用順序依然是一致的
(需要注意的是己肮,這部分內(nèi)容只是API的一種可能實現(xiàn)方法士袄,真實useState
使用鏈表存儲,為了大家更好地的理解它此處使用數(shù)組替代)
帶著剛剛的思考再次回顧谎僻,舉個??:
function RenderFunctionComponent() {
const [firstName, setFirstName] = useState("Rudi");
const [lastName, setLastName] = useState("Yardley");
return (
<Button onClick={() => setFirstName("Fred")}>Fred</Button>
);
}
-
初始化
創(chuàng)建兩個空數(shù)組“state”與“setters”娄柳,設置指針“cursor”為 0
- 首次渲染
每當useState()
被調(diào)用時,如果它是首次渲染艘绍,它會通過push
將一個setter
方法(綁定了指針“cursor”位置)放進“setters”數(shù)組中赤拒,同時,也會將另一個對應的狀態(tài)放進“state”數(shù)組中去
-
后續(xù)渲染re-render
每次的后續(xù)渲染都會重置指針“cursor”的位置(index=0)诱鞠,并會從每個數(shù)組中讀取對應的值(之前講了數(shù)據(jù)的存儲是獨立于組件之外的)
- 處理事件
每個setter
都會有一個對應的指針位置的引用挎挖,因此當觸發(fā)任何setter
調(diào)用的時候都會觸發(fā)去改變狀態(tài)數(shù)組中的對應的值
看到這里,想必大家對Hook的調(diào)用順序有了更深的印象了航夺,那么讓我們做一些React團隊禁止去做的事情蕉朵,比如在條件語句中使用Hook
let firstRender = true;
function RenderFunctionComponent() {
let initName;
if(firstRender){
[initName] = useState("Rudi");
firstRender = false;
}
const [firstName, setFirstName] = useState(initName);
const [lastName, setLastName] = useState("Yardley");
return (
<Button onClick={() => setFirstName("Fred")}>Fred</Button>
);
}
我們在條件語句中調(diào)用了useState
函數(shù),讓我們看看它對整個系統(tǒng)造成的破壞
到此為止阳掐,我們的變量
firstName
與lastName
依舊包含了正確的數(shù)據(jù)始衅,讓我們繼續(xù)去看一下第二次渲染會發(fā)生什么事情現(xiàn)在
firstName
與lastName
這兩個變量全部被設置為“Rudi”(該位置讀取到的是Rudi)冷蚂,與我們實際的存儲狀態(tài)不符
這個例子的用法顯然是不正確的,但是它讓我們知道了為什么我們必須使用React團隊規(guī)定的規(guī)則去使用Hooks
當然了汛闸,多虧了React提供了linter插件幫我們強制執(zhí)行了這條規(guī)則蝙茶,在代碼編譯過程中會報個錯 React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render.
React團隊yyds!orz!
所以你現(xiàn)在應該清楚為什么你不應該在條件語句或者循環(huán)語句中使用 Hooks 了嗎诸老?
因為我們維護了一個指針“cursor”指向一個數(shù)組尸闸,如果你改變了 render 函數(shù)內(nèi)部的調(diào)用順序,那么這個指針“cursor”將不會匹配到正確的數(shù)據(jù)孕锄,你的調(diào)用也將不會指向正確的數(shù)據(jù)或句柄
希望通過上面的兩個例子吮廉,為大家建立了一個關于 Hooks 的更加清晰的思維模型
另外,Dan也提到了幾個經(jīng)常有人提出的修改Hooks的方案畸肆,并對其缺陷進行了詳細闡述以佐證Hooks的設計是yyds宦芦!接著往下看
缺陷1:無法提取custom hook
有個替代方案是限制一個組件調(diào)用多次 useState(),你可以把 state 放在一個對象里轴脐,這樣還可以兼容 class 不是更好嗎调卑?
function Form() {
const [state, setState] = useState({
name: 'Mary',
surname: 'Poppins',
width: window.innerWidth,
});
// ...
}
Hooks 是允許這種風格寫的,你不必將 state 拆分成一堆 state 變量
但是useState
的關鍵在于你可以從組件中提取出部分有狀態(tài)的邏輯(state + effect)到 custom hooks 中
function Form() {
// 在組件內(nèi)直接定義一些 state 變量
const [name, setName] = useState('Mary');
const [surname, setSurname] = useState('Poppins');
// 我們將部分 state 和 effects 移至 custom hook
const width = useWindowWidth();
// ...
}
function useWindowWidth() {
// 在 custom hook 內(nèi)定義一些 state 變量
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
// ...
});
return width;
}
上面代碼大咱,可以將部分state
(width)提取到自定義組件(useWindowWidth)中恬涧,如果你只允許每個組件調(diào)用一次useState()
,你將失去用custom hook引入state
能力碴巾,這就是custom hooks的關鍵
缺陷2:命名沖突
一個常見的建議是讓組件內(nèi) useState() 接收一個唯一標識 key 參數(shù)(string 等)區(qū)分 state 變量
看起來大致是這樣:
function Form() {
// 我們傳幾種 state key 給 useState()
const [name, setName] = useState('name');
const [surname, setSurname] = useState('surname');
const [width, setWidth] = useState('width');
// ...
這試圖擺脫依賴順序調(diào)用(顯示 key)溯捆,但引入了另外一個問題 —— 命名沖突
而且,你可能無法在同一個組件調(diào)用兩次useState('name')
厦瓢,比方說每當你在custom hook里添加一個新的state
變量時提揍,就有可能破壞使用它的任何組件(直接或者間接),因為可能已經(jīng)有同名的變量位于組件內(nèi)
而通過一開始講到的Hooks提案煮仇,通過依賴順序調(diào)用來解決這個問題:即使兩個 Hooks都用name
變量劳跃,它們也會彼此隔離,每次調(diào)用useState()
都會獲得獨立的 「內(nèi)存單元」
缺陷3:同一個 Hook 無法調(diào)用兩次
給 useState 「加key」的另一種衍生提案是使用像 Symbol 這樣的東西浙垫,這樣就不沖突了對吧刨仑?
const nameKey = Symbol();
const surnameKey = Symbol();
const widthKey = Symbol();
function Form() {
// 我們傳幾種state key給useState()
const [name, setName] = useState(nameKey);
const [surname, setSurname] = useState(surnameKey);
const [width, setWidth] = useState(widthKey);
// ...
這個提案看起來好像有利于提取state
到custom hook當中
function Form() {
// ...
const width = useWindowWidth();
// ...
}
/*********************
* useWindowWidth.js *
********************/
const widthKey = Symbol();
function useWindowWidth() {
const [width, setWidth] = useState(widthKey);
// ...
return width;
}
但是如果多次調(diào)用,例如:
function Form() {
// ...
const name = useFormInput();
const surname = useFormInput();
// ...
return (
<>
<input {...name} />
<input {...surname} />
{/* ... */}
</>
)
}
/*******************
* useFormInput.js *
******************/
const valueKey = Symbol();
function useFormInput() {
const [value, setValue] = useState(valueKey);
return {
value,
onChange(e) {
setValue(e.target.value);
},
};
}
我們調(diào)用 useFormInput() 兩次夹姥,但 useFormInput() 總是用同一個 key 調(diào)用 useState()杉武,就像這樣:
const [name, setName] = useState(valueKey);
const [surname, setSurname] = useState(valueKey);
又又又又發(fā)生了沖突:)
而Hooks提案沒有這種問題,因為每次 調(diào)用useState()
會獲得單獨的state
(狀態(tài)不與其他組件共享)佃声。且依賴于固定順序調(diào)用使我們免于擔心命名沖突
缺陷4:鉆石問題(多層繼承問題)
比如useWindowWidth()
和useNetworkStatus()
這兩個custom hooks可能要用像 useSubscription() 這樣的 custom hook艺智,如下:
function StatusMessage() {
const width = useWindowWidth();
const isOnline = useNetworkStatus();
return (
<>
<p>Window width is {width}</p>
<p>You are {isOnline ? 'online' : 'offline'}</p>
</>
);
}
function useSubscription(subscribe, unsubscribe, getValue) {
const [state, setState] = useState(getValue());
useEffect(() => {
const handleChange = () => setState(getValue());
subscribe(handleChange);
return () => unsubscribe(handleChange);
});
return state;
}
function useWindowWidth() {
const width = useSubscription(
handler => window.addEventListener('resize', handler),
handler => window.removeEventListener('resize', handler),
() => window.innerWidth
);
return width;
}
function useNetworkStatus() {
const isOnline = useSubscription(
handler => {
window.addEventListener('online', handler);
window.addEventListener('offline', handler);
},
handler => {
window.removeEventListener('online', handler);
window.removeEventListener('offline', handler);
},
() => navigator.onLine
);
return isOnline;
}
嵌套+嵌套+嵌套 = ??
/ useWindowWidth() \ / useState() ?? Clash
Status useSubscription()
\ useNetworkStatus() / \ useEffect() ?? Clash
而固定順序調(diào)用的話
/ useState() ? #1. State
/ useWindowWidth() -> useSubscription()
/ \ useEffect() ? #2. Effect
Status
\ / useState() ? #3. State
\ useNetworkStatus() -> useSubscription()
\ useEffect() ? #4. Effect
缺陷5:復制粘貼的主意被打亂
或許我們可以通過引入某種命名空間來挽救給 state 加「key」提議,有幾種不同的方法可以做到這一點
一種方法是使用閉包隔離 state 的 key圾亏,這需要你在 「實例化」 custom hooks時給每個 hook 裹上一層 function:
/*******************
* useFormInput.js *
******************/
function createUseFormInput() {
// 每次實例化都唯一
const valueKey = Symbol();
return function useFormInput() {
const [value, setValue] = useState(valueKey);
return {
value,
onChange(e) {
setValue(e.target.value);
},
};
}
}
可是要知道上面實例代碼只是一個input組件十拣,但是可以看到封拧,它的代碼已經(jīng)很重了,真實的React App由多個類按照層級夭问,一層層構成泽西,復雜度成倍增長,嵌套地獄缰趋。
而Hooks 的設計目標之一就是避免使用高階組件和render props的深層嵌套函數(shù)
而且不得不操作兩次才能使組件用上custom hook
// 我們不得不在使用任何custom hook時進實例化
const useNameFormInput = createUseFormInput();
const useSurnameFormInput = createUseFormInput();
function Form() {
// ...
// 還有一次是最終的調(diào)用
const name = useNameFormInput();
const surname = useNameFormInput();
// ...
}
這意味著即使一個很小的改動捧杉,你也得在頂層聲明和render函數(shù)間來回跳轉
你還需要非常精確的命名,總是需要考慮「兩層」命名 —— 像 createUseFormInput 這樣的工廠函數(shù)和 useNameFormInput秘血、useSurnameFormInput這樣的實例 Hooks
參考資料:
Why Do React Hooks Rely on Call Order?
(譯)React hooks:它不是一種魔法味抖,只是一個數(shù)組——使用圖表揭秘提案規(guī)則
Rules of Hooks
RFC: React Hooks