import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div>
);
}
複製代碼
有時,咱們但願在React更新DOM以後運行一些額外的操做。如:html
這些操做不須要清理,也就是說能夠運行它們並當即忘記它們。下面咱們分別看看class和Hook是如何處理的react
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div> <p>You clicked {this.state.count} times</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div>
);
}
}
複製代碼
注意,咱們在componentDidMount和componentDidUpdate的時候執行了一樣的代碼。下面看看Hooks怎麼處理的git
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div>
);
}
複製代碼
useEffect作了什麼?github
爲何要在組件內部調用useEffect?npm
useEffect每次render後都執行嗎?數組
如今咱們對effect有了必定的瞭解,下面的代碼應該很容易懂了瀏覽器
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
複製代碼
也許你會發現每次render傳給useEffect的函數都不一樣,這是有意爲之的。實際上,這就是爲何咱們即便在useEffect內部讀取state也不用擔憂state過時。每次re-render,咱們都會安排一個不一樣的effect去取代以前的那個effect。在某種程度上,這使得effect更像是render的結果的一部分——每一個effect「屬於」特定的render。網絡
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
複製代碼
注意componentDidMount和componentWillUnmount的代碼需「相互鏡像」。生命週期方法迫使咱們拆分相互關聯的邏輯函數
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
複製代碼
Note性能
反作用函數不必定要返回具名函數。
以前在使用Hooks的動機那一章就有提到,使用Hook的緣由之一是class的生命週期使不相干的邏輯混在一塊兒,相關的邏輯散在各處,好比下面的代碼,
class FriendStatusWithCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0, isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
// ...
複製代碼
使用Hooks如何解決這個問題呢?
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
複製代碼
若是你習慣使用class組件,你可能會很疑惑爲何不是組件卸載的時候執行清理反作用的工做,而是在每次re-render的時候都要執行。下面咱們就看看爲何
前面咱們介紹了一個示例FriendStatus組件,該組件顯示朋友是否在線。咱們的類從this.props讀取friend.id,在組件掛載以後訂閱朋友狀態,並在卸載前取消訂閱。
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
複製代碼
**可是若是在該組件還在顯示的狀態下,friend屬性改變了怎麼辦?**組件顯示的將是原來那個friend的在線狀態。這是一個bug。而後後面取消訂閱調用又會使用錯誤的friend ID,還會在卸載時致使內存泄漏或崩潰。
在class組件中,咱們須要添加componentDidUpdate來處理這種狀況
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate(prevProps) {
// Unsubscribe from the previous friend.id
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// Subscribe to the next friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
複製代碼
忘記正確處理componentDidUpdate經常致使bug。
如今考慮使用Hooks實現這個組件
function FriendStatus(props) {
// ...
useEffect(() => {
// ...
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
複製代碼
這下沒bug了,即便咱們什麼也沒改
默認狀況下useEffect會在應用下一個effect以前清除以前的effect。爲了解釋清楚,請看下面這個訂閱和取消訂閱的調用序列
// Mount with { friend: { id: 100 } } props
// Run first effect
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);
// Update with { friend: { id: 200 } } props
// Clean up previous effect
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange);
// Run next effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);
// Update with { friend: { id: 300 } } props
// Clean up previous effect
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange);
// Run next effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);
// Unmount
// Clean up last effect
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange);
複製代碼
默認狀況下這種作法確保了邏輯連貫性,而且防止了經常在class組件裏出現的由於忘寫update邏輯而致使的bug
某些狀況下,在每次渲染後清理或執行effect可能會產生性能問題。在class組件中,咱們能夠經過在componentDidUpdate中編寫與prevProps或prevState的比較來解決這個問題
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
複製代碼
此要求很常見,它已內置到useEffect Hook API中。
若是從新渲染之間某些值沒有改變,你能夠告訴React跳過執行effect。只須要將一個數組做爲可選的第二個參數傳遞給useEffect
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
複製代碼
在上面的例子中,咱們將[count]做爲第二個參數傳遞。
這是什麼意思?
若是數組中有多個項,即便其中一個項不一樣,React也會從新執行這個effect。
對具備清理工做的effect一樣適用
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes
複製代碼
未來,第二個參數可能會被構建時轉換自動添加。
Note
- 若是你適用了這個優化,請確保數組包含了組件做用域內(例如props和state)effect用到的、隨時間變化的全部值。不然代碼可能引用了上一次render的那個過時的變量。Learn more about how to deal with functions and what to do when the array changes too often
- 若是僅執行effect並清理一次(在mount和unmount上),能夠傳遞一個空數組([])做爲第二個參數。
- 不要忘記React延遲執行行useEffect直到瀏覽器繪製完成,因此作額外的工做並非什麼問題。
- 咱們推薦使用
exhaustive-deps
規則(這是eslint-plugin-react-hooks
的一部分)。它會在錯誤地指定依賴項時發出警告並建議修復。
下面咱們將瞭解鉤子規則 - 它們對於使鉤子工做相當重要。