iT邦幫忙

2023 iThome 鐵人賽

DAY 13
0
Modern Web

react 學習記錄系列 第 13

[Day13]我的 react 學習記錄 - react forwardRef & useImperativeHandle

  • 分享至 

  • xImage
  •  

這篇文章的主要內容

簡單介紹 react forwardRef 跟 useImperativeHandle


forwardRef

上一篇介紹的 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 上的話會出現以下錯誤,而且也沒辦法取得元素。

https://ithelp.ithome.com.tw/upload/images/20230919/20161583DVnZO42fzN.png

這時候就可以使用到 forwardRef 了。


Syntax

const SomeComponent = forwardRef(render)

render: render function,寫法跟普通的元件沒有什麼不同,唯一的差別是除了 prop 參數以外,還會接收到另外一個參數 「ref」,這個 ref 就是當父層元件把 ref 往下傳遞時就可以透過這個 ref 取得子元件的 DOM 元素。

SomeComponent: react 元件,可以接受 ref 作為參數。

注意事項

  • forwardRef 並不是 hook,所以請不要在 react 的元件內呼叫它,像建立一個普通元件一樣,在元件外使用。
  • 嚴格模式開啟時,在開發模式下,react 會在畫面第一次 render 時下快速的進行 mount -> unmount -> mount 的動作確保沒有多餘的 side effect 而發生錯誤,這個動作不應該發生任何預期外的錯誤。

接著就來看 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-1

透過 forwardRef 可以讓父層的 ref 往下傳,這樣我們就可以取得子元件的 DOM 標籤了。


不要透過改變屬性把 ref 往下傳遞

上面有提到,當我們把 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 的元件內。

forwardRef

結果一樣可行,也沒有出現錯誤訊息。

但是 react 官方並不建議我們這麼做,react 不建議元件內的 DOM 節點可以被其他元件操作或隨意使用,大部分情況都可以透過 react 來協助做狀態的管理,會需要直接操作 DOM 的情況並不多。
當有必要的時候可以使用 forwardRef,來特別告訴 react,這一個元件的 DOM 元素可以被其他元件訪問。


useImperativeHandle

接著再另外介紹一個常跟 forwardRef 搭配使用的 hook useImperativeHandle

上面有提到 forwardRef 可以把元件內的 DOM 節點暴露到父層元件,讓父層元件可以操作子元件裡的 DOM 元素,如果我們希望可以保護子元件裡面的 DOM 元素不要被父元件隨意操作或修改的話,就可以使用這個 Hook 幫我們處理。

Syntax

useImperativeHandle(ref, createHandle, dependencies?)

ref: 這是從 forwardRef 裡面 render function 接收到第二個參數的那個 ref。
createHandle: 一個 function,回傳一個可以操作 DOM 元素的 object。
dependencies?: 跟 useEffect 一樣的 dependencies array,裡面放著createHandle 中有使用到的 props 或是 state,當 dependencies 裡面的內容有改變時會創建新的 createHandle

注意事項

  • hook 只能在元件裡使用,且只能在元件的最外層使用,不能放在迴圈或是判斷式裡使用。

接著就來看 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

useImperativeHandle 可以讓我們在保護元件內的 DOM 節點不被外部隨意操作下,讓父層元件一樣可以有限制的操作子元件的 DOM 節點。


forwardRef
Manipulating the DOM with Refs
useImperativeHandle

下一篇會簡單介紹 createContext & useContext。
如果內容有誤再麻煩大家指教,我會盡快修改。

這個系列的文章會同步更新在我個人的 Medium,歡迎大家來看看 👋👋👋
Medium


上一篇
[Day12]我的 react 學習記錄 - useRef
下一篇
[Day14]我的 react 學習記錄 - createContext & useContext
系列文
react 學習記錄30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言