React の useRef フックの使い方を具体例をまじえて解説【 TypeScript 】

ショウヘイ

どうも、ノマドクリエイターのショウヘイ(@shohei_creator)です。

ふーちゃん

ブログアシスタントのふーちゃんです。

React では、 getElementById メソッドや queryselector メソッドのような API を使うことも出来ますが、これらは非推奨となっています。 React によって各種ファイルがバンドルされた時の ID 衝突の危険性があるからです。

React には、きちんと DOM 操作のための機能が組み込まれています。特に、関数コンポーネントの実装向けに、 useRef フックが用意されています。

 この記事では、 React の useRef フックの構文と具体例について説明します。 

目次

React の useRef フックの 2 つの機能( DOM 操作と状態保持)

React の useRef フックの基本構文を紹介する前に、 useRef フックの 2 つの機能について紹介します。

useRef フックの 1 つ目の機能は、記事の冒頭でも触れたとおり、 DOM 操作です。 useRef フックを使うと、 ref オブジェクトを生成できます。この ref オブジェを対象の DOM ノード( HTML タグ)の ref 属性に設定すると、 ref オブジェクト経由で DOM 操作できるようになります。

useRef フックの 2 つ目の機能は、状態の保持です。 useRef フックを使って生成できる ref オブジェクトは、 current プロパティを持っています。この current プロパティに設定した値は、 useState フックのように、コンポーネントが再マウントされても保持されます。ただし、 useRef の current プロパティが更新されても、再レンダリングが起きません。ここが useState フックとの違いです。

ショウヘイ

useRef の状態保持の機能は、 useState と似ています。

しかし、 useRef フックの場合は、状態を更新しても再レンダリングされない特徴を持っています。レンダリング回数を抑えたい場面では、地味に活躍しそうです。

ふーちゃん

ひとまず、「 useRef フックは、 DOM 操作のために使う機能」と考えておいて差し支えなさそうですね。


React の useRef フックの基本構文

React の useRef フックには、引数として初期値を渡せます。この初期値は、 ref オブジェクトの current プロパティに設定されます。なお、 useRef フックを DOM 操作のために使うなら、引数に null を設定してください。

こちらは、 JavaScript で useEffect を書いた場合のコードです。

// 基本
const refObject = useRef(initalValue);

// useRefを状態保持を目的に使う場合は、引数に数字や文字列などを設定
const refObject = useRef(0);

// useRefをDOM操作のために使う場合は、引数にnullを設定
const refObject = useRef(null);

こちらは、 TypeScript で useEffect を書いた場合のコードです。 useRef の直後に型宣言します。 DOM 操作の場合は、対象の DOM ノード( HTML タグ)に対応する型を宣言してください。

// 基本
const refObject = useRef<Type>(initalValue);

// useRefを状態保持を目的に使う場合は、引数に数字や文字列などを設定
const refObject = useRef<number>(0);

// useRefをDOM操作のために使う場合は、引数にnullを設定

// inputタグをDOM操作したい場合
const refObject = useRef<HTMLInputElement>(null);

// buttonタグをDOM操作したい場合
const refObject = useRef<HTMLButtonElement>(null);

// divタグをDOM操作したい場合
const refObject = useRef<HTMLDivElement>(null);
ショウヘイ

実際に useRef フックを使う時は、 react ライブラリから import することを忘れないでください。

import React, { useRef } from "react";

React の useRef フックの初期値による仕様の違い

ショウヘイ

TypeScript で React の useRef を使う場合は、『初期値が null かどうか』によって、生成される ref オブジェクトの仕様が微妙に変わります。

useRef フックの初期値が null の場合

React の useRef フックの初期値に null を設定した場合は、 ref オブジェクトの current プロパティは、 readonly (読み取り専用)として宣言されます。 current プロパティの参照できますが、更新できません。初期値に null を設定する時は DOM 操作したい場合のはずなので、 current プロパティが readonly になっていても、特に問題ありません。

// usrRefにnullを渡したので、refObject.currentは、readonlyになる
const refObject = useRef<Type>(null);

なお、 TypeScript を使って useRef フックの初期値に null を設定した場合には、 null 参照による警告を考慮しなければいけません。実装では、 ref オブジェクトを参照する直前で、 if 文による分岐を作っておく必要があります。

こちらの具体例では、 useRef フックによって、 DOM ノード( input タグ)の参照が取得できている場合のみ、 if 文の中身が実行されます。

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

export const Component: React.VFC = () => {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {

    // if文を使って、inputRefのcurrentプロパティが存在するか確認
    if(inputRef.current) {
      inputRef.current.focus();
    }
  }, []);

  return (
    <form>
      <label>
        名前:
        <input ref={inputRef} type="text" name="name" />
      </label>
      <input type="submit" value="確定する" />
    </form>
  );
};

ちなみに、 useRef フックの初期値で非 null アサーション演算子 (Non-Null Assertion Operator) を使う手もあります。 null ではないことが宣言されるので、 ref オブジェクトの current プロパティを参照する前の if 文は不要になります。

(途中省略)

// 初期値のnullの後ろに「!」をつけて、null型ではないことを宣言
const inputRef = useRef<HTMLInputElement>(null!);

useEffect(() => {

 // inputRef.currentがnullではないことが前提になるので、if文が不要になる
  inputRef.current.focus();
}
}, []);

(以下、省略)

あるいは、オプショナルチェイニング「 ?. 」を使う手もあります。オプショルチェイニングの書き方は、参照先が null または undefined でありそうな変数の直後に「 ?. 」をつけます。すると、もしも参照先が null または undefined で あっても、エラーが起きなくなります(その代わり、 undefined が返されます)。

なお、オプショナルチェイニングは、エラーが起きなくなる特性があるため、 ref オブジェクトについて不具合があった場合のデバッグを困難にします。たとえば、『 DOM 操作したい DOM ノード( HTML タグ)の ref 属性に対して、 ref オブジェクトの設定を誤っていた』というような場合です。この点は、注意してください。

(途中省略)

useEffect(() => {

 // currentプロパティが参照できなかった時は、後続のfucus()が実行されずにすむ
  inputRef.current?.focus();
}
}, []);

(以下、省略)

useRef フックの初期値が null ではない場合 

React の useRef フックの初期値に null ではない……つまり文字列や数値といった何かしらの値の場合は、 ref オブジェクトの current プロパティは、書き換え可能な状態になります。

また、 useState フックのように、 state や props の更新によってコンポーネントがマウントされても、 current プロパティの状態が保持されます。なお、 current プロパティの状態が更新されても、再レンダリングが起きません。

let で宣言したように再代入可能ながら、 useState フックのように状態を保持できて、でも再レンダリングの対象とならない……という変わった仕様が特徴です。

ショウヘイ

useRef フックと useState フックを使って、ちょっとしたカウンターを用意しました。 

useRef の方のボタンをクリックしても、見た目の数値が増えません。でも、コンソールの出力では、 refCount の数値が増えます。

つまり、 refCount が更新されても、再レンダリングが起きない(再マウントが起きない)ということです。

ふーちゃん

useState の方のボタンをクリックしたら、 refCount の数値が画面に反映されますよ。 

stateCount の更新は再レンダリングの対象なので、結果的に、コンポーネントがマウントされた証拠ですね。

import React, { useState, useRef } from "react";
import { Container, Row, Alert, Button } from "react-bootstrap";

export const Component: React.VFC = () => {
  const [stateCount, setStateCount] = useState<number>(0);
  const refCount = useRef<number>(0);

  const incrementState = () => {
    setStateCount((prevCount) => prevCount + 1);
  };

  const incrementRef = () => {
    refCount.current++;
    console.log("refCount:" + refCount.current);
  };

  return (
    <Container className="mt-4">
      <Row>
        <Alert variant="primary">
          <p>useStateの「stateCount」: {stateCount}</p>
          <Button variant="primary" onClick={incrementState}>
            カウントを増やす
          </Button>
        </Alert>
      </Row>
      <Row>
        <Alert variant="success">
          <p>useRefの「refCount」: {refCount.current}</p>
          <Button variant="success" onClick={incrementRef}>
            カウントを増やす
          </Button>
        </Alert>
      </Row>
    </Container>
  );
};

React の useRef フックの初期値についての参考記事

Qiita
React + TypeScript: useRefの3つの型指定と初期値の使い方 - Qiita ReactとTypeScriptの使い方がまとめられた「React + TypeScript Cheatsheets」の「useRef」の情報にもとづいて、このフックの型づけと初期値の与え方について3つの定めを解...

React の useRef フックで複数の DOM ノードを操作する

ショウヘイ

React で複数の DOM ノードを操作したい場合には、 DOM ノードの数に応じた ref オブジェクトを作る必要があります。

複数の ref オブジェクトを作る方法は、大きく分けて 2 種類です。それぞれの作り方を紹介しますす。

何度も useRef フックを使って、複数の ref オブジェクトを作る

React で複数の DOM ノードを操作しようと考えた時に、 useRef フックだけで完結させるなら、『 DOM ノードの数だけ useRef フックを使って、複数の ref オブジェクトを作る』という方法になります。

useState フックや useEffect フックなどにも共通して言えることですが、 React では、コンポーネント内で何度でもフックを使えます。


// inputタグ用のrefオブジェクト
const inputRef = useRef<HTMLInputElement>(null);

// buttonタグ用のrefオブジェクト
const buttonRef = useRef<HTMLButtonElement>(null);

// divタグ用のrefオブジェクト
const divRef = useRef<HTMLDivElement>(null);
ショウヘイ

複数の useRef フックを使う具体例として、入力欄の数値で大きさの変わる四角形を用意しました。 

2 つの入力欄に対して、 useRef フックで作った ref オブジェクトを ref 属性に渡すことで、 value を取得しています。

ふーちゃん

useRef フックの仕様を説明するために、あえて ref オブジェクト経由で value を取得しています。

実際に input タグの value を取得する時は、 onInput や onChange と event.target プロパティを組み合わせた方がお手軽ですよ。

import React, { useState, useRef } from "react";
import "./Component.css";

type Style = {
  width: string;
  height: string;
  backgroundColor: string;
};

export const Component: React.VFC = () => {
  const refInputWidth = useRef<HTMLInputElement>(null);
  const refInputHeight = useRef<HTMLInputElement>(null);
  const [divStyle, setDivStyle] = useState<Style>({
    width: "100px",
    height: "100px",
    backgroundColor: "skyblue"
  });

  const updateWidth = () => {
    if (refInputWidth.current)
      setDivStyle({ ...divStyle, width: `${refInputWidth.current.value}px` });
  };

  const updateHeight = () => {
    if (refInputHeight.current)
      setDivStyle({ ...divStyle, height: `${refInputHeight.current.value}px` });
  };

  return (
    <div className="container">
      <div className="form">
        <div className="form__row">
          <label className="form__label" htmlFor="inputWidth">
            width
          </label>
          <input
            id="inputWidth"
            className="form__input"
            type="number"
            defaultValue="100"
            min="0"
            max="1000"
            ref={refInputWidth}
            onInput={updateWidth}
          />
          px
        </div>
        <div>
          <label className="form__label" htmlFor="inputHeight">
            height
          </label>
          <input
            id="inputHeight"
            className="form__input"
            type="number"
            defaultValue="100"
            min="0"
            max="1000"
            ref={refInputHeight}
            onInput={updateHeight}
          />
          px
        </div>
      </div>
      <div style={divStyle}></div>
    </div>
  );
};

map 関数と createRef メソッドを併用して、複数の ref オブジェクトを作る

React のフックは、 if 文や for ループの中で使えません。また、 map 関数の引数のように、コールバック関数として扱うことも出来ません。

そこで、 React のクラスコンポーネントで活躍している createRef メソッドを使うことで、 ref オブジェクトを複製する方法を採用します。


ショウヘイ

map 関数の中で createRef メソッドを実行して、複数の ref オブジェクトを作成しました。 

ref オブジェクトの受け皿として、『状態を保持できて、かつレンダリング対象にならない』という特徴のある useRef フックを使っています。

ふーちゃん

ボタンをクリックすると、「クリックしたボタン:」の右横に、ボタンのインナーテキストが表示される仕様になっていますよ!

import React, { useState, useRef } from "react";

export const Component: React.VFC = () => {
  const [word, setWord] = useState<string>("なし");
  const list: string[] = ["りんご", "みかん", "ぶどう"];
  const refs = useRef(list.map(() => React.createRef<HTMLButtonElement>()));

  return (
    <div>
      <p>クリックしたボタン:{word}</p>
      {refs.current.map((ref, i) => (
        <button
          className="mr-2"
          style={{ marginRight: "10px" }}
          ref={ref}
          key={i}
          onClick={() => {
            if (ref.current) setWord(ref.current.innerText);
          }}
        >
          {list[i]}
        </button>
      ))}
    </div>
  );
};
この記事を知り合いに共有する
URLをコピーする
URLをコピーしました!
目次
閉じる