都 2022 年了你還是可能不懂 useEffect
Zet Chou@iCHEF
2022.08.04 / ReactJS.tw 社群小聚
-
iCHEF Senior Front-End Engineer
-
七年 React 開發經驗
-
ALPHA Camp 社群助教
-
曾任 SITCON / JSDC 主議程講者
-
JSDC 2019 / 2020 議程組工作人員
Zet Chou
周昱安
About me
在進入到 useEffect 之前...
Function component & Class component
你可能不知道的關鍵區別
Function components capture
the rendered values.
Consider this demo...
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
Live demo: https://codesandbox.io/s/pjqnl16lm7
Capture the props
class ProfilePage extends React.Component {
render() {
// Capture the props!
const props = this.props;
// Note: we are *inside render*.
// These aren't class methods.
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>Follow</button>;
}
}
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
State
function MessageThread() {
const [message, setMessage] = useState('');
const showMessage = () => {
alert('You said: ' + message);
};
const handleSendClick = () => {
setTimeout(showMessage, 3000);
};
const handleMessageChange = (e) => {
setMessage(e.target.value);
};
return (
<>
<input value={message} onChange={handleMessageChange} />
<button onClick={handleSendClick}>Send</button>
</>
);
}
- Class component 是基於 OOP,以 mutate 來存取 this.props / this.state
- Function component 則是基於 FP,以每次都重新執行函數並傳入獨立參數的方式來存取 props / state
每一次 render 都有他自己的...
每ㄧ次 render 都有它自己的 props & state
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
// 在第一次 render 時
function Counter() {
const count = 0; // 被 useState() 回傳
// ...
<p>You clicked {count} times</p>
// ...
}
// 經過一次點擊,我們的 component function 再次被呼叫
function Counter() {
const count = 1; // 被 useState() 回傳
// ...
<p>You clicked {count} times</p>
// ...
}
// 經過另一次點擊,我們的 component function 再次被呼叫
function Counter() {
const count = 2; // 被 useState() 回傳
// ...
<p>You clicked {count} times</p>
// ...
}
每當我們 setState,React 就會重新呼叫 component 函式來執行一次 render。
每次 render 時都會捕捉到屬於它自己的 counter state 的值,
這些值是個只存在於該次 render 中的常數。
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
每ㄧ次 render 都有它自己的 event handlers
function Counter() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
Live demo: https://codesandbox.io/s/w2wxl3yo0l
function sayHi(person) {
setTimeout(() => {
alert('Hello, ' + person.name);
}, 3000);
}
let someone = { name: 'Dan' };
sayHi(someone);
someone = { name: 'Zet' };
sayHi(someone);
someone = { name: 'Foo' };
sayHi(someone);
Live demo:
每ㄧ次 render 都有它自己的 event handlers
function Counter() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
// 在第一次 render 時
function Counter() {
const count = 0; // 被 useState() 回傳
// ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
// ...
// 裡面的 count 是 0 的那個版本的 handleAlertClick
<button onClick={handleAlertClick} />
// ...
}
// 經過一次點擊,我們的 component function 再次被呼叫
function Counter() {
const count = 1; // 被 useState() 回傳
// ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
// ...
// 裡面的 count 是 1 的那個版本的 handleAlertClick
<button onClick={handleAlertClick} />
// ...
}
// 經過另一次點擊,我們的 component function 再次被呼叫
function Counter() {
const count = 2; // 被 useState() 回傳
// ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
// ...
// 裡面的 count 是 2 的那個版本的 handleAlertClick
<button onClick={handleAlertClick} />
// ...
}
- 在每一次的 render 之間的 props & state 都是獨立、不互相影響的
- 在每一次的 render 中的 props & state 永遠都會保持不變,像是該次函式執行的常數
- event handlers 是以原始資料(props & state)延伸出來的另一種資料結果
- 因此,每一次 render 都有他自己的 event handlers
每ㄧ次 render 都有它自己的 event handlers
每ㄧ次 render 都有它自己的 effects
function Counter() {
const [count, setCount] = useState(0);
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>
);
}
每ㄧ次 render 都有它自己的 effects
function Counter() {
const [count, setCount] = useState(0);
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>
);
}
// 在第一次 render 時
function Counter() {
// ...
useEffect(
// 在第一次 render 時的 effect 函式
() => {
document.title = `You clicked ${0} times`;
}
);
// ...
}
// 經過一次點擊,我們的 component function 再次被呼叫
function Counter() {
// ...
useEffect(
// 在第二次 render 時的 effect 函式
() => {
document.title = `You clicked ${1} times`;
}
);
// ...
}
// 經過另一次點擊,我們的 component function 再次被呼叫
function Counter() {
// ...
useEffect(
// 在第三次 render 時的 effect 函式
() => {
document.title = `You clicked ${2} times`;
}
);
// ..
}
概念上來說,
你可以想像 effects 是 render 結果的副產物
每個 effect 都是專屬於特定的一次 render
當依賴的值會從外部發生 mutate 時,
closures 是不直覺、難以預測結果的
而當依賴的值永遠不變時,closures 是直覺易懂的,
因為它依賴的都是常數,執行的行為效果永遠固定
Cleanup function
useEffect(() => {
ChatAPI.subscribe(props.id, handleChange);
return () => {
ChatAPI.unsubscribe(props.id, handleChange);
};
});
// 第一次 render,props 是 { id: 10 }
function Example(props) {
// ...
useEffect(
// 第一次 render 的 Effect
() => {
ChatAPI.subscribe(10, handleChange);
// 清理第一次 render 的 effect
return () => {
ChatAPI.unsubscribe(10, handleChange);
};
}
);
// ...
}
// 第二次 render,props 是 { id: 20 }
function Example(props) {
// ...
useEffect(
// 第二次 render 的 Effect
() => {
ChatAPI.subscribe(20, handleChange);
// 清理第二次 render 的 effect
return () => {
ChatAPI.unsubscribe(20, handleChange);
};
}
);
// ...
}
每個在 component 裡 render 的函式(包含 event handlers、effects、cleanup、timeouts 或裡面呼叫的 API),
都會捕捉到定義它的那次 render 中的 props 和 state。
真正學會 useEffect 的第一步:
忘記 class component 的生命週期
宣告式的同步化,而非生命週期
-
宣告式
-
只關心目標與結果,而不關心細節過程與方法
-
-
每當我們呼叫 setState 更新資料時,React 就會以最新的資料重新執行 render,並產生對應的 React elements 畫面結果然後自動同步到 DOM。對於 render 本身來說,這個過程在「mount」或是「update」之間並沒有差異。
function Greeting({ name }) {
return (
<h1 className="Greeting">
Hello, {name}
</h1>
);
}
宣告式的同步化,而非生命週期
- useEffect 讓你根據目前的 props 和 state 來同步 React elements 以外的東西,並且避免阻塞 UI 畫面的渲染
- 理想上這個 effect 無論隨著 render 執行了幾次,你的程式都應該保持同步且正常運作
function Greeting({ name }) {
useEffect(() => {
document.title = 'Hello, ' + name;
});
return (
<h1 className="Greeting">
Hello, {name}
</h1>
);
}
為什麼 effect & cleanup 要在每次 render 後都執行
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate(prevProps) {
// 從先前的 friend.id 取消訂閱
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// 訂閱下一個 friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
為什麼 effect & cleanup 要在每次 render 後都執行
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate(prevProps) {
// 從先前的 friend.id 取消訂閱
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// 訂閱下一個 friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
function FriendStatus(props) {
// ...
useEffect(() => {
// ...
ChatAPI.subscribeToFriendStatus(
props.friend.id,
handleStatusChange
);
return () => {
ChatAPI.unsubscribeFromFriendStatus(
props.friend.id,
handleStatusChange
);
};
});
為什麼 effect & cleanup 要在每次 render 後都執行
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate(prevProps) {
// 從先前的 friend.id 取消訂閱
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// 訂閱下一個 friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
function FriendStatus(props) {
// ...
useEffect(() => {
// ...
ChatAPI.subscribeToFriendStatus(
props.friend.id,
handleStatusChange
);
return () => {
ChatAPI.unsubscribeFromFriendStatus(
props.friend.id,
handleStatusChange
);
};
});
// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // 執行第一個 effect
// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除前一個 effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // 執行下一個 effect
// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除前一個 effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // 執行下一個 effect
// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最後一個 effect
dependencies 是一種效能最佳化
componentDidUpdate(prevProps) {
if (prevProps.friend.id === this.props.friend.id) {
return;
}
// 從先前的 friend.id 取消訂閱
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// 訂閱下一個 friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
useEffect(
() => {
const handleStatusChange = (status) => {
// ...
};
ChatAPI.subscribeToFriendStatus(
props.friend.id,
handleStatusChange
);
return () => {
ChatAPI.unsubscribeFromFriendStatus(
props.friend.id,
handleStatusChange
);
};
},
[props.friend.id]
);
dependencies 是一種效能最佳化
function Greeting({ name }) {
const [counter, setCounter] = useState(0);
useEffect(() => {
document.title = 'Hello, ' + name;
});
return (
<h1 className="Greeting">
Hello, {name}
<button onClick={() => setCounter(count + 1)}>
Increment
</button>
</h1>
);
}
dependencies 指的是「同步動作的資料依賴清單」,如果這個清單中記載的所有依賴資料都與上一次 render 時沒有差異,就代表沒有再次進行同步的需要,
因此就可以略過本次 effect 來節省效能。
function Greeting({ name }) {
const [counter, setCounter] = useState(0);
useEffect(() => {
document.title = 'Hello, ' + name;
}, [name]);
return (
<h1 className="Greeting">
Hello, {name}
<button onClick={() => setCounter(count + 1)}>
Increment
</button>
</h1>
);
}
- Function component 沒有生命週期的 API,只有 useEffect 用於「同步資料到 effect 行為」
- useEffect 讓你根據目前的資料來同步 React elements(畫面)以外的任何事物
- 在一般情況下,useEffect 會在每次 component render 然後瀏覽器完成 DOM 的更新 & 繪製畫面後才執行,以避免阻塞 component render 的過程 & 瀏覽器繪製畫面的過程
- useEffect 在概念上並不區分 mount 與 update 的情況,它們被視為是同一種情境
- 預設情況下,每一次 render 後都應該執行屬於該 render 的 useEffect,來確保同步的正確性與完整性
- 理想上這個 useEffect 無論隨著 render 重新執行了幾次,你的程式都應該保持同步且正常運作
- useEffect 的 dependencies 是一種「忽略某些不必要的執行」的效能最佳化,而不是用來控制 effect 發生在特定的 component 生命週期,或特定的商業邏輯時機
useEffect 的核心思考模型整理
不要欺騙 hooks 的 dependencies chain
function SearchResults() {
async function fetchData() {
// ...
}
useEffect(
() => {
fetchData();
},
[] // 這樣寫是 ok 的嗎? 並非總是沒問題,而且有更好的寫法
);
// ...
}
Consider this demo...
function Counter() {
const [count, setCount] = useState(0);
useEffect(
() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
},
[]
);
return <h1>{count}</h1>;
}
Live demo: https://codesandbox.io/s/91n5z8jo7r
// 第一次 render,count state 是 0
function Counter() {
// ...
useEffect(
// 第一次 render 的 effect
() => {
const id = setInterval(() => {
setCount(0 + 1); // 永遠 setCount(1)
}, 1000);
return () => clearInterval(id);
},
[] // 永遠不會重新執行
);
// ...
}
// 接下來的每次 re-render,count state 都是 1
function Counter() {
// ...
useEffect(
// 這個 effect 會永遠被忽略,
// 因為我們欺騙 React 說 effect dependencies 是空的
() => {
const id = setInterval(() => {
setCount(1 + 1);
}, 1000);
return () => clearInterval(id);
},
[]
);
// ...
}
function Counter() {
const [count, setCount] = useState(0);
useEffect(
() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
},
[]
);
return <h1>{count}</h1>;
}
// 第一次 render,count state 是 0
function Counter() {
// ...
useEffect(
// 第一次 render 的 effect
() => {
const id = setInterval(() => {
setCount(0 + 1); // setCount(count + 1)
}, 1000);
return () => clearInterval(id);
},
[0] // [count]
);
// ...
}
// 第二次 render,count state 是 1
function Counter() {
// ...
useEffect(
// 第二次 render 的 effect
() => {
const id = setInterval(() => {
setCount(1 + 1); // setCount(count + 1)
}, 1000);
return () => clearInterval(id);
},
[1] // [count]
);
// ...
}
function Counter() {
const [count, setCount] = useState(0);
useEffect(
() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
},
[count]
);
return <h1>{count}</h1>;
}
function Counter() {
const [count, setCount] = useState(0);
useEffect(
() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
},
[count]
);
return <h1>{count}</h1>;
}
// 第一次 render,count state 是 0
function Counter() {
// ...
useEffect(
// 第一次 render 的 effect
() => {
const id = setInterval(() => {
setCount(0 + 1); // setCount(count + 1)
}, 1000);
return () => clearInterval(id);
},
[0] // [count]
);
// ...
}
// 第二次 render,count state 是 1
function Counter() {
// ...
useEffect(
// 第二次 render 的 effect
() => {
const id = setInterval(() => {
setCount(1 + 1); // setCount(count + 1)
}, 1000);
return () => clearInterval(id);
},
[1] // [count]
);
// ...
}
function Counter() {
const [count, setCount] = useState(0);
useEffect(
() => {
const id = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
return () => clearInterval(id);
},
[]
);
return <h1>{count}</h1>;
}
函式與 dependencies
把函式定義移到 useEffect 中
function SearchResults() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('react');
useEffect(
() => {
// 把函式定義移到 useEffect 中
// 這個函式只有在 effect 執行時才會重新產生
async function fetchData() {
const result = await axios(
`https://hn.algolia.com/api/v1/search?query=${query}`,
);
setData(result.data);
}
fetchData();
},
[query] // 這裡的 dependencies 是誠實的
);
// ...
function SearchResults() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('react');
async function fetchData() {
const result = await axios(
`https://hn.algolia.com/api/v1/search?query=${query}`,
);
setData(result.data);
}
useEffect(
() => {
fetchData();
},
[] // 這樣是 ok 的嗎?
);
// ...
function SearchResults() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('react');
async function fetchData() {
const result = await axios(
`https://hn.algolia.com/api/v1/search?query=${query}`,
);
setData(result.data);
}
useEffect(
() => {
fetchData();
},
[] // deps 不誠實,effect 使用了內部沒有的變數 "fetchData"
);
// ...
Hooks exhaustive-deps linter rule
function SearchResults() {
async function fetchData(query) {
const result = await axios(
`https://hn.algolia.com/api/v1/search?query=${query}`,
);
}
useEffect(
() => {
fetchData('react').then(result => { /* 用資料進行某些操作 */ });
},
[]
);
useEffect(
() => {
fetchData('vue').then(result => { /* 用資料進行某些操作 */ });
},
[]
);
function SearchResults() {
async function fetchData(query) {
const result = await axios(
`https://hn.algolia.com/api/v1/search?query=${query}`,
);
}
useEffect(
() => {
fetchData('react').then(result => { /* 用資料進行某些操作 */ });
},
// deps 遺漏:fetchData
[]
);
useEffect(
() => {
fetchData('vue').then(result => { /* 用資料進行某些操作 */ });
},
// deps 遺漏:fetchData
[]
);
function SearchResults() {
async function fetchData(query) {
const result = await axios(
`https://hn.algolia.com/api/v1/search?query=${query}`,
);
}
useEffect(
() => {
fetchData('react').then(result => { /* 用資料進行某些操作 */ });
},
// deps 誠實
[fetchData]
);
useEffect(
() => {
fetchData('vue').then(result => { /* 用資料進行某些操作 */ });
},
// deps 誠實
[fetchData]
);
無效的 dependencies 效能最佳化
function SearchResults() {
async function fetchData(query) {
const result = await axios(
`https://hn.algolia.com/api/v1/search?query=${query}`,
);
}
useEffect(
() => {
fetchData('react').then(result => { /* 用資料進行某些操作 */ });
},
// deps 誠實,但是 fetchData 函式每次 render 時都會重新產生,
// 因此這個 useEffect deps 的效能最佳化完全無效
[fetchData]
);
useEffect(
() => {
fetchData('vue').then(result => { /* 用資料進行某些操作 */ });
},
// deps 誠實,但是 fetchData 函式每次 render 時都會重新產生,
// 因此這個 useEffect deps 的效能最佳化完全無效
[fetchData]
);
解決方案一:
把與 component 資料流無關的流程抽到 component 外部
async function fetchData(query) {
const result = await axios(
`https://hn.algolia.com/api/v1/search?query=${query}`,
);
return result;
}
function SearchResults() {
useEffect(
() => {
fetchData('react').then(result => { /* 用資料進行某些操作 */ });
},
// deps 誠實,因為 fetchData 是一個在 component 外部永遠不會改變的函式
[]
);
useEffect(
() => {
fetchData('vue').then(result => { /* 用資料進行某些操作 */ });
},
/// deps 誠實,因為 fetchData 是一個在 component 外部永遠不會改變的函式
[]
);
解決方案二:
把 useEffect 依賴的函式以 useCallback 包起來
function SearchResults(props) {
const fetchData = useCallback(
async (query) => {
const result = await axios(
`https://hn.algolia.com/api/v1/search?query=${query}&hitsPerPage=${props.rows}`,
);
return result;
},
[props.rows] // callback deps 誠實
);
useEffect(
() => {
fetchData('react').then(result => { /* 用資料進行某些操作 */ });
},
// effect deps 誠實,
// 且只有當 props.rows 不同時,fetchData 才會被重新產生,連帶的此時 effect 才會再次被執行
// 而如果 props.rows 沒有改變時,則連帶的這個 effect 就會被忽略
// 因此這裡的 effect deps 效能最佳化可以正常發揮效果
[fetchData]
);
// ...
function Parent() {
const [query, setQuery] = useState('react');
// fetchData 會保持不變,直到 query 的值與前一次 render 時不同
const fetchData = useCallback(
() => {
const result = await axios(
`https://hn.algolia.com/api/v1/search?query=${query}`,
);
return result;
},
[query] // callback deps 誠實
);
return <Child fetchData={fetchData} />
}
function Child({ fetchData }) {
let [data, setData] = useState(null);
useEffect(
() => {
fetchData().then(setData);
},
[fetchData] // effect deps 誠實
);
// ...
}
函式在 function component 與 hooks 中
是屬於資料流的一部份
class Parent extends Component {
state = {
query: 'react'
};
async fetchData = () => {
const result = await axios(
`https://hn.algolia.com/api/v1/search?query=${this.state.query}`,
);
return result;
};
render() {
return <Child fetchData={this.fetchData} />;
}
}
class Child extends Component {
state = {
data: null
};
componentDidMount() {
this.props.fetchData();
}
render() {
// ...
}
}
class Parent extends Component {
state = {
query: 'react'
};
async fetchData = () => {
const result = await axios(
`https://hn.algolia.com/api/v1/search?query=${this.state.query}`,
);
return result;
};
render() {
return <Child fetchData={this.fetchData} />;
}
}
class Child extends Component {
state = {
data: null
};
componentDidMount() {
this.props.fetchData();
}
componentDidUpdate(prevProps) {
// 這個條件式永遠都不會是 true
if (this.props.fetchData !== prevProps.fetchData) {
this.props.fetchData();
}
}
render() {
// ...
}
}
class Parent extends Component {
state = {
query: 'react'
};
async fetchData = () => {
const result = await axios(
`https://hn.algolia.com/api/v1/search?query=${this.state.query}`,
);
return result;
};
render() {
return <Child fetchData={this.fetchData} />;
}
}
class Child extends Component {
state = {
data: null
};
componentDidMount() {
this.props.fetchData();
}
componentDidUpdate(prevProps) {
// 這個呼叫會在每一次的 render 時都再次觸發
this.props.fetchData();
}
render() {
// ...
}
}
class Parent extends Component {
state = {
query: 'react'
};
async fetchData = () => {
const result = await axios(
`https://hn.algolia.com/api/v1/search?query=${this.state.query}`,
);
return result;
};
render() {
return <Child fetchData={this.fetchData} query={this.state.query} />;
}
}
class Child extends Component {
state = {
data: null
};
componentDidMount() {
this.props.fetchData();
}
componentDidUpdate(prevProps) {
if (this.props.query !== prevProps.query) {
this.props.fetchData();
}
}
render() {
// ...
}
}
useCallback & useMemo 讓由原始資料產生出來的延伸資料能夠完全的參與資料流,
並以 dependencies chain 維持 useEffect 的同步可靠性
Race conditions
class Article extends Component {
state = {
article: null
};
componentDidMount() {
this.fetchData(this.props.id);
}
async fetchData(id) {
const article = await API.fetchArticle(id);
this.setState({ article });
}
// ...
}
class Article extends Component {
state = {
article: null
};
componentDidMount() {
this.fetchData(this.props.id);
}
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
this.fetchData(this.props.id);
}
}
async fetchData(id) {
const article = await API.fetchArticle(id);
this.setState({ article });
}
// ...
}
function Article({ id }) {
const [article, setArticle] = useState(null);
useEffect(() => {
let didCancel = false;
async function fetchData() {
const article = await API.fetchArticle(id);
if (!didCancel) {
setArticle(article);
}
}
fetchData();
return () => {
didCancel = true;
};
}, [id]);
// ...
}
useReducer 是 dependencies chain 的合法作弊手段
Live demo: https://codesandbox.io/s/zxn70rnkx
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
useEffect(
() => {
const id = setInterval(() => {
setCount(prevCount => prevCount + step);
}, 1000);
return () => clearInterval(id);
},
[step]
);
return (
<>
<h1>{count}</h1>
<input
value={step}
onChange={e => setStep(Number(e.target.value))}
/>
</>
);
}
從動作分離更新
Live demo: https://codesandbox.io/s/xzr480k0np
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(
() => {
const id = setInterval(() => {
dispatch({ type: 'tick' }); // 而不是 setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
},
[] // deps 誠實
);
const initialState = {
count: 0,
step: 1,
};
function reducer(state, action) {
const { count, step } = state;
if (action.type === 'tick') {
return { count: count + step, step };
} else if (action.type === 'step') {
return { count, step: action.step };
} else {
throw new Error();
}
}
Live demo: https://codesandbox.io/s/7ypm405o8q
function Counter({ step }) {
const [count, dispatch] = useReducer(reducer, 0);
function reducer(state, action) {
if (action.type === 'tick') {
return state + step;
} else {
throw new Error();
}
}
useEffect(
() => {
const id = setInterval(() => {
dispatch({ type: 'tick' });
}, 1000);
return () => clearInterval(id);
},
[] // deps 誠實
);
return <h1>{count}</h1>;
}
以 dependencies 來控制 useEffect
執行邏輯的誤區
componentDidMount?
- useEffect 的用途是同步資料到 effect 效果,不是生命週期
- Function component & hooks 本身也沒有提供任何生命週期的 API
- useEffect 的 dependencies 是一種「忽略某些不必要的執行」的效能最佳化,而不是用來控制 effect 發生在特定的 component 生命週期,或特定的商業邏輯時機
componentDidMount in function component?
componentDidMount in function component?
import { useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
useEffect(
() => {
const id = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
return () => clearInterval(id);
},
[]
);
useEffect(
() => {
// do something ...
console.log('do something effect...');
},
);
return <div>{count}</div>;
}
componentDidMount in function component?
import { useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
useEffect(
() => {
const id = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
return () => clearInterval(id);
},
[]
);
useEffect(
() => {
// do something ...
console.log('do something effect...'); // 在 React 18 中會執行兩次
},
[]
);
return <div>{count}</div>;
}
componentDidMount in function component?
import { useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
useEffect(
() => {
const id = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
return () => clearInterval(id);
},
[]
);
useEffect(
() => {
// do something ...
console.log('do something effect...'); // 在 React 18 中會執行兩次
},
[]
);
return <div>{count}</div>;
}
componentDidMount in function component?
function App() {
const [count, setCount] = useState(0);
const isEffectCalledRef = useRef(false);
useEffect(
() => {
const id = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
return () => clearInterval(id);
},
[]
);
useEffect(
() => {
if (!isEffectCalledRef.current) {
isEffectCalledRef.current = true;
// do something effect...
console.log('do something effect...');
}
},
[]
);
return <div>{count}</div>;
}
componentDidMount in function component?
function App() {
const [count, setCount] = useState(0);
const isEffectCalledRef = useRef(false);
useEffect(
() => {
const id = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
return () => clearInterval(id);
},
[]
);
useEffect(
() => {
if (!isEffectCalledRef.current) {
isEffectCalledRef.current = true;
// do something effect...
console.log('do something effect...');
}
}
);
return <div>{count}</div>;
}
用 dependencies 來判斷執行邏輯?
function App() {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);
useEffect(
() => {
const id = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
return () => clearInterval(id);
},
[]
);
useEffect(
() => {
setCount(prevCount => prevCount * 2);
},
[todos] // deps 不誠實,填寫了未使用的依賴
);
// ...
}
用 dependencies 來判斷執行邏輯?
function App() {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);
const prevTodos = usePreviousValue(todos);
useEffect(
() => {
const id = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
return () => clearInterval(id);
},
[]
);
useEffect(
() => {
if (prevTodos !== todos) {
setCount(prevCount => prevCount * 2);
}
},
[todos] // deps 誠實
);
// ...
}
Reusable state — React 18 的 useEffect 在 mount 時為何會執行兩次?
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<StrictMode>
<App />
</StrictMode>
);
function App() {
useEffect(
// 在 React 18 的 development env + strict mode 時,
// 這個 effect 在 mount 時會執行兩次
() => {
// do something ...
console.log('do something effect...');
},
[]
);
// ...
}
- React 預計在未來添加的許多新功能們有一個共同前提約束
- Component 必須要設計得有足夠的彈性來多次 mount & unmount 也不會壞掉
- Offscreen API
- 讓 React 可以在 UI 切換時保留 component 的 local state 以及對應的真實 DOM elements,像是把他們暫時隱藏起來而不是真的移除它們
- 當這個 component 有再次顯示的需要時,就能以之前留下來的 state 狀態再次 mount
- 為了確保你的 component 能支援上述的特性,effect 必須要是可重複執行也不會壞掉的
Reusable State
Strict mode + dev env 時會自動 mount 兩次,
是在模擬「mount => unmount => mount 」的過程,
幫助開發者檢查 effect 的設計是否滿足這個彈性的要求。
Effect 調教建議:fetch API
useEffect(
() => {
async function startFetching() {
const json = await fetchTodos(userId);
setTodos(json);
}
startFetching();
},
[userId]
);
Effect 調教建議:fetch API
useEffect(
() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
},
[userId]
);
Effect 調教建議:控制 React 外部的插件
useEffect(
() => {
const map = mapRef.current;
map.setZoomLevel(zoomLevel);
},
[zoomLevel]
);
useEffect(
() => {
if (!mapRef.current) {
mapRef.current = new FooMap();
}
},
// 這裡是因為沒有任何依賴才填空陣列,
// 而不是為了控制 effect 只執行一次
[]
);
Effect 調教建議:控制 React 外部的插件
useEffect(
() => {
const dialog = dialogRef.current;
dialog.showModal();
return () => dialog.close();
},
[]
);
Effect 調教建議:訂閱事件
useEffect(
() => {
function handleScroll(e) {
console.log(e.clientX, e.clientY);
}
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll)
};
},
[]
);
Effect 調教建議:手動操作 DOM
useEffect(
() => {
const node = ref.current;
node.style.opacity = 1; // Trigger the animation
return () => {
node.style.opacity = 0; // Reset to the initial value
};
},
[]
);
Effect 調教建議:傳送 log 紀錄
useEffect(
() => {
logVisit(url); // 傳送一個 API request 去 log 經過這裡
},
[url]
);
不該是 effect:使用者的操作所觸發的事情
useEffect(
() => {
// 錯誤:這個 request 會在 dev env 自動被送出兩次
fetch('/api/buy', { method: 'POST' });
},
[]
);
不該是 effect:使用者的操作所觸發的事情
function handleClick() {
// 結帳購買的動作將會由使用者進行操作後才對應觸發一次
fetch('/api/buy', { method: 'POST' });
}
總結
- useEffect 的正確用途
- Function component 沒有生命週期的 API,只有 useEffect 用於「從資料同步到 effect 的行為與影響」
- useEffect 讓你根據目前的資料來同步 React elements(畫面)以外的任何事物
- useEffect 是隨著每次 render 後而自動觸發的
- 預設情況下,每一次 render 後都應該執行屬於該 render 的 useEffect,來確保同步的正確性與完整性
- useEffect 的 dependencies 是一種「忽略某些不必要的同步」的效能最佳化,而不是用來控制 effect 發生在特定的 component 生命週期,或特定的商業邏輯時機
- 確保你有自己寫邏輯來讓 effect 在你預期的條件情境時才執行,而不該依靠 dependencies 來控制這件事情
- useEffect 應設計成即使多次重複執行也有保持邏輯行為正確的彈性
- 確保你的 useEffect 無論隨著 render 重新執行了幾次,你的程式結果都應該保持同步且正常運作
- Strict mode + dev 環境時, component 會自動被 mount 兩次。這是在模擬「mount => unmount => mount 」的過程, 幫助開發者檢查 effect 的設計是否滿足這個彈性的要求
- 如果 effect 多次執行會導致問題,可以優先考慮實作 effect 的 cleanup function 來停止或逆轉 effect 中造成的影響
- https://overreacted.io/how-are-function-components-different-from-classes/
- https://overreacted.io/a-complete-guide-to-useeffect/
- https://reactjs.org/docs/hooks-effect.html
- https://beta.reactjs.org/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development
- https://github.com/reactwg/react-18/discussions/19
參考資料來源
iCHEF 產品團隊誠徵前端工程師中!
Q & A 時間
都 2022 年了你還是可能不懂 useEffect
By tz5514
都 2022 年了你還是可能不懂 useEffect
- 926