2026年8月14日
React useFocus Hook:追蹤並控制元素焦點狀態 (2026)
焦點是互動真正發生的地方——正在接收鍵盤輸入的輸入框、鍵盤使用者剛 Tab 到的按鈕。但 React 沒有為它提供任何 state。document.activeElement 知道答案卻從不通知你它變了,autoFocus 屬性只在掛載時觸發一次且無法重新觸發,而要把焦點用於渲染——編輯時顯示提示、浮動標籤、離開後再驗證——就得替每個需要的欄位手寫 focus/blur 監聽器。
@reactuses/core 的 useFocus 把這一切壓縮成一行:一個元件直接渲染的即時 isFocused 布林值,外加一個隨時能按你的邏輯聚焦或失焦元素的 setter。
快速上手
npm install @reactuses/core
import { useRef } from "react";
import { useFocus } from "@reactuses/core";
function SearchField() {
const ref = useRef<HTMLInputElement>(null);
const [isFocused, setFocused] = useFocus(ref);
return (
<div className={isFocused ? "field field--active" : "field"}>
<input ref={ref} placeholder="搜尋 hooks…" />
{isFocused && <kbd className="hint">esc 清空</kbd>}
<button onClick={() => setFocused(true)}>跳到搜尋</button>
</div>
);
}
整合到此為止。hook 訂閱元素的 focus 和 blur 事件並把它們鏡像成 state;setFocused(true) 呼叫 element.focus(),setFocused(false) 呼叫 element.blur()。一個元組,雙向打通——既能觀察焦點,也能指揮焦點。
為什麼不用 autoFocus、activeElement 或純 CSS?
內建的每個選項都只覆蓋問題的一角:
- CSS
:focus/:focus-within在回應是純樣式時就是正確工具——邊框變色、外發光。用它,零 JavaScript、零重渲染。hook 的用武之地在於焦點要驅動邏輯或 JSX:渲染提示面板、決定何時驗證、使用者打字時暫停輪播。 document.activeElement是快照,不是訂閱。在 render 裡讀它,下一次 Tab 它就過期了;焦點移動時沒有任何東西會讓你的元件重渲染。autoFocus只在掛載時觸發一次,這就是它的全部 API。它不能按需聚焦(「按/搜尋」)、不能失焦,也不告訴你當前狀態。- 在各個 handler 裡散落
ref.current.focus()能用——直到你還需要知道元素是否聚焦,於是監聽器還是得自己維護。
手寫版——以及坑在哪
手寫版看起來人畜無害:
// ⚠️ 手寫版——demo 裡能跑,應用裡漏 bug
function SearchField() {
const ref = useRef<HTMLInputElement>(null);
const [isFocused, setIsFocused] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const onFocus = () => setIsFocused(true);
const onBlur = () => setIsFocused(false);
el.addEventListener("focus", onFocus);
el.addEventListener("blur", onBlur);
return () => {
el.removeEventListener("focus", onFocus);
el.removeEventListener("blur", onBlur);
};
}, []);
// …
}
十五行裡藏著三個問題:
- 晚掛載的目標永遠接不上線。
if (!el) return只跑一次。如果輸入框是條件渲染的——在彈窗裡、在某個 tab 後面、在 loading 態之後——effect 早已 return,監聽器永遠掛不上。空依賴陣列表達不了「元素出現時重新執行」。 - 漏掉初始狀態。 如果在你的 effect 執行之前就有東西聚焦了這個元素(
autoFocus屬性、路由的焦點恢復),你的 state 是false,而元素正帶著焦點坐在那。除了監聽器,你還得在掛載時補一次document.activeElement檢查。 - 每個欄位都要複製一遍。 把這十五行乘以表單裡的每個輸入框,整個檔案大半是管線程式碼。
useFocus 把三個坑全部吸收:target 可以是惰性 getter(() => document.querySelector(".modal input")),隨 DOM 變化重新解析;掛載時的狀態幫你對齊;每個欄位就一行。
useFocus API
const [isFocused, setFocused] = useFocus(target, initialValue?);
target 很靈活——手頭有什麼傳什麼:
useFocus(ref); // ref 物件
useFocus(document.getElementById("search")); // 元素本身
useFocus(() => document.querySelector(".otp input")); // 惰性 getter
SVG 元素也支援——target 型別是 HTMLElement | SVGElement,圖表裡一個可聚焦的 <circle tabindex="0"> 同樣適用。
initialValue(預設 false)是宣告式的自動聚焦:傳 true,hook 會在掛載時聚焦元素。和 autoFocus 屬性不同,它走的是與 setFocused 相同的程式碼路徑,支援 getter 目標,之後你手裡還握著即時狀態。
setFocused 是自帶兜底的命令式控制:true → element.focus(),false → element.blur()。目標還不存在時,呼叫是安全的 no-op 而不是崩潰。
真實場景
浮動標籤——標籤自己知道何時上浮
Material 風格輸入框:標籤待在欄位裡,欄位活躍或有內容時上浮。
function FloatingLabelInput({ label }: { label: string }) {
const ref = useRef<HTMLInputElement>(null);
const [isFocused] = useFocus(ref);
const [value, setValue] = useState("");
const floated = isFocused || value.length > 0;
return (
<label className="float-field">
<span className={floated ? "float-label up" : "float-label"}>
{label}
</span>
<input ref={ref} value={value} onChange={e => setValue(e.target.value)} />
</label>
);
}
純 CSS 用 :focus-within + :placeholder-shown 能逼近,但上浮條件一旦涉及應用狀態——受控 value、驗證旗標——你就需要作為 state 的焦點,這就是它。
失焦時驗證,而不是每次擊鍵
對一個剛敲了三個字元的使用者吼「信箱格式錯誤」是經典的表單 UX 翻車。解法是 touched 語義——使用者離開欄位後才驗證:
function EmailField() {
const ref = useRef<HTMLInputElement>(null);
const [isFocused] = useFocus(ref);
const [value, setValue] = useState("");
const [touched, setTouched] = useState(false);
useEffect(() => {
if (isFocused) return; // 還在編輯——保持安靜
if (value) setTouched(true); // 帶著內容離開了欄位 → 開始評判
}, [isFocused, value]);
const error = touched && !isFocused && !value.includes("@");
return (
<div>
<input ref={ref} value={value} onChange={e => setValue(e.target.value)} />
{error && <p className="error">這看起來不像一個信箱地址。</p>}
</div>
);
}
isFocused 的翻轉就是 touched 訊號——不用層層透傳 onBlur,而且使用者回來修正的那一刻錯誤自動消失。
按 / 聚焦搜尋
每個文件站都有這個功能,setFocused 加 useEventListener 就是完整實作:
function DocSearch() {
const ref = useRef<HTMLInputElement>(null);
const [isFocused, setFocused] = useFocus(ref);
useEventListener("keydown", (e) => {
if (e.key === "/" && !isFocused) {
e.preventDefault(); // 別把斜線打進去
setFocused(true);
}
if (e.key === "Escape") setFocused(false);
});
return <input ref={ref} placeholder="按 / 搜尋" />;
}
注意元組的兩半各司其職:isFocused 防止劫持使用者正往輸入框裡敲的 /,setFocused 負責跳轉。
條件渲染也不怕的自動聚焦
聚焦彈窗表單的第一個欄位,而這個輸入框在 useDisclosure 點頭之前根本不存在:
function RenameDialog({ open }: { open: boolean }) {
const ref = useRef<HTMLInputElement>(null);
useFocus(ref, true); // 掛載即聚焦——也就是彈窗打開的那一刻
if (!open) return null;
return (
<dialog open>
<input ref={ref} defaultValue="untitled.md" />
</dialog>
);
}
因為元件(連同 hook)在彈窗打開時才掛載,initialValue: true 恰好在正確的時機觸發——不需要 setTimeout(…, 0) 之類的咒語。
useFocus 與它的兄弟們
@reactuses/core 提供三個焦點 hook,對應三個縮放級別——按你要問的問題選:
| Hook | 回答的問題 | 什麼時候用 |
|---|---|---|
useFocus | 「這個元素聚焦了嗎?」+ 控制 | 欄位級 UI:標籤、提示、驗證時機、快捷鍵 |
useActiveElement | 「全文件範圍內哪個元素持有焦點?」 | 表單級邏輯、焦點除錯、roving-focus 元件 |
useWindowFocus | 「分頁/視窗本身有焦點嗎?」 | 使用者切走時暫停輪詢或動畫 |
CSS :focus / :focus-visible | 僅樣式 | 任何純 CSS 能解決的回應——永遠先試它 |
經驗法則:單個元素 → useFocus;整個文件 → useActiveElement;瀏覽器視窗本身 → useWindowFocus。
生產環境注意事項
- SSR 已處理。 伺服器端沒有 DOM;hook 渲染你給的
initialValue,水合後再掛監聽器——你的程式碼裡不需要typeof window守衛。 element.focus()會捲動頁面。 瀏覽器會把新聚焦的元素捲進視口。頁面載入時自動聚焦一個首屏以下的元素會猛拽視口——initialValue: true留給使用者視線所在的元素(彈窗、行內編輯器)。- 別偷焦點。 移動焦點是無障礙動作,不是視覺動作:螢幕閱讀器會朗讀新聚焦的元素,鍵盤使用者會丟失位置。只在使用者意圖下聚焦(快捷鍵、打開彈窗),永遠不要在計時器或資料重新整理裡聚焦。
- 失焦會把焦點丟給
<body>。setFocused(false)不會把焦點還回原處——關閉彈窗後,請顯式把焦點交還給觸發按鈕。 - 樣式交給
:focus-visible,邏輯交給isFocused。 鍵盤專屬的焦點環是 CSS 已解決的問題;把環留在 CSS 裡,把 hook 的 state 花在邏輯上。相關的關注點也按同樣方式組合——點擊欄位外部是useClickOutside的事,不該用 blur hack。
要點回顧
- React 沒有焦點 state,原生原語也拼不出一個:
document.activeElement沒有響應性,autoFocus只觸發一次,手寫監聽器會漏掉晚掛載的元素和掛載前已聚焦的情況。useFocus就是那個缺失的[isFocused, setFocused]元組。 - setter 是雙向控制——
true聚焦,false失焦,元素還不存在時安全跳過;initialValue: true是宣告式自動聚焦,精確落在掛載那一刻。 - 殺手級場景都是時機場景:編輯時浮動標籤、離開後才驗證、按
/跳搜尋、彈窗第一個欄位一出現就聚焦。 - 純樣式歸 CSS 的
:focus和:focus-visible——把 hook 花在邏輯和 JSX 上。選對縮放級別:元素 →useFocus,文件 →useActiveElement,視窗 →useWindowFocus。 - 焦點是無障礙介面:跟隨使用者意圖移動它,永遠不偷它,用完把它還回原處。
useFocus 和其他 110+ 個 SSR 安全、TypeScript 優先的 hooks 都在 @reactuses/core——一次安裝,可搖樹最佳化,沒有需要看護的依賴。
npm install @reactuses/core