在 React Conf 2018 上,React團隊提出了Hooks提案。html
若是你想知道什麼是 Hooks,及它能解決什麼問題,查看咱們的講座(介紹),理解React Hooks(常見的誤解)。react
最初你可能會不喜歡 Hooks:git
它們就像一段音樂,只有通過幾回用心聆聽纔會慢慢愛上:程序員
當你閱讀文檔時,不要錯過關於最重要的部分——創造屬於你本身的Hooks!太多的人糾結於反對咱們的觀點(class學習成本高等)以致於錯過了Hooks更重要的一面,Hooks像 functional mixins
,可讓你創造和搭建屬於本身的Hook。github
Hooks受啓發於一些現有技術,但在 Sebastian 和團隊分享他的想法以後,我才知道這些。不幸的是,這些API和如今在用的之間的關聯很容易被忽略,經過這篇文章,我但願能夠幫助更多的人理解 Hooks提案中爭議較大的點。spring
接下來的部分須要你知道 Hook API 的useState
和如何寫自定義Hook。若是你還不懂,能夠看看早先的連接。數組
(免責說明:文章的觀點僅表明我的想法,與React團隊無關。話題大且複雜,其中可能有錯誤的觀點。)瀏覽器
一開始當你學習時你可能會震驚,Hooks 重渲染時是依賴於固定順序調用的,這裏有說明。安全
這個決定顯然是有爭議的,這也是爲何會有人反對咱們的提案。咱們會在恰當的時機發布這個提案,當咱們以爲文檔和講座能夠足夠好的描繪它時。bash
若是你在關注 Hooks API 的某些點,我建議你閱讀下 Sebastian對 1000+ 評論RFC的所有回覆,足夠透澈但內容很是多,我可能會將評論中的每一段都變成本身的博客文章。(事實上,我已經作過一次!)
我今天要關注一個具體部分。你可能還記得,每一個 Hook 能夠在組件裏被屢次使用,例如,咱們能夠用 useState
聲明多個state:
function Form() {
const [name, setName] = useState('Mary'); // State variable 1
const [surname, setSurname] = useState('Poppins'); // State variable 2
const [width, setWidth] = useState(window.innerWidth); // State variable 3
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
});
function handleNameChange(e) {
setName(e.target.value);
}
function handleSurnameChange(e) {
setSurname(e.target.value);
}
return (
<>
<input value={name} onChange={handleNameChange} />
<input value={surname} onChange={handleSurnameChange} />
<p>Hello, {name} {surname}</p>
<p>Window width: {width}</p>
</>
);
}
複製代碼
注意咱們用數組解構語法來命名 useState()
返回的 state 變量,但這些變量不會鏈接到React組件上。相反,這個例子中,React將 name
視爲「第一個state變量」,surname
視爲「第二個state變量」,以此類推。它們在從新渲染時用 順序調用 來保證被正確識別。這篇文章詳細的解釋了緣由。
表面上看,依賴於順序調用只是 感受有問題,直覺是一個有用的信號,但它有時會誤導咱們 —— 特別是當咱們尚未徹底消化困惑的問題。 這篇文章,我會提到幾個常常有人提出修改Hooks的方案,及它們存在的問題。
這篇文章不會詳盡無遺,如你所見,咱們已經看過十幾種至數百種不一樣的替代方案,咱們一直在考慮替換組件API。
諸如此類的博客很棘手,由於即便你涉及了一百種替代方案,也有人強行提出一個來:「哈哈,你沒有想到 這個 」!
在實踐中,不一樣替代方案提到的問題會有不少重複,我不會列舉 全部 建議的API(這須要花費數月時間),而是經過幾個具體示例展現最多見的問題,更多的問題就考驗讀者觸類旁通的能力了。🧐
這不是說 Hooks 就是完美的,可是一旦你瞭解其餘解決方案的缺陷,你可能會發現 Hooks 的設計是有道理的。
出乎意料的是,大多數替代方案徹底沒有提到 custom hooks。多是由於咱們在「motivation」文檔中沒有足夠強調 custom hooks,不過在弄懂 Hooks 基本原理以前,這是很難作到的。就像雞和蛋問題,但很大程度上 custom hooks 是提案的重點。
例如:有個替代方案是限制一個組件調用屢次 useState()
,你能夠把 state 放在一個對象裏,這樣還能夠兼容class不是更好嗎?
function Form() {
const [state, setState] = useState({
name: 'Mary',
surname: 'Poppins',
width: window.innerWidth,
});
// ...
}
複製代碼
要清楚,Hooks 是容許這種風格寫的,你沒必要將state拆分紅一堆state變量(請參閱參見問題解答中的建議)。
但支持屢次調用 useState()
的關鍵在於,你能夠從組件中提取出部分有狀態邏輯(state + effect)到 custom hooks 中,同時能夠單獨使用本地 state 和 effects:
function Form() {
// 在組件內直接定義一些state變量
const [name, setName] = useState('Mary');
const [surname, setSurname] = useState('Poppins');
// 咱們將部分state和effects移至custom hook
const width = useWindowWidth();
// ...
}
function useWindowWidth() {
// 在custom hook內定義一些state變量
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
// ...
});
return width;
}
複製代碼
若是你只容許每一個組件調用一次 useState()
,你將失去用 custom hook 引入 state 能力,這就是 custom hooks 的關鍵。
一個常見的建議是讓組件內 useState()
接收一個惟一標識key參數(string等)區分 state 變量。
和這主意有些出入,但看起來大體像這樣:
// ⚠️ This is NOT the React Hooks API
function Form() {
// 咱們傳幾種state key給useState()
const [name, setName] = useState('name');
const [surname, setSurname] = useState('surname');
const [width, setWidth] = useState('width');
// ...
複製代碼
這試圖擺脫依賴順序調用(顯示key),但引入了另一個問題 —— 命名衝突。
固然除了錯誤以外,你可能沒法在同一個組件調用兩次 useState('name')
,這種偶然發生的能夠歸結於其餘任意bug,可是,當你使用一個 custom hook 時,你總會遇到想添加或移除state變量和effects的狀況。
這個提議中,每當你在 custom hook 裏添加一個新的 state 變量時,就有可能破壞使用它的任何組件(直接或者間接),由於 可能已經有同名的變量 位於組件內。
這是一個沒有針對變化而優化的API,當前代碼可能看起來老是「優雅的」,但應對需求變化時十分脆弱,咱們應該從錯誤中吸收教訓。
實際中 Hooks 提案經過依賴順序調用來解決這個問題:即便兩個 Hooks 都用 name
state變量,它們也會彼此隔離,每次調用 useState()
都會得到獨立的 「內存單元」。
咱們還有其餘一些方法能夠解決這個缺陷,但這些方法也有自身的缺陷。讓咱們加深探索這個問題。
給 useState
「加key」的另外一種衍生提案是使用像Symbol這樣的東西,這樣就不衝突了對吧?
// ⚠️ This is NOT the React Hooks API
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);
// ...
複製代碼
這個提案看上去對提取出來的 useWindowWidth
Hook 有效:
// ⚠️ This is NOT the React Hooks API
function Form() {
// ...
const width = useWindowWidth();
// ...
}
/*********************
* useWindowWidth.js *
********************/
const widthKey = Symbol();
function useWindowWidth() {
const [width, setWidth] = useState(widthKey);
// ...
return width;
}
複製代碼
但若是嘗試提取出來的 input handling,它會失敗:
// ⚠️ This is NOT the React Hooks API
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);
},
};
}
複製代碼
(我認可 useFormInput()
Hook 不是特別好用,但你能夠想象下它處理諸如驗證和 dirty state 標誌之類,如Formik。)
你能發現這個bug嗎?
咱們調用 useFormInput()
兩次,但 useFormInput()
老是用同一個 key 調用 useState()
,就像這樣:
const [name, setName] = useState(valueKey);
const [surname, setSurname] = useState(valueKey);
複製代碼
咱們再次發生了衝突。
實際中 Hooks 提案沒有這種問題,由於每次 調用 useState()
會得到單獨的state。依賴於固定順序調用使咱們免於擔憂命名衝突。
從技術上來講這個和上一個缺陷相同,但它的臭名值得說說,甚至維基百科都有介紹。(有些時候還被稱爲「致命的死亡鑽石」 —— cool!)
咱們本身的 mixin 系統就受到了傷害。
好比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;
}
複製代碼
這是一個真實可運行的示例。 custom hook 做者準備或中止使用另外一個 custom hook 應該是要安全的,而沒必要擔憂它是否已在鏈中某處「被用過了」。
(做爲反例,遺留的 React createClass()
的 mixins 不容許你這樣作,有時你會有兩個 mixins,它們都是你想要的,但因爲擴展了同一個 「base」 mixin,所以互不兼容。)
這是咱們的 「鑽石」:💎
/ useWindowWidth() \ / useState() 🔴 Clash
Status useSubscription()
\ useNetworkStatus() / \ useEffect() 🔴 Clash
複製代碼
依賴於固定的順序調用很天然的解決了它:
/ useState() ✅ #1. State
/ useWindowWidth() -> useSubscription()
/ \ useEffect() ✅ #2. Effect
Status
\ / useState() ✅ #3. State
\ useNetworkStatus() -> useSubscription()
\ useEffect() ✅ #4. Effect
複製代碼
函數調用不會有「鑽石」問題,由於它們會造成樹狀結構。🎄
或許咱們能夠經過引入某種命名空間來挽救給 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);
},
};
}
}
複製代碼
這種做法很是繁瑣,Hooks 的設計目標之一就是避免使用高階組件和render props的深層嵌套函數。在這裏,咱們不得不在使用 任何 custom hook 時進行「實例化」 —— 並且在組件主體中只能單次使用生產的函數,這比直接調用 Hooks 麻煩好多。
另外,你不得不操做兩次才能使組件用上 custom hook。一次在最頂層(或在編寫 custom hook 時的函數裏頭),還有一次是最終的調用。這意味着即便一個很小的改動,你也得在頂層聲明和render函數間來回跳轉:
// ⚠️ This is NOT the React Hooks API
const useNameFormInput = createUseFormInput();
const useSurnameFormInput = createUseFormInput();
function Form() {
// ...
const name = useNameFormInput();
const surname = useNameFormInput();
// ...
}
複製代碼
你還須要很是精確的命名,老是須要考慮「兩層」命名 —— 像 createUseFormInput
這樣的工廠函數和 useNameFormInput
、useSurnameFormInput
這樣的實例 Hooks。
若是你同時調用兩次相同的 custom hook 「實例」,你會發生state衝突。事實上,上面的代碼就是這種錯誤 —— 發現了嗎? 它應該爲:
const name = useNameFormInput();
const surname = useSurnameFormInput(); // Not useNameFormInput!
複製代碼
這些問題並不是不可克服,但我認爲它們會比遵照 Hooks規則 的阻力大些。
重要的是,它們打破了複製粘貼的小算盤。在沒有封裝外層的狀況下這種 custom hook 仍然可使用,但它們只能夠被調用一次(這在使用時會產生問題)。不幸的是,當一個API看起來能夠正常運行,一旦你意識到在鏈的某個地方出現了衝突時,就不得不把全部定義好的東西包起來了。
還有另一種使用密鑰state來避免衝突的方法,若是你知道,可能會真的很生氣,由於我不看好它,抱歉。
這個主意就是每次寫 custom hook 時 組合 一個密鑰,就像這樣:
// ⚠️ This is NOT the React Hooks API
function Form() {
// ...
const name = useFormInput('name');
const surname = useFormInput('surname');
// ...
return (
<>
<input {...name} />
<input {...surname} />
{/* ... */}
</>
)
}
function useFormInput(formInputKey) {
const [value, setValue] = useState('useFormInput(' + formInputKey + ').value');
return {
value,
onChange(e) {
setValue(e.target.value);
},
};
}
複製代碼
和其餘替代提議比,我最不喜歡這個,我以爲它沒有什麼價值。
一個 Hook 通過屢次調用或者與其餘 Hook 衝突以後,代碼可能 意外產出 非惟一或合成無效密鑰進行傳遞。更糟糕的是,若是它是在某些條件下發生的(咱們會試圖 「修復」 它對吧?),可能在一段時間後才發生衝突。
咱們想提醒你們,記住全部經過密鑰來標記的 custom hooks 都很脆弱,它們不只增長了運行時的工做量(別忘了它們要轉成 密鑰 ),並且會漸漸增大 bundle 大小。但若是說咱們非要提醒一個問題,是哪一個問題呢?
若是非要在條件判斷裏聲明 state 和 effects,這種方法多是有做用的,但按過去經驗來講,我發現它使人困惑。事實上,我不記得有人會在條件判斷裏定義this.state
或者componentMount
的。
這段代碼到底意味着什麼?
// ⚠️ This is NOT the React Hooks API
function Counter(props) {
if (props.isActive) {
const [count, setCount] = useState('count');
return (
<p onClick={() => setCount(count + 1)}>
{count}
</p>;
);
}
return null;
}
複製代碼
當 props.isActive
爲 false
時 count
是否被保留?或者因爲 useState('count')
沒有被調用而重置 count
?
若是條件爲保留 state,effect 又會發生什麼?
// ⚠️ This is NOT the React Hooks API
function Counter(props) {
if (props.isActive) {
const [count, setCount] = useState('count');
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(id);
}, []);
return (
<p onClick={() => setCount(count + 1)}>
{count}
</p>;
);
}
return null;
}
複製代碼
無疑它不會在 props.isActive
第一次是 true
以前 運行,但一旦變成 true
,它會中止運行嗎?當 props.isActive
轉變爲 false
時 interval 會重置嗎?若是是這樣,effect 與 state(咱們說不重置時) 的行爲不一樣使人困惑。若是 effect 繼續運行,那麼 effect 外層的 if
再也不控制 effect,這也使人感到困惑,咱們不是說咱們想要基於條件控制的 effects 嗎?
若是在渲染期間咱們沒有「使用」 state 但 它卻被重置,若是有多個 if
分支包含 useState('count')
但只有其中一個會在給定時間裏運行,會發生什麼?這是有效的代碼嗎?若是咱們的核心思想是 「以密鑰分佈」,那爲何要 「丟棄」 它?開發人員是否但願在這以後從組件中提早 return
以重置全部state呢? 其實若是咱們真的須要重置state,咱們能夠經過提取組件使其明確:
function Counter(props) {
if (props.isActive) {
// Clearly has its own state
return <TickingCounter />; } return null; } 複製代碼
不管如何這可能成爲是解決這些困惑問題的「最佳實踐」,因此無論你選擇哪一種方式去解釋,我以爲條件裏 聲明 state 和 effect的語義怎樣都很怪異,你可能會不知不覺的感覺到。
若是還要提醒的是 —— 正確地組合密鑰的需求會變成「負擔」,它並無給咱們帶來任何想要的。可是,放棄這個需求(並回到最初的提案)確實給咱們帶來了一些東西,它使組件代碼可以安全地複製粘貼到一個 custom hook 中,且不須要命名空間,減少bundle大小及輕微的效率提高(不須要Map查找)。
慢慢理解。
Hooks 有個最好的功能就是能夠在它們之間傳值。
如下是一個選擇信息收件人的模擬示例,它顯示了當前選擇的好友是否在線:
const friendList = [
{ id: 1, name: 'Phoebe' },
{ id: 2, name: 'Rachel' },
{ id: 3, name: 'Ross' },
];
function ChatRecipientPicker() {
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);
return (
<>
<Circle color={isRecipientOnline ? 'green' : 'red'} />
<select
value={recipientID}
onChange={e => setRecipientID(Number(e.target.value))}
>
{friendList.map(friend => (
<option key={friend.id} value={friend.id}>
{friend.name}
</option>
))}
</select>
</>
);
}
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
const handleStatusChange = (status) => setIsOnline(status.isOnline);
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
複製代碼
當改變收件人時,useFriendStatus
Hook 就會退訂上一個好友的狀態,訂閱接下來的這個。
這是可行的,由於咱們能夠將 useState()
Hook 返回的值傳給 useFriendStatus()
Hook:
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);
複製代碼
Hooks之間傳值很是有用。例如:React Spring能夠建立一個尾隨動畫,其中多個值彼此「跟隨」:
const [{ pos1 }, set] = useSpring({ pos1: [0, 0], config: fast });
const [{ pos2 }] = useSpring({ pos2: pos1, config: slow });
const [{ pos3 }] = useSpring({ pos3: pos2, config: slow });
複製代碼
(這是 demo。)
在Hook初始化時添加默認參數或者將Hook寫在裝飾器表單中的提議,很難實現這種狀況的邏輯。
若是不在函數體內調用 Hooks,就不能夠輕鬆地在它們之間傳值了。你能夠改變這些值結構,讓它們不須要在多層組件之間傳遞,也能夠用 useMemo
來存儲計算結果。但你也沒法在 effects 中引用這些值,由於它們沒法在閉包中被獲取到。有些方法能夠經過某些約定來解決這些問題,但它們須要你在內心「覈算」輸入和輸出,這違背了 React 直接了當的風格。
在 Hooks 之間傳值是咱們提案的核心,Render props 模式在沒有 Hooks 時是你最早能想到的,但像 Component Component 這樣的庫,是沒法適用於你遇到的全部場景的,它因爲「錯誤的層次結構」存在大量的語法干擾。Hooks 用扁平化層次結構來實現傳值 —— 且函數調用是最簡單的傳值方式。
有許多提議處於這種範疇裏。他們儘量的想讓React擺脫對 Hooks 的依賴感,大多數方法是這麼作的:讓 this
擁有內置 Hooks,使它們變成額外的參數在React中無處不在,等等等。
我以爲 Sebastian的回答 比個人描述,更能說服這種方式,我建議你去了解下「注入模型」。
我只想說這和程序員傾向於用 try
/catch
捕獲方法中的錯誤代碼是同樣的道理,一樣對比 AMD由咱們本身傳入 require
的「顯示」聲明,咱們更喜歡 import
(或者 CommonJS require
) 的 ES模塊。
// 有誰想念 AMD?
define(['require', 'dependency1', 'dependency2'], function (require) {
var dependency1 = require('dependency1'),
var dependency2 = require('dependency2');
return function () {};
});
複製代碼
是的,AMD 可能更「誠實」 的陳述了在瀏覽器環境中模塊不是同步加載的,但當你知道了這個後,寫 define
三明治 就變成作無用功了。
try
/catch
、require
和 React Context API都是咱們更喜歡「環境」式體驗,多於直接聲明使用的真實例子(即便一般咱們更喜歡直爽風格),我以爲 Hooks 也屬於這種。
這相似於當咱們聲明組件時,就像從 React
抓個 Component
過來。若是咱們用工廠的方式導出每一個組件,可能咱們的代碼會更解耦:
function createModal(React) {
return class Modal extends React.Component {
// ...
};
}
複製代碼
但在實際中,這最後會變得畫蛇添足而使人厭煩。當咱們真的想以某種方式抓React時,咱們應該在模塊系統層面上實現。
這一樣適用於 Hooks。儘管如此,正如 Sebastian的回答 中提到的,在 技術上 能夠作到從 react
中「直接」導入不一樣實現的 Hooks。(我之前的文章有提到過。)
另外一種強行復雜化想法是把Hooks monadic(單子化) 或者添加像 React.createHook()
這樣的class理念。除了runtime以外,其餘任何添加嵌套的方案都會失去普通函數的優勢:便於調試。
在調試過程當中,普通函數中不會夾雜任何類庫代碼,且能夠清晰的知道組件內部值的流向,間接性很難作到這點。像啓發於高階組件(「裝飾器」 Hooks)或者 render props(adopt
提案 或 generators的yield
等)相似的方案,都存在這樣的問題。間接性也使靜態類型變得複雜。
如我以前提到的,這篇文章不會詳盡無遺,在其餘提案中有許多有趣的問題,其中有一些更加晦澀(例如於併發和高級編譯相關),這多是在將來另外一篇文章的主題。
Hooks並不是完美無瑕,但這是咱們能夠找到解決這些問題的最佳權衡。還有一些咱們仍然須要修復的東西,這些問題在 Hooks 中比在 class 中更加彆扭,這也會寫在別的文章裏頭。
不管我是否覆蓋掉你喜歡的替換方案,我但願這篇文章有助於闡述咱們的思考過程及咱們在選擇API時考慮的標準。如你所見,不少(例如確保複製粘貼、移動代碼、按但願的方式進行增刪依賴包)不得不針對變化而優化。我但願React開發者們會看好咱們所作的這些決定。
翻譯原文Why Do React Hooks Rely on Call Order?(2018-12-13)