簡單介紹 react forwardRef 跟 useImperativeHandle
上一篇介紹的 useRef 把 ref 放在一般 JSX 標籤上來取得元素,但是如果希望取得子元件的元素的話直覺可能會想說那就把 ref 傳到子元件然後在放到 JSX 的標籤上,像下面這樣。
import { RefObject, useEffect, useRef } from "react";
function Input({ ref }: { ref: RefObject<HTMLInputElement> }) {
return <input ref={ref} />;
}
function App() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// 透過 useEffect 來 console 觀察
console.log(inputRef.current);
}, []);
return (
<div>
<h1>forwardRef</h1>
<Input ref={inputRef} />
</div>
);
}
但是如果你嘗試把 ref 放在 react component 上的話會出現以下錯誤,而且也沒辦法取得元素。
這時候就可以使用到 forwardRef 了。
const SomeComponent = forwardRef(render)
render
: render function,寫法跟普通的元件沒有什麼不同,唯一的差別是除了 prop 參數以外,還會接收到另外一個參數 「ref」,這個 ref 就是當父層元件把 ref 往下傳遞時就可以透過這個 ref 取得子元件的 DOM 元素。
SomeComponent
: react 元件,可以接受 ref 作為參數。
接著就來看 code 吧。
import { useEffect, useRef, forwardRef } from "react";
const Input = forwardRef<HTMLInputElement>(function Input(prop, ref) {
return <input ref={ref} />;
});
function App() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// 透過 useEffect 來 console 觀察
console.log(inputRef.current);
}, []);
function focusInput() {
if (!inputRef.current) return;
inputRef.current.focus();
}
return (
<div>
<h1>forwardRef</h1>
<Input ref={inputRef} />
<button onClick={focusInput}>focus</button>
</div>
);
}
透過 forwardRef 可以讓父層的 ref 往下傳,這樣我們就可以取得子元件的 DOM 標籤了。
上面有提到,當我們把 ref 放在 react 元件時會出現錯誤訊息,那如果修改一下屬性的名字是不是就可以了?
import { useEffect, useRef, RefObject } from "react";
function Input({ myRef }: { myRef: RefObject<HTMLInputElement> }) {
return <input ref={myRef} />;
}
function App() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// 透過 useEffect 來 console 觀察
console.log(inputRef.current);
}, []);
function focusInput() {
if (!inputRef.current) return;
inputRef.current.focus();
}
return (
<div>
<h1>forwardRef</h1>
<Input myRef={inputRef} />
<button onClick={focusInput}>focus</button>
</div>
);
}
上面我用 myRef={ref}
把 ref 傳的 child 的元件內。
結果一樣可行,也沒有出現錯誤訊息。
但是 react 官方並不建議我們這麼做,react 不建議元件內的 DOM 節點可以被其他元件操作或隨意使用,大部分情況都可以透過 react 來協助做狀態的管理,會需要直接操作 DOM 的情況並不多。
當有必要的時候可以使用 forwardRef,來特別告訴 react,這一個元件的 DOM 元素可以被其他元件訪問。
接著再另外介紹一個常跟 forwardRef 搭配使用的 hook useImperativeHandle
上面有提到 forwardRef 可以把元件內的 DOM 節點暴露到父層元件,讓父層元件可以操作子元件裡的 DOM 元素,如果我們希望可以保護子元件裡面的 DOM 元素不要被父元件隨意操作或修改的話,就可以使用這個 Hook 幫我們處理。
useImperativeHandle(ref, createHandle, dependencies?)
ref
: 這是從 forwardRef 裡面 render function 接收到第二個參數的那個 ref。createHandle
: 一個 function,回傳一個可以操作 DOM 元素的 object。dependencies?
: 跟 useEffect 一樣的 dependencies array,裡面放著createHandle
中有使用到的 props 或是 state,當 dependencies 裡面的內容有改變時會創建新的 createHandle
。
接著就來看 code 吧
import { useEffect, useRef, forwardRef, useImperativeHandle } from "react";
type RefHandler = {
focus: () => void;
blur: () => void;
};
const Input = forwardRef<RefHandler>(function Input(_prop, ref) {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(
ref,
() => {
return {
// 改寫 focus 的行為
focus() {
if (!inputRef.current) return;
inputRef.current.value += "1";
},
// 改寫 blur 的行為
blur() {
if (!inputRef.current) return;
inputRef.current.focus();
},
};
},
[] // dependencies 為空
);
return <input ref={inputRef} />;
});
function App() {
const inputRef = useRef<RefHandler>(null);
useEffect(() => {
// 透過 useEffect 來 console 觀察
console.log(inputRef.current);
}, []);
function focusInput() {
if (!inputRef.current) return;
inputRef.current.focus();
}
function blurInput() {
if (!inputRef.current) return;
inputRef.current.blur();
}
return (
<div>
<h1>forwardRef</h1>
<Input ref={inputRef} />
<button onClick={focusInput}>focus</button>
<button onClick={blurInput}>blur</button>
</div>
);
}
我把 App 裡面 useRef 所回傳的 inputRef
放到了 Child 裡面,並且在 Child 裡面又另外使用 useRef 取得一個 inputRef
,並且把 Child 的 inputRef
放到了 input 上,然後使用 useImperativeHandle
讓 App 一樣可以操作子元件裡面的 DOM,但是我做了一些修改,讓本來的 focus 跟 blur 的行為不一樣。
useImperativeHandle
可以讓我們在保護元件內的 DOM 節點不被外部隨意操作下,讓父層元件一樣可以有限制的操作子元件的 DOM 節點。
forwardRef
Manipulating the DOM with Refs
useImperativeHandle
下一篇會簡單介紹 createContext & useContext。
如果內容有誤再麻煩大家指教,我會盡快修改。
這個系列的文章會同步更新在我個人的 Medium,歡迎大家來看看 👋👋👋
Medium