useRef 和 useTransition 兩個 Hook 解決了常見的性能和狀態管理問題。useRef 用來保存一個可變的引用,改變 ref 的值不會觸發重新渲染。useTransition 用於標記一個非同步狀態轉換,當你有一個需要等待的操作(比如搜尋、排序),可以用 useTransition 讓 UI 保持回應不會卡頓。
特性 | useRef | useTransition |
|---|
用途 | 保存可變引用 | 標記非同步轉換 |
觸發重新渲染 | 否 | 是 |
常見場景 | DOM 操作、計時器、連接物件 | 搜尋、排序、伺服器操作 |
返回值 | ref 物件(.current) | isPending 和 startTransition |
性能影響 | 無(只是保存值) | 優先級管理(不阻塞 UI) |
場景 | 值改變不需要顯示更新 | 操作費時但需要 UI 回饋 |
useRef:
useRef 一個常見的用途是操作 DOM:
'use client'
import { useRef } from 'react'
export function TextInput() {
const inputRef = useRef(null)
const handleFocus = () => {
inputRef.current.focus()
}
return (
<>
<input ref={inputRef} />
<button onClick={handleFocus}>Focus</button>
</>
)
}useRef 真正的用處是跨越多次渲染保存一個值,同時改變它不會觸發重新渲染。
實例 1:POS 結帳倒計時
在 POS 系統中,如果說要幫結帳系統設計一個自動倒計時,30 秒後如果用戶沒有確認,則自動取消交易。這個計時器 ID 不需要顯示在 UI 上,改變它也不需要重新渲染。
'use client'
import { useRef, useEffect, useState } from 'react'
export function CheckoutTimer() {
const [timeLeft, setTimeLeft] = useState(30)
const timerIdRef = useRef(null)
useEffect(() => {
timerIdRef.current = setInterval(() => {
setTimeLeft(prev => {
if (prev <= 1) {
clearInterval(timerIdRef.current)
return 0
}
return prev - 1
})
}, 1000)
return () => {
clearInterval(timerIdRef.current)
}
}, [])
return (
<div>
<p>自動完成交易: {timeLeft} 秒</p>
<button onClick={() => clearInterval(timerIdRef.current)}>
取消倒計時
</button>
</div>
)
}這裡 timerIdRef 保存了計時器 ID,改變 timerIdRef.current 不會觸發重新渲染,因為只有 timeLeft 狀態改變才會重新渲染。組件卸載時,cleanup 函數通過 timerIdRef.current 來清理計時器。這個 case 如果不用 useRef,就必須把 timerIdRef 作為狀態,改變它每次都會重新渲染。
實例 2:記住上一次的值
在電商搜尋中,有時需要知道用戶上一次搜尋的關鍵詞(比如推薦相關商品),這個值沒有必要顯示。
'use client'
import { useRef, useEffect, useState } from 'react'
export function ProductSearch() {
const [searchTerm, setSearchTerm] = useState('')
const prevSearchTermRef = useRef('')
useEffect(() => {
prevSearchTermRef.current = searchTerm
}, [searchTerm])
const handleSearch = () => {
console.log(`當前: ${searchTerm}, 上次: ${prevSearchTermRef.current}`)
if (prevSearchTermRef.current && prevSearchTermRef.current !== searchTerm) {
console.log('推薦與上次搜尋相關的商品')
}
}
return (
<div>
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder="搜尋商品..."
/>
<button onClick={handleSearch}>搜尋</button>
</div>
)
}prevSearchTermRef 在每次渲染後保存上一次的值,useEffect 在更新 DOM 後執行,所以當你使用 prevSearchTermRef.current 時,它總是前一次的搜尋詞。
實例 3:保存 WebSocket 連接
在 real time 通知系統中,WebSocket 連接物件需要在組件卸載時關閉。
'use client'
import { useRef, useEffect } from 'react'
export function RealtimeNotifications() {
const wsRef = useRef(null)
useEffect(() => {
wsRef.current = new WebSocket('wss://api.example.com/notifications')
wsRef.current.onmessage = (event) => {
console.log('收到通知:', event.data)
}
wsRef.current.onerror = (error) => {
console.error('連接錯誤:', error)
}
return () => {
if (wsRef.current) {
wsRef.current.close()
}
}
}, [])
return <div>實時通知已連接</div>
}WebSocket 連接在組件掛載時建立,卸載時通過 wsRef.current 關閉。如果沒有 useRef,你無法在 cleanup 函數中存取這個連接物件。
useTransition:讓非同步操作不卡 UI
useTransition 的核心是讓 React 知道某個操作是低優先級的,React 會在這個操作進行時繼續回應用戶的高優先級操作(比如輸入框)。
import { useTransition, useState } from 'react'
const [isPending, startTransition] = useTransition()isPending 是一個布林值,當轉換正在進行時為 true。startTransition 是一個函數,你在裡面放要延遲的代碼。
實例 1:電商搜尋
當用戶搜尋 1000 個商品並排序時,不應該讓輸入框卡頓,useTransition 可以確保輸入框回應。
'use client'
import { useTransition, useState } from 'react'
export function ProductList({ products }) {
const [isPending, startTransition] = useTransition()
const [sortBy, setSortBy] = useState('name')
const [displayedProducts, setDisplayedProducts] = useState(products)
const handleSort = (newSortBy) => {
setSortBy(newSortBy)
startTransition(() => {
const sorted = products.sort((a, b) => {
if (newSortBy === 'name') {
return a.name.localeCompare(b.name)
} else if (newSortBy === 'price') {
return a.price - b.price
}
return 0
})
setDisplayedProducts([...sorted])
})
}
return (
<div>
<div>
<button onClick={() => handleSort('name')} disabled={isPending}>
{isPending ? '排序中...' : '按名稱排序'}
</button>
<button onClick={() => handleSort('price')} disabled={isPending}>
{isPending ? '排序中...' : '按價格排序'}
</button>
</div>
{isPending && <p>正在排序 {products.length} 個商品...</p>}
<ul>
{displayedProducts.map(p => (
<li key={p.id}>{p.name} - ${p.price}</li>
))}
</ul>
</div>
)
}關鍵是排序複雜計算被 startTransition 包裹。React 知道這個操作是低優先級的,如果用戶在排序時打字,輸入框會優先回應。
實例 2:POS 複雜訂單排序
在 POS 系統中,訂單列表可能有幾百條。按金額、日期、狀態排序時前端該怎麼互動呢?
'use client'
import { useTransition, useState } from 'react'
export function OrderList({ orders }) {
const [isPending, startTransition] = useTransition()
const [sortKey, setSortKey] = useState('date')
const [sortedOrders, setSortedOrders] = useState(orders)
const complexSort = (newSortKey) => {
setSortKey(newSortKey)
startTransition(() => {
const sorted = orders.sort((a, b) => {
if (newSortKey === 'amount') {
return b.totalAmount - a.totalAmount
} else if (newSortKey === 'date') {
return new Date(b.date) - new Date(a.date)
} else if (newSortKey === 'status') {
const statusOrder = { pending: 1, completed: 2, cancelled: 3 }
return statusOrder[a.status] - statusOrder[b.status]
}
return 0
})
setSortedOrders([...sorted])
})
}
return (
<div>
<div>
<button onClick={() => complexSort('amount')}>
{isPending && sortKey === 'amount' ? '排序中...' : '按金額排序'}
</button>
<button onClick={() => complexSort('date')}>
{isPending && sortKey === 'date' ? '排序中...' : '按日期排序'}
</button>
</div>
{isPending && <p>正在處理 {orders.length} 個訂單...</p>}
<table>
<tbody>
{sortedOrders.map(order => (
<tr key={order.id}>
<td>{order.id}</td>
<td>${order.totalAmount}</td>
<td>{order.date}</td>
<td>{order.status}</td>
</tr>
))}
</tbody>
</table>
</div>
)
}即使有幾百個訂單,排序時 UI 仍然回應迅速,按鈕不會卡。
常見錯誤
誤區 1:在 useRef 中放會影響 UI 的值
const countRef = useRef(0)
const handleClick = () => {
countRef.current += 1
console.log(countRef.current)
}
如果你想讓 UI 更新應該要用 useState,useRef 只適合完全不影響 UI 的值。
誤區 2:在 startTransition 中忘記 await
const handleFetch = () => {
startTransition(() => {
fetch('/api/data').then(r => {
setData(r)
})
})
}
正確做法:
const handleFetch = async () => {
startTransition(async () => {
const r = await fetch('/api/data')
const data = await r.json()
setData(data)
})
}這樣 isPending 會在 fetch 完成後才變成 false。
誤區 3:useTransition 用於同步操作
startTransition(() => {
setSortBy('time')
const filtered = simpleFilter(items)
setItems(filtered)
})
useTransition 主要用於異步操作或複雜同步計算(超過 50ms)。簡單操作不需要。