【筆記】React Input 輸入框 autoFocus 無法生效

前言

最近遇到一個有趣的小問題,簡單地筆記一下:React 用變數判斷是否啟用 autuFocus,有時會失效?原來是跟 DOM 的行為有關。本文將提供三種解法——條件渲染、初始寬度判斷以及 ref 手動 foucs。

情境與需求

我有一個 hook 會偵測瀏覽器的視窗大小變化,並回傳布林值表達是否為行動裝置(isMobile):

interface UseRwdResult {
isMobile: boolean;
}

const useRwd = (): UseRwdResult => {
const [width, setWidth] = useState<number>(0);

useEffect(() => {
setWidth(window.innerWidth);
const handleResize = ˙(() => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);

return { isMobile: width <= 768 };
};

我的需求是:行動裝置 input 框不要有 autoFocus,反之電腦版要有。

所以我的 input 框很直覺地這樣寫:

const CustomInput: FC = () => {
const { isMobile } = useRwd();

return (
<input
type="input"
autoFocus={!isMobile}
></input>
);
}

但在電腦版,這樣的寫法 autoFocus 並不會生效。

問題原因

window.innerWidth 是在 useEffect() 裡才取得,而 width 初始值是 0,所以第一次 render 時 isMobile 永遠會是 true(width <= 768)。後續雖然會更新成正確的值,但為時已晚。

input 框初始化時 autoFocus={!isMobile} 是 false,所以瀏覽器就不會對它做 focus。
後續即使 isMobile 變成 false,autoFocus=true,也不會再重新 focus 一次

解決方法

既然知道了根因是 autoFocus 是 DOM 一次性的行為,那就只要確保「初次 render input 框時就給予正確的 autoFocus 值」就可以了。有幾種方式:

const CustomInput: FC = () => {
// 要一併將 useRwd() 中 isMobile 的初始值改為 undefined
const { isMobile } = useRwd();

if (isMobile === undefined) return null;

return (
<input
type="text"
autoFocus={!isMobile}
/>
);
};

但這樣的缺點是畫面可能會有短暫的留白,而我的場景(可以點擊不同的按鈕,切換不同的 input 框,但 input 框位置都相同)就會造成 input 框有短暫地閃一下,我是不太喜歡這樣。

解法二(不推薦):調整 useRwd(),直接讓 width 預設 window.innerWidth

const useRwd = (): UseRwdResult => {
const [width, setWidth] = useState<number>(window?.innerWidth || 0);

useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);

return { isMobile: width <= 768 };
};

看起來是個好解法,但這不符合 react 設計模式:與外部世界互動的程式應該放在 useEffect 中。尤其是在伺服器渲染(SSR)時並並不會有 window 物件,所以這樣的寫法是危險的,也不推薦

const CustomInput: FC = () => {
const { isMobile } = useRwd();
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
if (!isMobile && inputRef.current) {
inputRef.current.focus();
}
}, [isMobile]);

return (
<input
ref={inputRef}
type="text"
/>
);
};

這是我最後採用的解法,雖然多幾行程式碼,但能確保 autoFocus 能如期運行,且不會影響 UX。

後記

沒想到 autoFocus 看似簡單,卻因為 react 的渲染方式,和 DOM 的行為,而讓人踩到這樣的雷。不過解法也很簡單,如果不介意 input 框因為延遲載入而導致畫面可能會有短暫留白或閃爍,那解法一將會是最簡單直覺的方式。

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments