基礎與語法
TypeScript、React Hooks 與 JavaScript 核心觀念整理,聚焦面試高頻考點與實際使用情境。
React Hooks
useState — 不可變更新
直接 mutate state 物件不會觸發重新渲染。更新物件或陣列時,必須回傳新的參考(spread / filter / map)。 函數式更新(prev => ...)可避免 stale closure 問題。
// ❌ 錯誤:直接 mutate,React 偵測不到 reference 改變
state.items.push(newItem)
setState(state)
// ✅ 正確:回傳新陣列
setState(prev => ({ ...prev, items: [...prev.items, newItem] }))
// ✅ 函數式更新:解決快速連點時的 stale state
setCount(prev => prev + 1)useEffect — 依賴陣列與 Cleanup
依賴陣列決定 effect 的觸發時機。遺漏依賴會產生 stale closure;不必要的依賴會造成無限迴圈。 回傳的 cleanup function 會在 下次 effect 執行前及元件卸載時執行。
useEffect(() => {
// 訂閱 WebSocket
const ws = new WebSocket(url)
ws.onmessage = (e) => setMessages(prev => [...prev, e.data])
// Cleanup:元件卸載或 url 改變時,關閉舊連線
return () => ws.close()
}, [url]) // url 改變 → 先 cleanup 舊的,再建立新的
// 常見陷阱:effect 內用到的所有外部變數都必須列入依賴
// ESLint plugin exhaustive-deps 可自動偵測useMemo vs useCallback
兩者都是快取機制,差別在於快取的東西:useMemo 快取計算結果(値),useCallback 快取函數定義。 與 React.memo 搽配使用,才能真正阻止子元件不必要的重渲染。
// useMemo:大量計算結果快取,避免每次 render 重算
const sortedList = useMemo(
() => items.sort((a, b) => b.score - a.score),
[items]
)
// useCallback:穩定的函數參考,搭配 React.memo 使用
const handleDelete = useCallback((id: string) => {
setItems(prev => prev.filter(item => item.id !== id))
}, []) // 無外部依賴 → 永遠是同一個函數參考
// React.memo 只在 props 改變時才重渲染
const ItemRow = React.memo(({ item, onDelete }) => (
<li onClick={() => onDelete(item.id)}>{item.name}</li>
))useRef — DOM 存取 vs 可變容器
useRef 有兩個用途:取得 DOM 節點(如 focus、動畫);以及儲存不影響渲染的可變値(如 timer id、前一次的値)。 修改 .current 不會觸發重渲染。
// 用途 1:DOM 操作
const inputRef = useRef<HTMLInputElement>(null)
const focusInput = () => inputRef.current?.focus()
// 用途 2:儲存 timer id(修改不觸發 re-render)
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const startTimer = () => {
timerRef.current = setTimeout(() => console.log('done'), 1000)
}
const cancelTimer = () => {
if (timerRef.current) clearTimeout(timerRef.current)
}Custom Hook — 邏輯抽取與複用
Custom Hook 以 use 開頭,可在其中使用任意 Hook。 適合將「狀態 + 副作用」打包成可測試、可複用的邏輯單元,元件只負責 UI 邏輯。
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
const controller = new AbortController()
setLoading(true)
fetch(url, { signal: controller.signal })
.then(r => r.json())
.then(setData)
.catch(e => { if (e.name !== 'AbortError') setError(e) })
.finally(() => setLoading(false))
// Cleanup:path 改變時取消前一個請求(避免 race condition)
return () => controller.abort()
}, [url])
return { data, loading, error }
}
// 使用:元件完全不感知 fetch 細節
const { data: user, loading } = useFetch<User>('/api/user/1')TypeScript
type vs interface
兩者大多數情況可互換。主要差異:interface 支援 declaration merging(同名自動合併);type 可表達 Union、Intersection、Mapped types 等更複雜的組合。 物件形狀優先用 interface,其餘用 type。
// interface:可被 extend、可 declaration merging
interface User { id: string; name: string }
interface User { age: number } // ✅ 自動合併為 { id, name, age }
// type:可組合 Union 與 Intersection
type Status = 'idle' | 'loading' | 'success' | 'error'
type AdminUser = User & { permissions: string[] }
type Nullable<T> = T | nullDiscriminated Union — 型別縮窄
以共同的「判別屬性」(discriminant)讓 TypeScript 自動縮窄型別,取代 as 強制轉型。 搽配 switch 竮舉時,加入 never 可確保所有分支都被處理。
type ApiState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string }
function render<T>(state: ApiState<T>) {
switch (state.status) {
case 'loading': return <Spinner />
case 'success': return <View data={state.data} /> // TS 知道 data 存在
case 'error': return <Alert msg={state.error} />
case 'idle': return null
default:
// never:若未來新增 status 但忘記處理,編譯時期報錯
const _exhaustive: never = state
return _exhaustive
}
}Generics + Constraints
泛型搽配 extends 約束,讓函數在保持彈性的同時仍有型別安全。keyof T 取得物件所有 key 的聯合型別,是存取屬性時常用的技巧。
// keyof 約束:確保 key 是 T 的合法屬性
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { id: 1, name: 'Wilson', role: 'admin' }
getProperty(user, 'name') // ✅ string
getProperty(user, 'email') // ❌ 編譯錯誤
// 實際應用:型別安全的 API response wrapper
interface ApiResponse<T> {
data: T
status: number
message: string
}
async function fetchUser(): Promise<ApiResponse<User>> { ... }Utility Types — 實際使用情境
TypeScript 內建的型別工具,用於衍生新型別,避免重複定義。
interface User {
id: string
name: string
email: string
password: string
createdAt: Date
}
// Partial:更新 API 的 request body(所有欄位可選)
type UpdateUserDto = Partial<Omit<User, 'id' | 'createdAt'>>
// Pick:只暴露給前端的安全欄位
type PublicUser = Pick<User, 'id' | 'name'>
// Record:建立 id → user 的查詢 map
type UserMap = Record<string, User>
// ReturnType / Parameters:從既有函數自動推導型別
type FetchResult = ReturnType<typeof useFetch>
type RouterParams = Parameters<typeof router.push>[0]→ 泛型與 Interface 完整筆記JavaScript 核心
Closure(閉包)
函數記住並持有其定義時所處作用域的變數,即使外部函數已執行完畢。 React 的 stale closure 問題本質上就是 effect 捕捉了過時的 state/props 參考。
// 典型應用:counter factory
function makeCounter(initial = 0) {
let count = initial // 被閉包捕捉的私有變數
return {
increment: () => ++count,
decrement: () => --count,
value: () => count,
}
}
const c = makeCounter(10)
c.increment() // 11 — 外部無法直接存取 count,只能透過回傳的函數
// React stale closure 範例
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
// ❌ 閉包捕捉了初始的 count = 0,每秒都 log 0
const id = setInterval(() => console.log(count), 1000)
return () => clearInterval(id)
}, []) // 空依賴 → count 永遠是 0
// ✅ 解法:加入 count 到依賴,或改用 functional update
}Event Loop — 執行順序
JS 是單執行緒,透過 Event Loop 處理非同步。Microtask queue(Promise.then、queueMicrotask)的優先權高於 Macrotask queue(setTimeout、setInterval)。 每個 macrotask 執行完後,會先清空所有 microtask 再取下一個 macrotask。
console.log('1') // 同步
setTimeout(() => console.log('2'), 0) // macrotask
Promise.resolve()
.then(() => console.log('3')) // microtask
.then(() => console.log('4')) // microtask
console.log('5') // 同步
// 輸出順序:1 → 5 → 3 → 4 → 2
// 同步執行完 → 清空 microtask queue → 取 macrotaskDebounce vs Throttle
Debounce:停止觸發後等待 N ms 才執行,適合搜尋框(等用戶打完字再送 API)。Throttle:每 N ms 最多執行一次,適合 scroll / resize(持續觸發但需要限制頻率)。
function debounce<T extends (...args: unknown[]) => void>(fn: T, delay: number) {
let timer: ReturnType<typeof setTimeout>
return (...args: Parameters<T>) => {
clearTimeout(timer) // 每次觸發就重置計時
timer = setTimeout(() => fn(...args), delay)
}
}
// Custom Hook 版本(搭配 useEffect cleanup)
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay)
return () => clearTimeout(timer) // value 改變 → 取消上一個 timer
}, [value, delay])
return debounced
}
// 搜尋框:只有用戶停止輸入 300ms 後才發送請求
const debouncedQuery = useDebounce(searchInput, 300)
useEffect(() => { fetchResults(debouncedQuery) }, [debouncedQuery])