Zet Chou@iCHEF
2022.08.04 / ReactJS.tw 社群小聚
iCHEF Senior Front-End Engineer
七年 React 開發經驗
ALPHA Camp 社群助教
曾任 SITCON / JSDC 主議程講者
JSDC 2019 / 2020 議程組工作人員
Zet Chou
周昱安
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
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>
);
}
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>
</>
);
}
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>
);
}
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:
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} />
// ...
}
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>
);
}
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 是直覺易懂的,
因為它依賴的都是常數,執行的行為效果永遠固定
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。
宣告式
只關心目標與結果,而不關心細節過程與方法
每當我們呼叫 setState 更新資料時,React 就會以最新的資料重新執行 render,並產生對應的 React elements 畫面結果然後自動同步到 DOM。對於 render 本身來說,這個過程在「mount」或是「update」之間並沒有差異。
function Greeting({ name }) {
return (
<h1 className="Greeting">
Hello, {name}
</h1>
);
}
function Greeting({ name }) {
useEffect(() => {
document.title = 'Hello, ' + name;
});
return (
<h1 className="Greeting">
Hello, {name}
</h1>
);
}
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
);
}
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
);
};
});
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
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]
);
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 SearchResults() {
async function fetchData() {
// ...
}
useEffect(
() => {
fetchData();
},
[] // 這樣寫是 ok 的嗎? 並非總是沒問題,而且有更好的寫法
);
// ...
}
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>;
}
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"
);
// ...
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]
);
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]
);
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 外部永遠不會改變的函式
[]
);
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 的同步可靠性
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]);
// ...
}
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>;
}
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>;
}
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>;
}
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>;
}
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>;
}
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>;
}
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 不誠實,填寫了未使用的依賴
);
// ...
}
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 誠實
);
// ...
}
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...');
},
[]
);
// ...
}
Strict mode + dev env 時會自動 mount 兩次,
是在模擬「mount => unmount => mount 」的過程,
幫助開發者檢查 effect 的設計是否滿足這個彈性的要求。
useEffect(
() => {
async function startFetching() {
const json = await fetchTodos(userId);
setTodos(json);
}
startFetching();
},
[userId]
);
useEffect(
() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
},
[userId]
);
useEffect(
() => {
const map = mapRef.current;
map.setZoomLevel(zoomLevel);
},
[zoomLevel]
);
useEffect(
() => {
if (!mapRef.current) {
mapRef.current = new FooMap();
}
},
// 這裡是因為沒有任何依賴才填空陣列,
// 而不是為了控制 effect 只執行一次
[]
);
useEffect(
() => {
const dialog = dialogRef.current;
dialog.showModal();
return () => dialog.close();
},
[]
);
useEffect(
() => {
function handleScroll(e) {
console.log(e.clientX, e.clientY);
}
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll)
};
},
[]
);
useEffect(
() => {
const node = ref.current;
node.style.opacity = 1; // Trigger the animation
return () => {
node.style.opacity = 0; // Reset to the initial value
};
},
[]
);
useEffect(
() => {
logVisit(url); // 傳送一個 API request 去 log 經過這裡
},
[url]
);
useEffect(
() => {
// 錯誤:這個 request 會在 dev env 自動被送出兩次
fetch('/api/buy', { method: 'POST' });
},
[]
);
function handleClick() {
// 結帳購買的動作將會由使用者進行操作後才對應觸發一次
fetch('/api/buy', { method: 'POST' });
}