目前這個系列已挑選ChatGPT、前端&React相關章節出版成書!
感謝大家的閱讀與支持!
《用 ChatGPT 詠唱來點亮 React & 前端技能樹》
歷經重新整理、校正、編排、與修改後,讓章節內容之間的銜接更平順,除了補充上更多的解說之外,甚至有部分章節為重新撰寫的內容,讓理解變得更輕鬆!
目前已在天瓏書局、博客來、金石堂等通路都買得到了! 也有電子書的選擇!
歡迎各位點擊連結選購,謝謝大家~
在上一個章節,已經充分了解要怎麼在元件中設置「State」(狀態)
但在介紹 useState
時,ChatGPT有提到一個陌生的名詞
他說「在React中引入 useState
鉤子(hook)」
他說useState是一個hook,但hook是什麼?
在React中,hook(鉤子)是一種讓你可以在函式型元件中使用狀態(state)與React特性的方式。它們讓你在無需轉換成類別元件的情況下,可以使用React的功能。
最常用的hook是
useState
和useEffect
。
useState
可以讓你在函式型元件中添加狀態。它接受一個初始狀態並返回一個包含目前狀態值和一個更新狀態的函式。舉例來說:import React, { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); return ( <div> <p>目前計數: {count}</p> <button onClick={() => setCount(count + 1)}>增加</button> </div> ); } export default Counter;
useEffect
讓你可以在每次渲染後執行副作用操作,比如資料的獲取或訂閱事件。舉例來說:import React, { useState, useEffect } from 'react'; function Example() { const [count, setCount] = useState(0); useEffect(() => { document.title = `目前計數: ${count}`; }); return ( <div> <p>目前計數: {count}</p> <button onClick={() => setCount(count + 1)}>增加</button> </div> ); } export default Example;
這些只是其中兩個例子,React有許多不同的hook可以讓你在函式型元件中使用狀態、生命周期等特性。當然,你也可以創建自己的自定義hook。
在一開始【Day15】ChatGPT請教教我:React入門(一)- 起始安裝、JSX元素、元件!
在介紹React的元件時
就有提到元件有函式型(Functional Component)與類別元件(Class Component)
Class Component 的寫法要牽涉到 class的語法,寫法也比較複雜,透過繼承 React.Component
Class 來建立一個元件,透過這個繼承的Class來達成一些狀態管理、或是render 顯示畫面的功能
最初開發主要使用Class Component 來撰寫
後來Hook和Functional Component 一起出現
讓開發可以用函數來寫React元件,且具備原本Class Component 所提供的功能
讓function內也能夠管理狀態、使用React給的功能
所以ChatGPT才會提到「讓你在無需轉換成類別元件的情況下,可以使用React的功能。」
接著ChatGPT提到了useEffect
,說useEffect
可以「執行副作用操作」
什麼是副作用呢?
在程式設計的背景下,副作用(Side Effect)是指一段程式碼在執行過程中會對程式之外的狀態進行修改或產生其他影響的行為。
在React中,副作用通常指的是在函式元件中使用
useEffect
這個hook。useEffect
可以讓你在每次渲染之後執行一些操作,這些操作可能會引起網路請求、設置訂閱、手動修改DOM等,這些都是副作用,因為它們會影響到程式之外的狀態。例如,在一個React函式元件中,你可以使用
useEffect
來設置一個定時器:import React, { useState, useEffect } from 'react'; function Timer() { const [seconds, setSeconds] = useState(0); useEffect(() => { const interval = setInterval(() => { setSeconds(prevSeconds => prevSeconds + 1); }, 1000); return () => clearInterval(interval); }, []); // 第二個參數是一個依賴陣列,若空陣列代表只在元件掛載與卸載時執行 return ( <div> <p>已過秒數: {seconds}</p> </div> ); } export default Timer;
這裡的
setInterval
就是一個副作用,因為它會影響到程式之外的狀態(即定時器的運作)。了解和處理副作用是一個重要的程式設計概念,因為它們可能會對程式的行為造成不可預測的影響。在React中,
useEffect
提供了一個方便的方式來處理這些副作用。
上面的內容,指定了「顯示該元件的時候就開始執行」
他不是我們按下按鈕,觸發什麼事件時執行的程式內容
而是「在XXX 之後做OOO」這類的處理
接下來請ChatGPT介紹useEffect
的語法介紹,來學習useEffect
的使用
當然可以,讓我們針對
useEffect
進行更深入的解釋。
useEffect
是 React 提供的一個 hook,它讓你可以在函式型元件中執行副作用操作。這些副作用可以包括資料的獲取、設置訂閱、手動修改 DOM,以及其他會影響程式之外狀態的操作。
useEffect
接受兩個參數:一個是函式,另一個是一個依賴陣列(dependency array)。1. 基本語法
useEffect(() => { // 執行副作用操作 }, [依賴1, 依賴2, ...]);
- 第一個參數是一個函式,它包含了你想要在每次渲染後執行的操作。
- 第二個參數是一個陣列,包含了所有可能會影響這個副作用的變數或值。當這些變數或值有變化時,
useEffect
會重新執行。2. 為何需要依賴陣列?
依賴陣列的存在是為了告訴 React 哪些變數會影響到這個副作用。如果某個變數沒有在依賴陣列中列出,那麼當該變數發生變化時,
useEffect
不會重新執行。舉例來說:
useEffect(() => { console.log('這裡的代碼會在每次渲染後執行'); }, []); // 空陣列代表沒有依賴,只會在元件掛載與卸載時執行
這段程式碼中的
useEffect
將在元件掛載時執行一次,並在元件卸載時清理。3. 如何處理清理工作?
如果你的副作用操作包括了需要清理的工作(比如清除訂閱或定時器),你可以在
useEffect
的回傳函式中執行這些清理操作。useEffect(() => { // 執行副作用操作 return () => { // 執行清理操作 }; }, [依賴1, 依賴2, ...]);
4. 延遲執行副作用
如果你想要在下一次渲染後執行副作用,可以在函式中使用
setTimeout
或setInterval
來達到延遲執行的效果。5. 常見錯誤
- 忘記加入依賴陣列:如果你忘記了加入依賴陣列,可能會導致副作用無法正確執行或造成無窮迴圈。
- 在回傳函式中執行異步操作:回傳的函式應該是同步的,不應該包含異步操作。
這些是對
useEffect
的基本說明,希望這能幫到你更清楚地理解這個重要的 React hook。如果你有任何進一步的問題,歡迎隨時詢問!
基本用法瞭解了,這裡ChatGPT又提了一個新的用法:清理工作
在useEffect回傳一個箭頭函式
請ChatGPT深入解釋
當你在
useEffect
中返回一個函式時,這個函式通常被稱為清理函式(cleanup function)或清理機制(cleanup mechanism)。清理函式的主要目的是在下一次執行副作用之前,清理或取消副作用所產生的任何資源或效果。這對於需要釋放資源、取消訂閱、清除定時器等情況非常有用。
讓我們使用一個實際的範例來說明:
useEffect(() => { // 副作用操作 const timer = setInterval(() => { console.log('執行副作用'); }, 1000); // 返回清理函式 return () => { clearInterval(timer); console.log('清理副作用'); }; }, []);
在這個範例中,我們建立了一個定時器(
setInterval
),每隔一秒執行一次副作用操作(這裡是輸出一段文字到控制台)。然後,在useEffect
中返回了一個函式,這個函式包含了清理定時器的操作。當元件卸載或
useEffect
的依賴發生變化時,React 會在下次渲染之前執行這個清理函式。一些重要的事情要注意:
- 清理函式只在元件卸載或依賴發生變化時執行。 如果依賴陣列為空(
[]
),則清理函式只會在元件卸載時執行。- 清理函式可以執行任何需要的清理工作。 這可能包括取消訂閱、關閉連線、清除計時器、釋放資源等。
- 清理函式是可選的。 如果你的副作用操作沒有需要清理的部分,你可以省略返回清理函式。
- 如果依賴陣列為空,則清理函式只會在元件卸載時執行一次。 如果有依賴,則在每次依賴變化時都會執行一次清理函式,然後再執行新的副作用。
總的來說,清理函式是一個非常重要的機制,它讓你可以在副作用產生影響之前,進行必要的清理工作,這對於避免記憶體洩漏或其他意外行為非常重要。
OK,請ChatGPT幫我們整理一下,以上這些的執行順序
流程和全貌是怎麼樣的
我無法以圖表的方式呈現資訊,但我會以文字方式來講解
state
、useEffect
執行以及清理函式的執行順序。
- 初始化階段:
- 首先,
state
會被初始化,這是透過useState
hook 來進行的。這會設定元件的初始狀態值。- 元件渲染階段:
- 當元件被渲染時,
state
的初始值會被使用,並且useEffect
中的副作用操作會被執行。- 如果有清理函式,它也會被返回,但在這個階段不會執行。
- 清理函式的執行:
- 如果之前的渲染中有返回清理函式,它會在下一次渲染前執行。這可能發生在下列情況:
- 元件卸載時,如果
useEffect
的依賴為空,或是有新的依賴。useEffect
的依賴發生變化時,它會在執行新的副作用前被呼叫。- 依賴變化時的執行:
- 如果
useEffect
中的依賴發生變化,則清理函式會先執行(如果存在的話),然後新的副作用操作會被執行。總結來說,
state
初始化是第一步,接著useEffect
中的副作用操作會在每次渲染時執行。如果有清理函式,它會在下一次渲染前執行,並在新的副作用操作之前執行。在依賴變化時,清理函式會先執行(如果存在的話),然後新的副作用操作會被執行。
這個流程確保了在每次渲染之前,先清理之前的副作用,然後再執行新的副作用。
ChatGPT拒絕幫我畫流程圖XD
再盧他一次看看
抱歉,由於我無法直接生成圖表,但我可以以文字描述的方式,再次簡要地呈現流程:
graph TD; A[初始化 state] --> B[渲染元件] B -->|執行副作用| C[返回清理函式] C -->|卸載元件或依賴變化| D[執行清理函式] D -->|清理完成| E[執行新副作用] E -->B
- 初始化階段:初始化
state
,設定元件的初始狀態值。- 元件渲染階段:當元件被渲染時,
state
的初始值會被使用,並且useEffect
中的副作用操作會被執行。如果有清理函式,它也會被返回,但在這個階段不會執行。- 清理函式的執行:如果之前的渲染中有返回清理函式,它會在下一次渲染前執行。這可能發生在元件卸載時,或是
useEffect
的依賴發生變化時。- 依賴變化時的執行:如果
useEffect
中的依賴發生變化,清理函式會先執行(如果存在的話),然後新的副作用操作會被執行。總結來說,這個流程確保了在每次渲染之前,先清理之前的副作用,然後再執行新的副作用。
再來,為了瞭解useEffect的重要性
我們很叛逆的問看看ChatGPT,如果不使用useEffect的話會出什麼問題
當你不使用
useEffect
時,你可能會在 React 函式型元件中遇到一些問題,特別是當需要處理副作用的情況下。讓我們以一個簡單的範例來說明:
假設你想在元件渲染時設置一個定時器來更新狀態:
import React, { useState } from 'react'; function Timer() { const [seconds, setSeconds] = useState(0); setInterval(() => { setSeconds(seconds + 1); }, 1000); return ( <div> <p>已過秒數: {seconds}</p> </div> ); } export default Timer;
在這個例子中,我們使用
setInterval
來每秒更新一次seconds
的狀態。然而,這樣的做法會產生幾個問題:
- 無窮迴圈:
setInterval
會在每次渲染時執行,因此它會一直運行下去,導致無窮迴圈。- 效能問題:每次渲染都會重新設置一個新的定時器,這會造成效能上的浪費。
- 無法清理:沒有清理機制,這意味著即使元件被卸載,定時器仍然會持續運行,可能會導致記憶體洩漏或其他問題。
使用
useEffect
可以解決這些問題:import React, { useState, useEffect } from 'react'; function Timer() { const [seconds, setSeconds] = useState(0); useEffect(() => { const interval = setInterval(() => { setSeconds(prevSeconds => prevSeconds + 1); }, 1000); return () => clearInterval(interval); }, []); return ( <div> <p>已過秒數: {seconds}</p> </div> ); } export default Timer;
在這個例子中,我們使用
useEffect
來確保定時器只在元件第一次渲染時被設置,並在元件卸載時清理。這樣可以避免了無窮迴圈和記憶體洩漏的問題,同時也提升了效能。
把第一個程式碼範例貼到一個新檔案中,然後導入App.tsx
中
然後運行看看
前面1、2、3… 看起來都還正常
隨著執行的數量越來越多,無窮的迭代執行setInterval
出現的數字就開始發生異常與競爭,最後變成高速跑起來的無窮迴圈
然後改成有使用useEffect版本的
穩妥的La
我們再來了解更多使用useEffect的情境
當然,我將提供一些更多的範例以及解釋各種情境下如何使用
useEffect
。1. 資料獲取
import React, { useState, useEffect } from 'react'; function DataFetching() { const [data, setData] = useState(null); useEffect(() => { // 在這裡進行資料獲取的操作 fetch('<https://api.example.com/data>') .then(response => response.json()) .then(data => setData(data)); }, []); return ( <div> {data ? <p>資料: {data}</p> : <p>載入中...</p>} </div> ); } export default DataFetching;
這個範例展示了如何在元件渲染時使用
useEffect
來發起一個資料請求。在useEffect
中,我們使用fetch
函式來獲取資料,並在獲取成功後更新狀態。2. 設定訂閱
import React, { useState, useEffect } from 'react'; function Subscription() { const [subscribed, setSubscribed] = useState(false); useEffect(() => { // 設定訂閱 const subscription = subscribeToData(data => { // 在資料更新時執行的邏輯 }); // 在元件卸載時取消訂閱 return () => { unsubscribeFromData(subscription); }; }, []); return ( <div> <p>訂閱狀態: {subscribed ? '已訂閱' : '未訂閱'}</p> <button onClick={() => setSubscribed(!subscribed)}> 切換訂閱狀態 </button> </div> ); } export default Subscription;
在這個範例中,我們模擬了訂閱一個資料源的行為。當元件掛載時,我們設定了訂閱,並在元件卸載時取消訂閱,以避免資源洩漏。
3. 監聽事件
import React, { useState, useEffect } from 'react'; function EventListener() { const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); useEffect(() => { const handleMouseMove = (e) => { setMousePosition({ x: e.clientX, y: e.clientY }); }; window.addEventListener('mousemove', handleMouseMove); return () => { window.removeEventListener('mousemove', handleMouseMove); }; }, []); return ( <div> <p>目前滑鼠位置: {mousePosition.x}, {mousePosition.y}</p> </div> ); } export default EventListener;
這個範例示範了如何在元件渲染時監聽滑鼠移動事件,並在元件卸載時取消監聽。
這些範例展示了
useEffect
在不同情境下的使用方式,包括資料獲取、訂閱設定、事件監聽等。每個範例都包含了清理機制,以確保在元件卸載時進行必要的清理工作。
看完以上範例,這邊要特別講一下
雖然ChatGPT說useEffect可以避免無窮迴圈
但useEffect是只要依賴的變數更動,就會啟動,所以…?
是的,雖然
useEffect
是 React 提供的一個強大工具,但在使用時仍然可能導致無窮迴圈的情況。這種情況通常發生在你在
useEffect
中更新某個狀態,而這個狀態的變化會觸發useEffect
重新執行的情況下。讓我們以一個範例來說明:
import React, { useState, useEffect } from 'react'; function InfiniteLoopExample() { const [count, setCount] = useState(0); useEffect(() => { setCount(count + 1); }, []); return ( <div> <p>目前計數: {count}</p> </div> ); } export default InfiniteLoopExample;
在這個範例中,我們在
useEffect
中嘗試更新count
的狀態。然而,每次setCount
被呼叫時,它會觸發元件重新渲染,進而觸發useEffect
重新執行,這將導致無窮迴圈。為了避免這種情況,你需要確保在
useEffect
中更新狀態時,不會引起無窮迴圈。這可能需要你重新考慮你的邏輯或使用依賴陣列來限制useEffect
的執行。總結來說,雖然
useEffect
是一個非常強大的工具,但它也需要小心使用,以避免出現無窮迴圈或其他意外行為。
上面這個實在是太危險了,我們請ChatGPT繪製流程圖就好,不要真的運行
當然,以下是使用 Mermaid 語法描述的情境:
graph TD; A[初始化 state] --> B[執行第一次 useEffect] B -->|更新 count| C[重新渲染] C -->B C -->|count 變化| B
這個流程示意了在初始化時,
useEffect
會被執行一次。然後,每次count
更新會觸發重新渲染,並再次執行useEffect
。這樣就形成了一個無窮迴圈。為了避免這種情況,你需要確保在
useEffect
中的操作不會引起無窮迴圈,或者使用依賴陣列來限制useEffect
的執行。
最後的最後,請ChatGPT幫我們統整今日重點
在這次的對話中,我們探討了React中的
useEffect
以及相關概念。以下是本次對話的重點:
useEffect
是什麼:
useEffect
是React提供的一個hook,用於處理副作用操作,如資料獲取、設定訂閱、監聽事件等。useEffect
的基本語法:
useEffect(() => { // 副作用操作 }, [依賴1, 依賴2, ...]);
- 第一個參數是一個函式,包含了副作用操作。
- 第二個參數是一個陣列,包含了影響副作用的變數或值。
- 清理函式:
- 可以透過返回一個函式來執行清理工作,以避免資源洩漏或其他問題。
useEffect
的執行流程:
- 初始化階段 -> 元件渲染階段 -> 清理函式的執行(若有)-> 依賴變化時的執行。
useEffect
的使用情境:
- 資料獲取、設定訂閱、監聽事件等。
- 可能的無窮迴圈問題:
- 在
useEffect
中更新某個狀態,且這個狀態的變化會觸發useEffect
重新執行,可能導致無窮迴圈。結語:
useEffect
是React函式型元件中一個重要的工具,它讓我們可以在函式型元件中處理副作用。然而,使用時需要注意避免可能的無窮迴圈,並確保在需要時提供清理函式。透過正確的使用useEffect
,我們可以有效地管理元件的生命週期及處理各種副作用。
這次的對話記錄在這裡:
https://chat.openai.com/share/cee5659c-952c-4764-9a1c-c1f85b64bc9b
我原本估計章節的時候,實在太小看這前後端一條龍的資訊量了
還有前面的前導也佔不少篇章
導致我從「糟糕好像只有23篇」,到現在的「預估可能超過35篇才能講完」
很多鐵人賽文章都要花30篇才能深入講解React,我卻妄想用一兩篇就能講完
總之,React這裡我會放慢一點
一來是React本來就是我想講的主題
二來是…嗯真的資訊量蠻多的
如果可以,我預計這個系列會一直寫到我想寫的都寫完
(聽說鐵人賽系列文章可以一直寫到農曆過年前)
也謝謝各位跟著我一起一條龍的閱讀到這裡
hook還可以講很多東西,下一篇要來講更多的hook!
我是調整節奏中的前端工程師一宵三筵,我們明天見!