原文連接javascript
在2018年10月的React Conf上引入了React Hooks,做爲在React函數組件中使用state和反作用的一種方式。儘管功能組件之前被稱爲函數無狀態組件(FSC),但它們最終可以與React Hooks一塊兒使用狀態。所以,許多人如今將它們稱爲函數組件。java
在本演練中,我想解釋鉤子函數背後的動機,React會發生什麼變化,本教程只是對React Hooks的介紹。在本教程的最後,您將找到更多的教程來深刻了解React Hooks。 ##爲何要使用React Hooks? React Hooks是由React團隊發明的,旨在在函數組件中引入狀態管理和反作用。這是他們的一種方法,它無需使用生命週期方法將React函數組件重構爲React類組件,而是以使用反作用或state的方式,使得React函數組件變得更加輕鬆,React Hooks讓咱們可以僅使用函數組件來編寫React應用程序。react
沒必要要的組件重構:之前,僅React類組件用於本地狀態管理和生命週期方法。後者對於在React類組件中引入諸如偵聽器或數據獲取之類的反作用相當重要。npm
import React from 'react';
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button
onClick={() =>
this.setState({ count: this.state.count + 1 })
}
>
Click me
</button>
</div>
);
}
}
export default Counter;
複製代碼
僅當您不須要狀態或生命週期方法時,纔可使用React無狀態組件。並且因爲React函數組件更輕巧(和美觀),人們已經使用了不少函數組件。這樣作的缺點是每次須要狀態或生命週期方法時都會將組件從React函數組件重構爲React類組件(反之亦然)。編程
import React, { useState } from 'react';
// how to use the state hook in a React function component
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Counter;
複製代碼
使用Hooks時,不須要進行此重構。反作用和狀態最終能夠在React函數組件中得到。這就是將函數無狀態組件重命名爲函數組件的合理緣由。api
反作用邏輯:在React類組件中,反作用大可能是在生命週期方法中引入的(例如componentDidMount,componentDidUpdate,componentWillUnmount)。反作用多是在React中獲取數據或與Browser API交互。一般,這些反作用來自設置和清理階段。例如,若是您想刪除您的監聽器,可能會遇到React性能問題。數組
// side-effects in a React class component
class MyComponent extends Component {
// setup phase
componentDidMount() {
// add listener for feature 1
// add listener for feature 2
}
// clean up phase
componentWillUnmount() {
// remove listener for feature 1
// remove listener for feature 2
}
...
}
// side-effects in React function component with React Hooks
function MyComponent() {
useEffect(() => {
// add listener for feature 1 (setup)
// return function to remove listener for feature 1 (clean up)
});
useEffect(() => {
// add listener for feature 2 (setup)
// return function to remove listener for feature 2 (clean up)
});
...
}
複製代碼
如今,若是您要在React類組件的生命週期方法中引入多個以上反作用,那麼全部反作用將按生命週期方法分組,而不是按反作用自己來分組。這就是React Hooks經過useEffect鉤子函數將反作用封裝在其中,而每一個hook在處理和清理階段都有其本身的反作用。您將在本教程的後面部分中看到如何經過在React Hook中添加和刪除監聽器來實現此功能。瀏覽器
React的抽象地獄:React中的高階組件和render props組件體現了抽象以及可複用性。還有React的上下文Context及其Provider和Consumer組件,它們引入了另外一個層級的抽象。React中全部這些高級模式都使用了所謂的包裝組件。如下組件的實現對於建立更大的React應用程序的開發人員來講不該該陌生。bash
import { compose } from 'recompose';
import { withRouter } from 'react-router-dom';
function App({ history, state, dispatch }) {
return (
<ThemeContext.Consumer>
{theme =>
<Content theme={theme}>
...
</Content>
}
</ThemeContext.Consumer>
);
}
export default compose(
withRouter,
withReducer(reducer, initialState)
)(App);
複製代碼
Sophie Alpert在React中將其稱爲「包裝地獄」。您不只會在實現中看到它,並且還會在瀏覽器中檢查組件時看到它。因爲Render Prop組件(包括React上下文中的Consumer組件)和High-Order組件,包裝的組件有數十個。它變成了一個不可讀的組件樹,由於全部抽象的邏輯都被其餘React組件所掩蓋。實際可見的組件很難在瀏覽器的DOM中找到。那麼,若是不須要這些附加組件,而是僅將邏輯做爲反作用封裝在函數中,該怎麼辦呢?那麼您能夠刪除全部這些包裝組件並展平組件樹的結構:react-router
function App() {
const theme = useTheme();
const history = useRouter();
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Content theme={theme}>
...
</Content>
);
}
export default App;
複製代碼
這就是React Hooks提出的建議。全部反作用都直接存在於組件中,而沒有引入其餘組件做爲業務邏輯的容器。容器消失了,邏輯僅存在於函數的React Hooks中。
JavaScript類混亂: JavaScript很好地融合了兩個概念:面向對象編程(OOP)和函數式編程。React向開發人員很好地展現了這兩個概念。一方面,React(和Redux)向人們介紹了由函數組成的函數式編程(FP),還有其餘函數的通用編程概念(例如,高階函數,JavaScript內置方法(如map,reduce,filter))以及其餘術語,例如做爲不變性(immutability )和反作用( side-effects)。React自己並無真正引入這些東西,由於它們是語言或編程範例自己的功能,可是它們在React中大量使用,使得每一個React開發人員都潛移默化地成爲更好的JavaScript開發人員。
另外一方面,React使用JavaScript類做爲定義React組件的一種方法。類僅是聲明,而組件其實是類的實例化。它建立一個類實例,而該類實例的this對象用於與類方法進行交互(例如setState,forceUpdate,其餘自定義類方法)。可是,對於沒有面向對象編程思想(OOP)的React初學者,課程的學習曲線更爲陡峭。這就是爲何類綁定,this對象和類繼承會形成混淆。
如今,許多人都認爲React不該該取消JavaScript類,這是人們不理解React Hooks。可是,引入Hooks API的假設之一是,對於React初學者來講,當他們一開始就編寫沒有JavaScript類的React組件時,學習曲線就更加平滑。
每次引入新功能時,人們都會對此加以關注。創新派會所以感到興奮,而部分人則對變革感到恐懼。我聽人們最關心React Hooks的問題是:
讓我來回答以上問題:
一切都變了:React Hooks未來會改變咱們編寫React應用程序的方式。可是,如今仍是什麼都沒有改變。您仍然可使用局部狀態和生命週期方法編寫類組件,還有其餘例如高階組件或render props組件。React團隊確保React保持向後兼容。與React 16.7相同。
React變得像Angular同樣臃腫:React一直被視爲具備輕量API的庫。沒錯,未來也是如此。可是,爲了適應幾年前基於組件構建應用程序的作法,而且不被其餘庫所取代,React引入了一些更改,以支持較舊的API。若是React有全新的東西,應該只有函數組件和React Hooks。可是React是在幾年前發佈的,須要進行調整以跟上現狀或改變現狀。也許幾年後將會棄用React類組件和生命週期方法,以支持React函數組件和React Hooks,可是目前,React團隊將React類組件保留在其工具庫中。畢竟,React團隊利用hooks做爲一項發明但願能夠長期使用。顯然,React Hooks爲React添加了另外一個API,可是有利於未來簡化React的API。我喜歡這種過渡,而不是推出一個與以前React大相徑庭的React2。
這沒用,用Class組件也能夠正常工做:假設您從零開始學習React,而後直接給您介紹React Hooks。也許建立React應用程序不會從React類組件開始,而是從React函數組件開始。您須要學習的全部組件都是React Hooks。它們管理狀態和反作用,所以您只須要了解state和effect hooks。對於React初學者來講,無需使用JavaScript類(繼承,this,綁定,super,...)帶來的全部其餘開銷,這些都是React類組件以前的作法。經過React Hooks學習React將會更加簡單。想象一下React Hooks是一種如何編寫React組件的新方法-這是一種新思惟。我參加過不少次React研討會,而且我是一個多疑的人,可是當我用React Hooks編寫了一些簡單的場景,我就確信這是編寫和學習React的最簡單方法。
最後,以這種方式進行思考:基於組件的解決方案(例如Angular,Vue和React)在每一個發行版中都在推進Web開發的邊界。它們創建在二十多年前發明的技術之上,而且不斷進行調整,使得Web開發在2018年變得不費吹灰之力。他們瘋狂地對其進行了優化,以知足當代的需求。咱們正在使用組件而不是HTML模板來構建Web應用程序。我能夠想將來象咱們將坐在一塊兒,爲瀏覽器發明基於組件的標準。Angular,Vue和React只是這一運動的先行者。
你已經在代碼中看到了一個典型的計數器示例的useState Hook。它用於管理功能組件中的本地狀態。讓咱們在更復雜的場景中使用該鉤子函數,在該示例中,咱們將管理列表項:
import React, { useState } from 'react';
const INITIAL_LIST = [
{
id: '0',
title: 'React with RxJS for State Management Tutorial',
url:
'https://www.robinwieruch.de/react-rxjs-state-management-tutorial/',
},
{
id: '1',
title: 'React with Apollo and GraphQL Tutorial',
url: 'https://www.robinwieruch.de/react-graphql-apollo-tutorial',
},
];
function App() {
const [list, setList] = useState(INITIAL_LIST);
return (
<ul>
{list.map(item => (
<li key={item.id}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;
複製代碼
useState鉤子函數接收一個初始狀態做爲參數,並經過使用數組解構來返回兩個可命名的變量。第一個變量是state的值,而第二個變量是則是更新狀態的函數。
此示例的目標是從列表中刪除一個項。爲了實現它,呈現列表中的每一個項都有一個帶有點擊事件的按鈕。能夠在函數組件中內聯一個onRemoveItem
函數,它稍後將使用list和setList。不須要將這些變量傳遞給函數,由於它們已在組件的外部範圍中可用。
function App() {
const [list, setList] = useState(INITIAL_LIST);
function onRemoveItem(id) {
// remove item from "list"
// set the new list in state with "setList"
}
return (
<ul>
{list.map(item => (
<li key={item.id}>
<a href={item.url}>{item.title}</a>
<button type="button" onClick={() => onRemoveItem(item.id)}>
Remove
</button>
</li>
))}
</ul>
);
}
複製代碼
咱們須要以某種方式知道應該從列表中刪除的列表項。使用高階函數,咱們能夠將該項的id傳遞給處理函數。不然,咱們將沒法識別應從列表中刪除的項目。
function App() {
const [list, setList] = useState(INITIAL_LIST);
function onRemoveItem(id) {
const newList = list.filter(item => item.id !== id); //刪除目標item
setList(newList); // 從新設置列表
}
return (
<ul>
{list.map(item => (
<li key={item.id}>
<a href={item.url}>{item.title}</a>
<button type="button" onClick={() => onRemoveItem(item.id)}>
Remove
</button>
</li>
))}
</ul>
);
}
複製代碼
你能夠根據傳遞給函數的id從列表中刪除列表項。而後,用filter函數過濾列表,並使用setList函數設置列表的新的state。
useState鉤子函數爲你提供了管理功能組件中的狀態所需的一切:初始狀態,最新狀態和狀態更新函數。其餘一切都是JavaScript。此外,你沒必要像之前在類組件中那樣爲狀態對象的淺層合併而煩惱。相反,你可使用useState來封裝一個域(例如,列表),可是若是你須要另外一個狀態(例如,計數器),則只需使用另外一個useState來封裝該域。
讓咱們轉到下一個名爲useEffect的鉤子函數。如上所述,功能組件應該可以經過鉤子管理狀態和反作用。經過useState掛鉤展現了管理狀態。如今出現了useEffect鉤子,用於產生反作用,這些反作用一般用於與Browser / DOM API或外部API(例如數據提取)進行交互。讓咱們看看如何經過實現一個簡單的秒錶將useEffect鉤子函數與瀏覽器API交互:
import React, { useState } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
return (
<div>
{!isOn && (
<button type="button" onClick={() => setIsOn(true)}>
Start
</button>
)}
{isOn && (
<button type="button" onClick={() => setIsOn(false)}>
Stop
</button>
)}
</div>
);
}
export default App;
複製代碼
目前尚未秒錶。可是至少有一個條件渲染顯示「開始」或「中止」按鈕。按鈕的狀態由useState掛鉤管理。
讓咱們用useEffect介紹咱們的反作用,該反作用記錄了一個間隔。它每秒發出一個console.log,在控制檯打印出來。
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
setInterval(() => console.log('tick'), 1000); // 每秒執行一次
});
return (
<div>
{!isOn && (
<button type="button" onClick={() => setIsOn(true)}>
Start
</button>
)}
{isOn && (
<button type="button" onClick={() => setIsOn(false)}>
Stop
</button>
)}
</div>
);
}
export default App;
複製代碼
爲了在組件卸載時清空定時器,能夠在useEffect中返回一個函數,以進行任何清理操做。例如,當該組件再也不存在時,不該有任何內存泄漏。
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
const interval = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(interval); // 這樣在組件卸載時,就會清空定時器
});
...
}
export default App;
複製代碼
如今,你要在掛載組件時設置反作用,並在卸下組件時清理反作用。若是你記錄useEffect鉤子中的函數被調用的次數,則會看到每次組件狀態改變時,它都會設置一個新的interval(例如,單擊「開始」 /「中止」按鈕)。
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
console.log('effect runs');
const interval = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(interval);
});
...
}
export default App;
複製代碼
爲了僅在組件的掛載和卸載時執行反作用,您能夠向其傳遞一個空數組做爲第二個參數。
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
const interval = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(interval);
}, []); // 這樣useEffect只會在掛載和卸載階段執行
...
}
export default App;
複製代碼
可是,因爲每次卸載時清空了定時器,所以咱們也須要在更新週期中setInterval。可是咱們能夠告訴效果useEffect僅在isOn變量更改時才運行。僅當數組中的變量之一更改時,useEffect纔會在更新週期內運行。若是將數組保留爲空,則效果將僅在掛載和卸載時運行,由於沒有變量可用於再次運行反作用。
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
const interval = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(interval);
}, [isOn]); // useEffect會在isOn變化時來執行
...
}
export default App;
複製代碼
不管isOn是true仍是false,setInterval都會運行。但咱們只但願在秒錶激活的時候才運行:
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
let interval;
if (isOn) {
interval = setInterval(() => console.log('tick'), 1000);
}
return () => clearInterval(interval);
}, [isOn]);
...
}
export default App;
複製代碼
如今在功能組件中引入另外一種狀態,以跟蹤秒錶的計時器。它用於更新計時器,但僅在秒錶被激活時使用。
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
const [timer, setTimer] = useState(0);
useEffect(() => {
let interval;
if (isOn) {
interval = setInterval(
() => setTimer(timer + 1), // 秒錶被激活時,計時器就+1
1000,
);
}
return () => clearInterval(interval);
}, [isOn]);
return (
<div>
{timer}
{!isOn && (
<button type="button" onClick={() => setIsOn(true)}>
Start
</button>
)}
{isOn && (
<button type="button" onClick={() => setIsOn(false)}>
Stop
</button>
)}
</div>
);
}
export default App;
複製代碼
代碼中仍然存在一個錯誤。當setInterval運行時,它將每秒增長1,從而更新計時器。可是,它始終依賴計時器的失效狀態。僅當inOn改變時,狀態才時正常的。爲了在間隔運行時始終接收計時器的最新狀態,能夠對始終具備最新狀態的狀態更新功能使用功能。
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
const [timer, setTimer] = useState(0);
useEffect(() => {
let interval;
if (isOn) {
interval = setInterval(
() => setTimer(timer => timer + 1),
1000,
);
}
return () => clearInterval(interval);
}, [isOn]);
...
}
export default App;
複製代碼
另外一種選擇是在計時器更改時也運行效果。而後效果將接收最新的計時器狀態。
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
const [timer, setTimer] = useState(0);
useEffect(() => {
let interval;
if (isOn) {
interval = setInterval(
() => setTimer(timer + 1),
1000,
);
}
return () => clearInterval(interval);
}, [isOn, timer]);
...
}
export default App;
複製代碼
這是使用瀏覽器API的秒錶的實現。若是要繼續,您也能夠經過提供「重置」按鈕來擴展現例。
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
const [timer, setTimer] = useState(0);
useEffect(() => {
let interval;
if (isOn) {
interval = setInterval(
() => setTimer(timer => timer + 1),
1000,
);
}
return () => clearInterval(interval);
}, [isOn]);
const onReset = () => {
setIsOn(false);
setTimer(0);
};
return (
<div>
{timer}
{!isOn && (
<button type="button" onClick={() => setIsOn(true)}>
Start
</button>
)}
{isOn && (
<button type="button" onClick={() => setIsOn(false)}>
Stop
</button>
)}
<button type="button" disabled={timer === 0} onClick={onReset}>
Reset
</button>
</div>
);
}
export default App;
複製代碼
useEffect鉤子函數用於React函數組件中的反作用,該函數用於與瀏覽器/ DOM API或其餘第三方API進行交互(例如,數據獲取)。
最後,當您瞭解了在功能組件中引入狀態和反作用的兩個最流行的鉤子函數以後,我要向您展現的最後一件事:自定義鉤子。沒錯,您能夠實現本身的自定義React Hook,能夠在您的應用程序或其餘應用程序中複用。讓咱們看看它們如何與示例應用程序一塊兒使用,該應用程序可以檢測您的設備是在線仍是離線。
import React, { useState } from 'react';
function App() {
const [isOffline, setIsOffline] = useState(false);
if (isOffline) {
return <div>Sorry, you are offline ...</div>;
}
return <div>You are online!</div>;
}
export default App;
複製代碼
再次,引入useEffect鉤子以產生反作用。在這種狀況下,該效果會添加和刪除監聽器,用於檢查設備是在線仍是離線。兩個偵聽器僅在安裝時設置一次,並在卸載時清除一次(空數組做爲第二個參數)。每當調用其中一個偵聽器時,它都會爲isOffline布爾值設置狀態。
import React, { useState, useEffect } from 'react';
function App() {
const [isOffline, setIsOffline] = useState(false);
function onOffline() {
setIsOffline(true);
}
function onOnline() {
setIsOffline(false);
}
useEffect(() => {
window.addEventListener('offline', onOffline);
window.addEventListener('online', onOnline);
return () => {
window.removeEventListener('offline', onOffline);
window.removeEventListener('online', onOnline);
};
}, []);
if (isOffline) {
return <div>Sorry, you are offline ...</div>;
}
return <div>You are online!</div>;
}
export default App;
複製代碼
如今一切都很好地封裝在一個useEffect中,也能夠在其餘地方複用。這就是爲何咱們能夠將這個功能提取爲其自定義鉤子函數,該鉤子遵循與其餘鉤子相同的命名約定。
import React, { useState, useEffect } from 'react';
function useOffline() {
const [isOffline, setIsOffline] = useState(false);
function onOffline() {
setIsOffline(true);
}
function onOnline() {
setIsOffline(false);
}
useEffect(() => {
window.addEventListener('offline', onOffline);
window.addEventListener('online', onOnline);
return () => {
window.removeEventListener('offline', onOffline);
window.removeEventListener('online', onOnline);
};
}, []);
return isOffline;
}
function App() {
const isOffline = useOffline();
if (isOffline) {
return <div>Sorry, you are offline ...</div>;
}
return <div>You are online!</div>;
}
export default App;
複製代碼
將定製鉤子提取爲函數並非惟一的事情。您還必須根據isOffline從自定義鉤子函數返回狀態,以便在您的應用程序中使用它來向離線用戶顯示消息。這是用於檢測您在線仍是離線的自定義鉤子。您能夠在React的文檔中閱讀有關自定義鉤子的更多信息。
React Hooks的可複用性很是高,由於有可能發展出一個能夠從npm安裝到任何React應用程序的自定義React Hooks生態系統。