3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【React】DOM操作について(CreatePortal・Ref)

Posted at

ポータルとは

ReactにおけるDomツリーを操作するのに使用する機能。
対象の子要素を現在の直接的な親要素から、別のDOM要素にマウントすることができる。

createPortalは、そのポータルを作成するためのメソッド。

具体例

import { useState } from "react";
import { createPortal } from "react-dom";

const ModalPortal = ({ children }) => {
  const containerStart = document.querySelector('.container.start');
  return createPortal(children, containerStart);
}


const Example = () => {
  const [modalOpen, setModalOpen] = useState(false);
  return (
    <div>
      <div className="container start"></div>

      <button
        type="button"
        onClick={() => setModalOpen(true)}
        disabled={modalOpen}
      >
        モーダルを表示する
      </button>
      {modalOpen && 
        (
          <ModalPortal>
            <Modal handleCloseClick={() => setModalOpen(false)} />
          </ModalPortal>
        )
      }
    </div>
  );
};

export default Example;

コンポーネント

import "./Modal.css";

const Modal = ({ handleCloseClick }) => {
  return (
    <div className="modal">
      <div className="modal__content">
        <p>モーダル</p>
        <button type="button" onClick={handleCloseClick}>
          閉じる
        </button>
      </div>
    </div>
  );
};

export default Modal;

ポータルの注意点

バブリング

子要素でイベント(クリックイベントなど)が発生した場合、その子要素に直接関係する親要素に伝播する。

このとき、親要素にも要素にハンドラが登録されている場合には、親要素のハンドラが実行される。

これはDOMが移動するポータルでも同様の現象が発生する。

import { useState } from "react";
import { createPortal } from "react-dom";

const ModalPortal = ({ children }) => {
  const containerStart = document.querySelector('.container.start');
  return createPortal(children, containerStart);
}


const Example = () => {
  const [modalOpen, setModalOpen] = useState(false);
  return (
    <div onClick={ () => console.log("バブリング") }>
      <div className="container start"></div>

      <button
        type="button"
        onClick={() => setModalOpen(true)}
        disabled={modalOpen}
      >
        モーダルを表示する
      </button>
      {modalOpen && 
        (
          <ModalPortal>
            <Modal handleCloseClick={() => setModalOpen(false)} />
          </ModalPortal>
        )
      }
    </div>
  );
};

export default Example;

モーダルを表示すると、consoleに”バブリング”が出力される。

スクリーンショット 2022-12-24 15.42.12.png

Ref

React Hooksの1つ。
DOMの参照データの保持の2つの機能を持っているフック。

useRefを使用して作成されるオブジェクトはrefオブジェクトと呼び、currentプロパティを持つ。このcurrentプロパティに値が保持される。
参照する際には、変数.currentで呼び出し、呼び出しや書き換えを行う。


currentプロパティに値を保持するには、保持したいDOMに対しref属性を設定し、ref属性にrefオブジェクトを代入することでcurrentプロパティに保持される。

import { useState, useRef } from "react";

const Case1 = () => {
  const [value, setValue] = useState("");
  const inputRef = useRef();

  return (
    <div>
      <h3>ユースケース1</h3>
      {// ref属性にrefオブジェクトを渡している。}
      <input type="text" ref={inputRef} value={value} onChange={(e) => setValue(e.target.value)} />
      <button onClick={ () => inputRef.current.focus() }>
        インプット要素をフォーカスする
      </button>
    </div>
  );
};

const Example = () => {
  return (
    <>
      <Case1 />
    </>
  );
};

export default Example;

useRefで保持された値は、更新されても再描画が行われないのが最大の特徴。

Portalとの違い
・Portalは特定の子要素を別の親要素に依存関係を変更する。

・refはDOMに直接にアクセスし、アクションを反映させる。

forwardRef

他のコンポーネントのDOMにアクセスすることができる。

通常のuseRefで作成するrefオブジェクトはpropsでコンポーネントに渡すことができない。

import { useRef } from "react";

const Input = ({ref}) => {
  return (
    <input type="text" ref={ref} />
  )
}

const Example = () => {
  const ref = useRef();
  return (
    <>
      <Input ref={ref}/>
      <button onClick={() => ref.current.focus()}>
        インプット要素をフォーカスする
      </button>
    </>
  );
};

export default Example;

スクリーンショット 2022-12-24 18.23.57.png

このやり方はReactでは推奨されていない。

解決方法①

refというprops名を使わず、別のprops名で指定して渡す。

import { useRef } from "react";

const Input = ({customRef}) => {
  return (
    <input type="text" ref={customRef} />
  )
}

const Example = () => {
  const ref = useRef();
  return (
    <>
      <Input customRef={ref}/>
      <button onClick={() => ref.current.focus()}>
        インプット要素をフォーカスする
      </button>
    </>
  );
};

export default Example;

refはReact上では特別な単語なので、props名としてrefを使用することが先ほどのエラーに繋がっている。
よって別の名前を使用することで解決が可能。

解決方法②

forwardRef関数を使用する。

import { useRef, forwardRef } from "react";

/**
 * 第一引数にはprops、
 * 第二引数にrefオブジェクトを受け取るようにする。
 *
 * 通常の関数コンポーネントでは、第二引数を利用することはないが、
 * forwardRefは例外で第二引数を利用する。
 */
const Input = forwardRef((props, ref) => {
  return (
    <input type="text" ref={ref} />
  )
})

const Example = () => {
  const ref = useRef();
  return (
    <>
      <Input ref={ref}/>
      <button onClick={() => ref.current.focus()}>
        インプット要素をフォーカスする
      </button>
    </>
  );
};

export default Example;

forwardRef関数を使用することで、refでも渡すことが可能になる。
refの受け渡しについては、親 → 子の一方通行であり、親から子のDOMを操作することができる。

①と②はどちらを使用しても良い。
しかし、この親から子にrefを渡すという行為は、コンポーネント間の依存関係を強めてしまう行為であるため、使用には注意が必要である。

useImperativeHandle

先ほどのforwardRefで、親コンポーネントからInputコンポーネント`に対してrefオブジェクトを渡し、親から Inputコンポーネントを操作できるようにした。

これは逆にいうと、Inputコンポーネントを親コンポーネントから自在に操作できてしまうというデメリットがある。
作成した作成者にとって意図しない操作も自在にできてしまうのはあまり良いこととは言えないので、操作を限定することが必要。

そこで使われるのが、useImperativeHandleである。

使い方

import { useRef, forwardRef, useImperativeHandle } from "react";

const Input = forwardRef((props, ref) => {

  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    myFocus() {
      inputRef.current.focus();
    }
  }))

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

const Example = () => {
  const ref = useRef();
  return (
    <>
      <Input ref={ref} />
      <button onClick={() => ref.current.myFocus()}>
        インプット要素をフォーカスする
      </button>
    </>
  );
};

export default Example;

ポイント①

・第一引数 → 親から渡されたrefオブジェクトを受け取る。

・第二引数 → 実行したいメソッドを含むオブジェクトを返す、関数を定義する。

const Input = forwardRef((props, ref) => {
         .
         .
         .
  useImperativeHandle(ref, () => ({
    myFocus() {
      inputRef.current.focus();
    }
  }))

});

const Example = () => {
  const ref = useRef();
         .
         .
         .
  return <Input ref={ref} />;
}

ポイント②

子コンポーネントのref属性には、親から渡されたrefオブジェクトとは別のrefオブジェクトを代入する。


const Input = forwardRef((props, ref) => {

  {// useRefオブジェクトを新規作成。}
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    myFocus() {
      inputRef.current.focus();
    }
  }))

  {// 子で新規作成したコンポーネントをrefに代入。}
  return <input type="text" ref={inputRef} />;
});

なぜそうするのか

子コンポーネントのuseImperativeHandleで定義したメソッドを親から実行するという形を取るため。
refを直接代入したら、結局親から自在に操作できることになってしまう。よって、

①子コンポーネントで一度新規でrefオブジェクトを作成する。

useImperativeHandleの第二引数で、「新規refオブジェクトのcurrentプロパティに対してイベントを実行する」というメソッドを定義する。

useImperativeHandleの第二引数で定義したメソッドは、第一引数で設定した親のrefオブジェクトからアクセスができるので、そのメソッドを実行。

④親から子コンポーネントで定義した新規refオブジェクトに直接アクセスする方法はなく、子コンポーネントのrefにはその新規refオブジェクトを代入しているため、親からの操作を限定できている。

という流れになる。

最後に

途中にも記載をしたように、親子のコンポーネント間でpropsでrefを受け渡しする行為は、

①親子間の依存関係を強めてしまう。

②処理が行ったり来たりするためコードが煩雑になる + 可読性が悪くなる。

ということが起きるため、他に解決策がある場合はそちらを採用することが推奨されている。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?