ReactのRef属性でDOMをコントロールする

RefはDOM操作に役立つReactのAPIです。Hooksで追加されたのがuseRefになります。
基本的にReactでDOM操作を行おうと思ったとき、ピュアJavaScriptのdocument.getElementByIddocument.querySelector系のAPIは使用できません。というよりもidを使用すること自体がNGです。
idをもつということはReactが最終的なbundle.jsとして吐き出した際に、idが重複したものが出力される可能性があるためです。そういったことからidの使用はアンチパターンといえます。
そこでReactでは代替えとしてref属性からDOMを参照し、操作を行うことができます。

Ref属性とは

そもそもこのRefとは何?という人のために解説。
reference a DOM リファレンス(参照)という名前から命名されており、

このRefの主な役割は以下の3つ

  • Managing focus, text selection, or media playback.
  • Triggering imperative animations.
  • Integrating with third-party DOM libraries.
  • テキストの選択、フォームへのフォーカスもしくはメディア(動画、音声)の再生の制御
  • アニメーションを発火
  • サードパーティのDOMコンポーネント組み込み

といった意味合いになります。
様々な記事で取り上げられてるのがrefを使用したスクロールイベントの発火処理になります。
このようにRefはReact上でDOMを参照したい場合などに使用されます。

どのように使うのか?

Refに関するAPIは複数存在しています。
公式ドキュメント内で取り上げられているのは以下の3つ
– CreateRef
– Refコールバック属性
– Ref文字列属性を用いる

Ref文字列属性はversion 17より非推奨になり、削除されています。
参考までにコードを載せておきます。

<!-- refsは使用できません -->
<input type="text" refs="name"/>

よって、使用するのはCreateRefもしくはRefコールバック属性になります。
更に追加でReact HooksからuseRefが加わっています。
基本的にFunction Componentで実装を進めることが多いので、本記事ではuseRefベースを解説しますが、先にCreateRefとRefコールバックについても理解しておきましょう。

まずCreateRefについて。
こちらは名前の通りRefを生成することが可能です。
useRefとCrateRef、どちらを使用するかという議論もありますが、そもそもの役割が異なっています。

createRef – 常に新しいrefを生成して返します
useRef – 最初のレンダリング時と同じrefを毎回返します

なのでcreateRefは新しいrefの生成に用います、またこちらに関してはClassComponentのときに主に使われていたAPIになるため、FunctionComponentにおいてはuseRefを推奨しています。しかし、使用はできます。後ほどuseRefとcreateRefを使用したコードを記載します。

続いて、refのコールバックですがReactは、コンポーネントがマウントされるとDOM要素でrefコールバックを呼び出し、アンマウントされるとnullで呼び出します。
よってrefコールバックは、componentDidMountおよびcomponentDidUpdateライフサイクルフックの前に呼び出されます。

まず、前提としてDOMを操作しようとしたときDOMがすでにレンダリングされている状態でなければなりません。先程も書いたようにマウントされるとDOM要素でrefを呼び出します。なのでuseEffectを使用し、componentDidMount時にRefでのDOM操作を加えます。
以下のソースはstyled-componentsによって生成されたInput要素にフォーカスを当てるという簡単なDOM操作です。

import React, { useRef, useEffect } from 'react';
import styled from 'styled-components';

const StyledInput = styled.input`
  background: transparent;
`

const InputForm: React.MutableRefObject<HTMLInputElement> = ():JSX.Element => {
  const inputRef = useRef<HTMLInputElement>(null) as React.MutableRefObject<HTMLInputElement>;

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

  return (
    <StyledInput ref={inputRef}/>
  );
}
export default InputForm;

処理の内容としては、とても簡単で最初にRefを定義します。

const inputRef = useRef<HTMLInputElement>(null) as React.MutableRefObject<HTMLInputElement>;

useRefの引数にnullが入っていますが、こちらはレンダリングする前の初期値です。(そもそもレンダリング前なのでDOMが存在していないため)
そして引数の型にHTMLInputElementとあるように、useRefにはHTMLInputElementが入ってくることを意味しています。

そして先程も述べたようにfocusのDOMイベントを発火させるのはComponentDidMountです。しかし、そのタイミングでもuseRefがきちんと取れていない場合があるためrefにcurrentが存在しているかチェックする必要があります。
また、TypeScriptにおけるRef属性の型についてですがReact.RefObject<HTMLInputElement>を当てることができます。
interfaceを使用する場合など、refは以下のように定義できます。

interface Props {
    ref: React.RefObject<HTMLInputElement>;
}

Refが複数存在する場合

Refが複数存在するケースがあります。その場合はmapを使用して処理することが可能です。
以下のコードを見てください

import React, { useRef, useEffect } from 'react';

const MyDiv = () => {
    const elRef: React.MutableRefObject<React.RefObject<HTMLDivElement>[]> = useRef(...Array(3).map(() => createRef<HTMLDivElement>()));
    return (
        <>
            {[1, 2, 3].map((el, i) => (
                <div ref={elRef.current[i]}>Test Div</div>
            ))}
        </>
    );
}
export default MyDiv;

こちらはmapで回してCreateRefでRefを生成するという方法になります。
結果的にelRefReact.MutableRefObject<React.RefObject<HTMLDivElement>[]>というRefObjectの配列型になります。
このように複数のDOMに対して操作したい場合でも有効です。

まとめ

Reactにおいて、DOM操作をすることは多少なりとあります。そうなった場合、ちゃんとRefによる操作を理解しているかどうかが大事になってきます。

抑えておくべきポイントとしては

  • useRefとcreateRefの意味
  • refのコールバックの理解、タイミング
  • refの型について

といったところではないでしょうか?

関連記事