目前這個系列已挑選ChatGPT、前端&React相關章節出版成書!
感謝大家的閱讀與支持!
《用 ChatGPT 詠唱來點亮 React & 前端技能樹》
歷經重新整理、校正、編排、與修改後,讓章節內容之間的銜接更平順,除了補充上更多的解說之外,甚至有部分章節為重新撰寫的內容,讓理解變得更輕鬆!
目前已在天瓏書局、博客來、金石堂等通路都買得到了! 也有電子書的選擇!
歡迎各位點擊連結選購,謝謝大家~
和hook有關主題寫了三篇,其實不在我預料之內
也可以說四篇啦,useState也是一個hook
OK總之,React提供了各種的hook
工具的出現是為了解決問題的,不用因為很多類型學不完而太焦慮
微微回顧上一篇,ChatGPT對於useRef的提及如下
useRef
useRef
可以用來獲得 DOM 元素的引用,也可以用來儲存任意可變值。
我們先來了解一下,React平時在開發的時候,和DOM的關係是什麼
為什麼要特別獲得DOM元素呢?
當然可以!首先,讓我來解釋一下 DOM 的概念。
DOM(Document Object Model)翻譯成中文就是「文件物件模型」,它是一種表示網頁文件結構的方式。在網頁上,每一個 HTML 元素(比如說標題、段落、按鈕等等)都被看作是一個物件,透過 DOM,我們可以透過程式碼來操控這些物件,改變它們的內容、樣式、屬性等等。
當你使用 React 開發時,React 會幫助你管理 DOM。React 使用虛擬 DOM(Virtual DOM)來追蹤和記錄 DOM 的變化,這樣可以極大地提升應用程式的效能。
比方說,當你在 React 中修改一個元素的狀態(state),React 會計算出新的虛擬 DOM,然後對比新舊虛擬 DOM 的差異,僅僅更新有變化的部分,而不是整個網頁重新渲染。這個過程讓 React 非常高效。
舉個例子:
假設你在 React 中有一個按鈕元素:
import React, { useState } from 'react'; function MyComponent() { const [count, setCount] = useState(0); const handleClick = () => { setCount(count + 1); }; return ( <div> <button onClick={handleClick}>點我增加數字</button> <p>目前數字:{count}</p> </div> ); } export default MyComponent;
當你點擊這個按鈕時,
handleClick
函式會被呼叫,這會觸發 React 更新相應的虛擬 DOM,然後只更新變化的部分,也就是計數器的數字,而不會重新渲染整個網頁。這樣的操作方式使得 React 在處理大型、複雜的應用程式時非常高效,因為它只會處理必要的部分,而不是整個頁面。
自從開始使用React之後,和原生JS或jQuery相比,幾乎是沒有在操縱DOM元素物件了
React建立和操作的是「虛擬DOM」
在jQuery或是原生JS中,要管理顯示的UI,動不動就就要取得DOM
請ChatGPT來解說和示範
好的,現在來談談使用原生 JavaScript 或 jQuery 時,以及它們和 DOM 的操作關係。
- 原生 JavaScript:
使用原生 JavaScript 時,你會直接透過 JavaScript 來選取、操作 DOM 元素。例如,在上述的例子中,如果你想要實現一個計數器,你會這樣做:
document.addEventListener('DOMContentLoaded', function() { var count = 0; var button = document.getElementById('myButton'); var counter = document.getElementById('myCounter'); button.addEventListener('click', function() { count++; counter.innerText = '目前數字:' + count; }); });
這段程式碼使用
document.getElementById
來獲取按鈕和計數器元素,然後透過事件監聽器來設置按鈕點擊事件的處理函式。當按鈕被點擊時,計數器的內容將被更新。
- jQuery:
如果你使用 jQuery,則可以更為簡潔地進行相同的操作:
$(document).ready(function() { var count = 0; $('#myButton').click(function() { count++; $('#myCounter').text('目前數字:' + count); }); });
這段程式碼使用 jQuery 提供的簡潔的選擇器語法來選取元素,並使用
.click
來設置按鈕點擊事件的處理函式。當按鈕被點擊時,計數器的內容將被更新。總的來說,使用原生 JavaScript 或 jQuery 時,你需要手動選擇 DOM 元素,然後透過 JavaScript 來操作它們的屬性、內容等。相對於 React,這樣的操作方式可能會更為繁瑣,因為你需要管理 DOM 的變化,並確保更新的正確性。而 React 通過虛擬 DOM 簡化了這個過程,使得開發者可以更專注於應用程式的邏輯和組件的開發。
先取得DOM元素,然後針對元素內部的數值、方法去做存取或更動
但在react中是:
const [count, setCount] = useState(0);
return (
<div>
<button onClick={()=> setCount(count + 1)}>點我增加數字</button>
<p>目前數字:{count}</p>
</div>
);
JSX指定好之後,只要管理好狀態變化,就可以隨時對渲染出的元件做出對應的改變
但有沒有什麼時候,是「必須」取得DOM再來直接對DOM做操作的呢
在 React 的開發中,通常盡量避免直接操作或更新 DOM,而是透過 React 的狀態(state)和屬性(props)來管理元素的狀態和外觀。這是因為 React 使用虛擬 DOM 來追蹤 DOM 變化,並根據變化來做出更新,這樣可以提升應用程式的效能。
然而,有時候還是會遇到需要直接操作或更新 DOM 的情況,例如:
- 整合第三方函式庫: 有些第三方函式庫可能需要直接操作 DOM,例如地圖、視頻播放器等等。在這種情況下,你可能需要在 React 應用程式中使用
ref
來引用特定的 DOM 元素,以便進行操作。- 特殊效果或動畫: 有時候,一些特殊的效果或動畫可能需要直接修改 DOM 的樣式。你可以透過 React 的
ref
或者使用 React 庫中的動畫函式庫來實現這些效果。- 與非受控元素互動: 在某些情況下,你可能需要與非受控元素(uncontrolled elements)互動,這些元素不受 React 狀態的控制。這時,你可能需要使用原生 JavaScript 或者 jQuery 來處理這些元素。
儘管如此,在直接操作或更新 DOM 的情況下,也要謹慎使用,盡可能避免影響 React 的狀態管理,以免引起不一致性或效能問題。如果有需要操作 DOM 的情況,建議在生命週期函式(如
componentDidMount
、componentDidUpdate
)中進行,並注意適當的清理和銷毀操作。
所以,少數情況下,還是有「必須」直接操作DOM的時候
除了這些情況之外,React都不鼓勵這麼做
我們請ChatGPT給點例子好了
當然,這裡提供一個特殊例子,示範在 React 中必須直接操作 DOM 的情況。
假設你需要在一個表單中,當用戶點擊提交按鈕時,自動將焦點設置到下一個輸入欄位,這種情況下可能需要直接操作 DOM。以下是一個範例程式碼:
import React, { useRef } from 'react'; function MyComponent() { const inputRef = useRef(null); const handleSubmit = (e) => { e.preventDefault(); // 直接操作 DOM,將焦點設置到下一個輸入欄位 inputRef.current.nextSibling.focus(); }; return ( <form> <input type="text" ref={inputRef} /> <input type="text" /> <button onClick={handleSubmit}>提交</button> </form> ); } export default MyComponent;
在這個範例中,我們使用了
useRef
來建立一個對第一個輸入欄位的參考(ref)。當用戶點擊提交按鈕時,handleSubmit
函式會被呼叫,並透過inputRef.current.nextSibling
來獲取下一個輸入欄位,然後將焦點設置到該輸入欄位。這是一個需要直接操作 DOM 的特殊情況,因為 React 並不提供一個直接的方式來設置下一個元素的焦點。在這種情況下,我們可以使用
useRef
來建立一個參考,然後透過current
屬性來獲取 DOM 元素,進行需要的操作。
出現ㄌ !useRef!
像上面ChatGPT給的範例,要指定「focus」這個動作的時候
在原生/ jQuery中,取得dom之後就用 .foucs()
來指定
<input id="nameInput" value="">
<script>
$("#nameInput").foucs() // jQuery
document.getElementById("nameInput".foucs() // 原生寫法
<script>
這時就會突然發現,欸?react沒辦法透過attr等方式,去指定input現在是否foucs
確實就需要「先獲取DOM元素、再直接對DOM進行操作」
接著我們請ChatGPT提供有關更多useRef的教學
)
當然!
useRef
是 React 提供的一個 hooks,用於在函式型組件中創建一個可變的物件參考。它主要用於存儲和訪問 DOM 元素或其他 React 元素。以下是關於
useRef
的深入介紹:1. 創建一個
useRef
:import React, { useRef } from 'react'; function MyComponent() { const myRef = useRef(null); // myRef.current 將會是一個物件,可以用於存儲任何你想要的值 // 初始值為 null return <div ref={myRef}>這是一個 DOM 元素</div>; } export default MyComponent;
在這個範例中,我們使用
useRef
創建了一個myRef
。它是一個可變的物件,初始值為null
。2. 連接到 DOM 元素:
function MyComponent() { const myRef = useRef(null); useEffect(() => { // myRef.current 會指向這個 div 元素 myRef.current.style.backgroundColor = 'lightblue'; }, []); // 空依賴陣列表示只在組件掛載時執行一次 return <div ref={myRef}>這是一個 DOM 元素</div>; }
在這個範例中,我們使用了
useEffect
來在組件掛載後執行一段程式碼。在這個程式碼中,我們可以透過myRef.current
來訪問這個 DOM 元素,並對它進行操作。3. 存儲可變值:
function MyComponent() { const countRef = useRef(0); useEffect(() => { countRef.current = countRef.current + 1; console.log(`countRef 的值為:${countRef.current}`); }, []); return <div>這是一個 DOM 元素</div>; }
在這個範例中,我們將一個計數器存儲在
countRef
中,並在每次組件掛載後增加它的值。我們可以透過countRef.current
來訪問和修改這個計數器。4. 保留對 React 元素的引用:
function MyComponent() { const myRef = useRef(null); const handleClick = () => { myRef.current.focus(); }; return ( <div> <input ref={myRef} type="text" /> <button onClick={handleClick}>將焦點設置到輸入欄位</button> </div> ); }
在這個範例中,我們將一個
input
元素的參考存儲在myRef
中。當按鈕被點擊時,我們可以透過myRef.current
來訪問這個input
元素,並將焦點設置到它上面。總的來說,
useRef
提供了一個方便的方式來在函式型組件中存儲和訪問 DOM 元素或其他 React 元素,並且也可以用於存儲任何可變的值。
當然,使用
useRef
時有一些需要注意的地方:
初始化值: 如果你希望
useRef
在創建時具有一個初始值,你可以像這樣使用useRef(initialValue)
。這個初始值會在組件的整個生命週期中保持不變。const myRef = useRef(initialValue);
當組件重新渲染時,
useRef
不會改變:useRef
的返回值(也就是它的.current
屬性)在組件重新渲染時保持不變。這使得它成為存儲可以保留在渲染之間的值的良好選擇。避免直接操作 DOM: 一般來說,盡量避免直接操作 DOM。React 通常會自動管理 DOM,直接操作 DOM 可能會破壞 React 的預期行為。
不要將
useRef
與useState
混淆:useRef
是用來保存可變的引用的,而useState
是用來保存組件的狀態的。不要在渲染過程中依賴
useRef
的值: 由於useRef
的值在渲染過程中不會改變,因此不要在渲染過程中依賴它的值進行邏輯判斷。function MyComponent() { const myRef = useRef(0); // 這裡的 myRef.current 將永遠是 0 if (myRef.current === 0) { // ... } return <div>這是一個 DOM 元素</div>; }
總的來說,
useRef
是一個非常有用的 hooks,但需要小心使用它,確保遵守 React 的最佳實踐,並避免直接操作 DOM。
當你需要使用
useRef
的情境並不限於直接操作 DOM。以下是一些使用useRef
的其他情境和相應的範例:
保存上一次渲染的值: 如果你想要比較前一次和當前渲染的值,可以使用
useRef
來保存上一次的值,這在 useEffect 中非常有用。import React, { useEffect, useRef } from 'react'; function MyComponent() { const prevValueRef = useRef(null); const value = 42; useEffect(() => { prevValueRef.current = value; console.log(`上一次的值是:${prevValueRef.current}`); }); return <div>當前值是:{value}</div>; }
觸發非受控元素的操作: 在某些情況下,你可能需要觸發非受控元素(如原生 JavaScript 或第三方庫中的元素)的操作。使用
useRef
來保存對這些元素的引用可以方便地執行這些操作。import React, { useRef } from 'react'; function MyComponent() { const inputRef = useRef(null); const handleButtonClick = () => { // 觸發 input 元素的選取操作 inputRef.current.select(); }; return ( <div> <input ref={inputRef} type="text" defaultValue="文本內容" /> <button onClick={handleButtonClick}>選取文本</button> </div> ); }
儲存計時器的 ID: 如果你需要管理計時器,例如在組件卸載時清除計時器,可以使用
useRef
來儲存計時器的 ID。import React, { useEffect, useRef } from 'react'; function TimerComponent() { const timerIdRef = useRef(null); useEffect(() => { // 啟動計時器 timerIdRef.current = setInterval(() => { console.log('計時中...'); }, 1000); // 在組件卸載時清除計時器 return () => { clearInterval(timerIdRef.current); }; }, []); return <div>計時器組件</div>; }
這些情境示例展示了
useRef
在 React 中的多種應用方式,不僅僅限於操作 DOM。它可以用於管理狀態,執行非受控元素的操作,保存之前的值等等。請根據你的具體需求來使用它。
看了這麼多useRef的例子,最後還是要探究根源一下
這個ref指的到底是什麼
在 React 中,
ref
是一種特殊的屬性,它用來建立對 DOM 元素或 React 元素的引用。ref
的名稱來源於 "reference"(引用),它允許你在 React 組件中訪問、操作或管理特定元素的方式。主要的用途包括:
- 訪問 DOM 元素: 你可以使用
ref
來建立對特定 DOM 元素的引用,然後在需要的時候訪問和操作這些元素。這在需要直接操作 DOM 時非常有用。- 訪問 React 元素: 你也可以使用
ref
來建立對其他 React 組件的引用,然後透過這些引用來調用該組件的方法或訪問其狀態。- 儲存可變值:
ref
也可以用來儲存可變的值,並在組件的多次渲染之間保持不變。這對於記錄之前的值或進行一些特殊的邏輯非常有用。簡而言之,
ref
允許你在 React 中建立對元素的引用,以便在需要時可以更方便地訪問和操作這些元素,或者執行一些特殊的操作。請注意,直接操控 DOM 的情況下應謹慎使用ref
,盡量遵循 React 的組件化和狀態管理模式。
開始已知用火,學會在必要的時候使用Ref來操控DOM之後…
假設今天我們自己做了了一個和input相關的Component
比如說長這樣:一個紅色邊框的input (先不管value或是onChange)
function MyInput(){
return (
<input style={{border: '1px red solid'}}/>
)
}
然後外層用了一個Component引入他,並把Ref給這個MyInput
function MyComponent(){
const myRef = useRef(null)
function handleClick(){
myRef.current.focus()
}
return(
<div>
<button onClick={handleClick}>點我focus</button>
<MyInput ref={myRef}/>
</div>
)
}
興高采烈地準備跑起來,卻發現完全不能動,還噴了錯誤訊息出來
來試試沒有什麼特別技巧的,單純把整個問題描述丟給ChatGPT
在 React 中,要將
ref
傳遞到自定義組件中,你需要使用React.forwardRef
來將ref
正確地傳遞到內部的input
元素中。以下是你的程式碼經過修正:
import React, { useRef, forwardRef, useImperativeHandle } from 'react'; const MyInput = forwardRef((props, ref) => { const inputRef = useRef(null); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); } })); return ( <input style={{border: '1px red solid'}} ref={inputRef} /> ) }); function MyComponent() { const myRef = useRef(null); function handleClick() { myRef.current.focus(); } return ( <div> <button onClick={handleClick}>點我focus</button> <MyInput ref={myRef} /> </div> ) } export default MyComponent;
這裡做了幾個變更:
- 使用
React.forwardRef
包裹了MyInput
組件,這樣可以接收ref
作為第二個參數。- 在
MyInput
組件中,建立了一個inputRef
來引用input
元素。- 使用
useImperativeHandle
定義了ref
對外暴露的方法focus
,當呼叫myRef.current.focus()
時,會觸發inputRef.current.focus()
。- 將
inputRef
傳遞給input
元素的ref
屬性。現在,你可以在
MyComponent
中正確地使用myRef.current.focus()
來聚焦到input
元素了。
突然跑出了新的語法!React.forwardRef
和 useImperativeHandle
逐一來了解這兩個語法的用法與介紹
當然,現在讓我來為你提供
React.forwardRef
的相關教學和介紹:使用
React.forwardRef
:
React.forwardRef
是 React 提供的一個方法,用於傳遞ref
到子組件。它允許子組件接收ref
作為第二個參數(props),然後將這個ref
傳遞給內部的 DOM 元素或其他 React 組件。以下是一個簡單的示例:
import React, { forwardRef } from 'react'; const MyComponent = forwardRef((props, ref) => { return <input ref={ref} />; }); export default MyComponent;
如何使用
React.forwardRef
:
在子組件中使用
forwardRef
:首先,在子組件中使用
forwardRef
來包裹組件定義,並在函式的參數中接收props
和ref
。const MyComponent = forwardRef((props, ref) => { return <input ref={ref} />; });
將
ref
傳遞到內部的 DOM 元素或其他 React 組件:在子組件的 JSX 中,將
ref
傳遞給需要引用的 DOM 元素或其他 React 組件。return <input ref={ref} />;
在父組件中使用
ref
:在父組件中可以正常地將
ref
傳遞給子組件。const myRef = useRef(null); return <MyComponent ref={myRef} />;
注意事項:
forwardRef
接收一個函式,這個函式接收兩個參數:props
和ref
。在這個函式中,你可以返回 JSX 或 React 元素。forwardRef
是一個高階組件(Higher-Order Component,HOC)工具,它使得傳遞ref
到子組件變得容易。- 使用
forwardRef
時,你需要記得將接收到的ref
傳遞到子組件的 DOM 元素或其他 React 組件上。總的來說,
React.forwardRef
提供了一個簡潔的方式來在 React 中傳遞ref
,使得你可以更容易地在父組件中操作子組件中的 DOM 元素或 React 組件。
嗯嗯,也就是說可以把外部接收的ref給正確的傳遞到Component內
但ChatGPT介紹React.forwardRef
的回答中
並沒有使用到他先前用到的useImperativeHandle
於是繼續追問
useImperativeHandle
是 React 提供的另一個 hooks,它用於自定義ref
對外暴露的接口。這樣可以控制父組件中的ref.current
被訪問時可以看到哪些屬性或方法。在你的情境中,當你想要讓父組件可以透過
ref
來直接調用子組件的特定方法(例如focus()
)時,可以使用useImperativeHandle
。以下是對你之前提供的程式碼進一步解釋:
import React, { useRef, forwardRef, useImperativeHandle } from 'react'; const MyInput = forwardRef((props, ref) => { const inputRef = useRef(null); // 使用 useImperativeHandle 定義對外暴露的方法 focus useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); } })); return ( <input style={{border: '1px red solid'}} ref={inputRef} /> ) });
這裡的
useImperativeHandle
的作用是將一個對外的方法focus
添加到了ref
對象上,當父組件使用myRef.current.focus()
時,實際上會調用子組件中inputRef.current.focus()
。要使用
useImperativeHandle
,你需要注意以下幾點:
useImperativeHandle
需要在forwardRef
中使用,因為它需要接收第二個參數ref
。- 在
useImperativeHandle
內部,你可以定義一個對外暴露的方法(例如focus()
),這樣父組件就可以透過ref.current
調用這個方法。總的來說,
useImperativeHandle
可以用來精確地控制父組件可以訪問子組件中的哪些方法或屬性,提高了程式碼的可讀性和可維護性。
理解一下,所以useImperativeHandle
指定了外部只能用dom的哪些方法
和ChatGPT確認一下我的理解是否正確
正確,當你使用了
useImperativeHandle
來定義ref
對外暴露的方法後,外部的ref
就只能操控子組件中定義的那些方法,如上例中的focus
。如果你希望在
ref
對象上添加更多的方法或屬性,你可以在useImperativeHandle
的回調函式中返回更多的屬性或方法。以下是一個範例,假設你想要在
MyInput
中添加一個名為getValue
的方法,讓父組件可以獲取input
元素的值:const MyInput = forwardRef((props, ref) => { const inputRef = useRef(null); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); }, getValue: () => { return inputRef.current.value; } })); return ( <input style={{border: '1px red solid'}} ref={inputRef} /> ) });
現在,在父組件中,你可以使用
myRef.current.getValue()
來獲取input
元素的值。總結來說,你可以在
useImperativeHandle
的回調函式中返回任何你希望在ref
對象上公開的屬性或方法,這樣父組件就可以根據需要來訪問子組件的相關功能。
我是叛逆的壞小孩,我想知道useImperativeHandle
是否可以自行選擇要不要使用
是的,當你使用
React.forwardRef
沒有使用useImperativeHandle
,父元件可以直接調用子元件的 DOM 操作或者存取其數值。這是因為forwardRef
預設會將ref
傳遞給子元件。以下是一個範例程式碼:
import React, { forwardRef } from 'react'; const MyInput = forwardRef((props, ref) => { return ( <input style={{border: '1px red solid'}} ref={ref} /> ) }); function MyComponent() { const myRef = useRef(null); function handleClick() { myRef.current.focus(); // 可以直接調用 input 元素的 focus 方法 console.log(myRef.current.value); // 可以直接獲取 input 元素的值 } return ( <div> <button onClick={handleClick}>點我 focus</button> <MyInput ref={myRef} /> </div> ) } export default MyComponent;
在這個範例中,我們沒有使用
useImperativeHandle
,但是父元件MyComponent
仍然可以直接調用子元件MyInput
中input
元素的focus
方法以及存取其值。這是因為forwardRef
預設會將ref
傳遞給子元件。請注意,這種情況下父元件可以直接訪問子元件的所有 DOM 操作和數值,但這樣的寫法可能會使程式碼耦合度增加,請謹慎使用,保持良好的程式設計實踐。
OK! 所以不使用也是可以的,只是有可能會暴露過多
又回到使用原生JS或jQuery時毫無規範的窘境,變得混亂不堪
內部完全不知道外面會對這個元件進行什麼操作或更變,要追起來十分困難
在這次的對話中,我們探討了以下重要概念:
useRef
的使用:useRef
是 React 提供的 hooks,用於創建可變的物件參考,主要用途包括訪問 DOM 元素、訪問 React 元素、儲存可變值等。它可以幫助你在函式型組件中管理狀態和 DOM 操作。React.forwardRef
的使用:React.forwardRef
是用來傳遞ref
到子組件的方法,它允許你在子組件中接收ref
作為第二個參數,然後將這個ref
傳遞給內部的 DOM 元素或其他 React 組件。這在自定義組件中使用ref
時非常有用。useImperativeHandle
的使用:useImperativeHandle
是 React 提供的 hooks,它用於自定義ref
對外暴露的接口,使得父組件可以根據需要訪問子組件中的特定方法或屬性。這對於控制ref
的外部介面非常有用。總結來說,React 提供了一個強大的機制,讓你可以更容易地在組件之間傳遞
ref
,訪問和操作 DOM 元素,以及自定義ref
的對外介面。然而,使用這些功能時需要謹慎,遵循最佳實踐,以確保你的程式碼保持可讀性和可維護性。最後,請記住,React 是一個強大且靈活的庫,它提供了多種方式來處理不同的場景,請根據你的具體需求選擇適合的方法。希望這次的討論對你有所幫助,祝你在 React 開發中取得成功!
Hook的介紹大概就到這邊告一個段落
本次和ChatGPT的對話紀錄:
https://chat.openai.com/share/8083b95b-eba7-4234-aa83-e246b727f20c
下個章節要講 React & TypeScript了!
為了簡化介紹,後來我請ChatGPT先不要給TypeScript版本的
不然語法上看起來好像很複雜
如果是跟著我從Day15建置環境開始,就使用typescript
建立檔案都是使用.tsx
而不是.jsx
的話
可能這個章節會微微..微..地發現上面的範例沒辦法直接跑
會有一些type上的限制與警告,像是說myRef.current
可能是null
或undefined
之類的
想要先讓他跑起來,就是各種補any還有?.
例如:myRef.current?.focus()
、 const myRef = useRef<any>(null)
等等
詳細會到下個章節一起介紹!
我是期待回到TypeScript懷抱的前端工程師一宵三筵,我們明天見!