deve.K

エンジニアが未来を切り開く。

ReactとTypeScript useRefフックの型指定と使い方

ReactとTypeScriptのuseRefフック

useRefフックを使用してrefを宣言するさまざまな方法が、現在のrefプロパティの不変性にどのような影響を与えるかを学びます。

当記事では、currentプロパティを不変にする方法、可変にする方法を紹介します。

useRefフックとは?

useRefフックは他のフックでは解決できない、いくつかの問題を解決するために使用できる便利なReact フックの1つです。

Reactでは、レンダリングに使用するデータは不変です。

状態の一部の変更は、セッターまたはレデューサーを通じて反映されます。

const App = () => {
    const [name, setName] = useState<string>('');

    return (
        <input
          type="text"
          value={name}
          onChange={e => setName(e.target.value)}
        />
    );
};

これらの変更では、エフェクトの実行、メモ化の無効化、およびコンポーネントの再レンダリングのために観察が可能です。

useRefでは、コンポーネントまたはフックの実行全体で変更可能な変数を初期化し、アクセスできるようにします。

レンダリングのたびに、useRefは1つの引数を取りデータを格納できる同じRefオブジェクトを返すので、可変となります。

つまり、変更可能なrefオブジェクトを返します。

そのオブジェクトは『 .current 』という名前のプロパティを含んでいます。

currentの初期値は、useRefに渡された引数の値です。

引数が存在しない場合、currentプロパティはundefinedに設定されます。

const ref = useRef<number>(0);

return <button onClick={() => ref.current++}/>;

これは、書き換え可能な値を .currentプロパティ内に保持することができる変数のようなものです。

下記では、初期値をnullで含めております。

// 型引数にnullを含める
const ref = useRef<number | null>(null);

const ref = useRef<number>(null);
//読み取り専用

useRefフックを作成するときは、デフォルト値としてnullを渡すことも重要です。

通常refはnullで初期化します、これは後でJSX呼び出しで設定したためです。

useRefフックの初期値をnullにした場合、戻り値のrefオブジェクトは読み取り専用になります

その場合は書き換える事はできません。

つまり、useRefを使用してDOMノードまたは Reactコンポーネントへの参照を格納する場合、開発者は通常、初期値をnullに設定します。

フックを初期化すると、オブジェクトが返されます。

そのオブジェクトには、 currentというプロパティが含まれています。

フックに使用した初期値がそのプロパティの値となります。

useRefフックの最も一般的な用途は次の2つとなります。

レンダリング間で変数に値を保持する

DOM要素への直接アクセス

currentの可変性と不変性は実行時ではなく、型レベルで適用されます。

変数の値を保持するのは、useStateフックと少し似ておりますが重要な違いがあります。

それを明確にしましょう。

useStateとuseRefの違い

useStateとの主な違いは次のとおりです。

状態を更新すると、コンポーネントが再レンダリングされます。

refに格納されている値を更新しても、何も起こりません。

useStateは2つのプロパティまたは配列を返します。

1 つは値または状態で、もう1つは状態を更新する関数です、対照的にuseRefは格納された実際のデータである1つの値のみを返します。

useStateでは、状態またはその値を更新するためにコンポーネントを再度レンダリングする必要があります。

useRefは状態とは異なり、参照または refに格納されたデータまたは値は、コンポーネントの再レンダリング後でも同じままです。

したがって、参照はコンポーネントレンダリングに影響しませんが、状態には影響します。

参照値が変更されると、更新または再レンダリングする必要なく更新されます。

これは、バグではありません機能です。

useRefフックには、DOM要素へのアクセスと変数に値を保持する用途がありますが、どちらの用途にも異なる型が必要となります。

より単純なものから始めて、useRefフックが役立つユースケースを見てみましょう。

useRefでDOMノードの入力方法

useRefフックの不変currentは通常、DOM要素で使用されます。

DOM要素の参照では、次のような便利な事ができます。

要素の高さと幅を取得する

スクロールバーが存在するかどうかを確認する

focus()特定の瞬間に要素を呼び出す

そして、各ネイティブ HTML要素には独自の型定義がございます。

// <div> 参照型
const divRef = useRef<HTMLDivElement>(null);

// <button> 参照型
const buttonRef = useRef<HTMLButtonElement>(null);

// <br /> 参照型
const brRef = useRef<HTMLBRElement>(null);

// <a> 参照型
const linkRef = useRef<HTMLLinkElement>(null);

//<input> 参照型
const inputRef = useRef<HTMLInputElement>(null);

冒頭でも説明した通り、useRefフックを作成するときは、デフォルト値としてnullを渡すことが重要です。

一般的なユースケースは、要素の参照を取得し、ボタンがクリックされるたびにその要素にフォーカスすることです。

下記のアプローチは、単純に見えるかもしれませんが型や関数コンポーネントに慣れていない人にとっては難しいと感じるかもしれません。

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

const App = () => {
 const nameRef = useRef<HTMLInputElement | null>(null);
 const emailRef = useRef<HTMLInputElement | null>(null);


 useEffect(() => {
    nameRef.current?.focus();
  }, []);

  const focusonName = () => {
    nameRef.current?.focus();
  }

  const focusonEmail = () => {
    emailRef.current?.focus();
  }

return (
    <div className="container">
      <h1>useRefでのfocus</h1>
      <input ref={nameRef} type="text" className="input" placeholder="Name" />
      <input ref={emailRef} type="text" className="input" placeholder="Email" />

      <div className="btn-wrapper">
        <button onClick={focusonName} className="btn">Foucs on Name</button>
        <button onClick={focusonEmail} className="btn">Focus on Email</button> 
      </div>
    </div>
  );
};

See the Pen React with Typescript useRefフック focus by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.


名前とメールの情報を受け取るための2つのテキスト入力があり、また2つのボタンがあります。

何もしていないときは、useEffectフックを使用し最初のテキスト入力がフォーカスされます。

ボタンがクリックされると、そのボタンに対応するテキスト入力にフォーカスが当たります。

デモで確認すると、より直感的に理解できるかと思います。

先述した通り、TypeScriptにはDOM要素の型が組み込まれており、それを利用することができます。

これらの型の構造は常に同じで、nameが使用しているHTMLタグの名前であれば、対応する型はHTMLNameElementとなります。

 const nameRef = useRef<HTMLNameElement | null>(null);

入力の場合、型の名前はHTMLInputElementとなります。

nameRef.current?.focus();

上記のように、疑問符 ?を付けていることに注意してください。

これは、TypeScriptではnameRef.currentがnullになる可能性があるためです。

上記では、最初の実行前にReactによって入力されるため、nullにならないことがわかっています。

疑問符 ?を追加することは、TypeScriptがその問題について満足するための最も簡単な方法となります。

useRefで可変値の入力方法

useRefフックを実装するユニークな方法は、DOM 参照の代わりに値を格納するために使用することです。

refのcurrentプロパティを変更可能にするには、ref自体の宣言方法を変更する必要があります。

基本的にはuseStateと同じですが、useRefに値を保持させたいので、その型を指定します。

下記のように、refはすべてのクリック参照を追跡可能です。

カウントの追跡となります。

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

const App = () => {
  const [count, setCount] = useState<number>(0);

  const ref = useRef<number>(0);
  
  const handleCount = (event, num) => {
    ref.current++
    setCount(count + num);
  };

  return (
    <div>
    <button onClick={event => handleCount(event, 1)}>+</button>
      <button onClick={event => handleCount(event, -1)}>-</button>


      <div>Count: {count}</div>
      <div>ボタンを {ref.current} 回クリックした</div>
    </div>
  );
};

See the Pen React with Typescript useRef 可変値の追跡 by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.


refは、Reactの再レンダリングカニズムを使用せずに何らかの状態を追跡する必要があるときはいつでも、Reactの関数コンポーネントインスタンス変数として使用できます。

refをインスタンス変数として使用して値を取得し、イベントと引数を並列に渡しております。

値を状態または変数として格納する代わりに参照が使用されます。

つまり、以前の状態を追跡し参照を保持することです。

インクリメントまたはデクリメントするたびに、ref内にcountの値を格納できます。

タイマーを扱うコンポーネントとしてもuseRefフックは、タイマーへの参照を保持するための理想的な候補となっています。

少し不自然に見えるかもしれませんが、例を示します。

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

const App = () => {
  const [count, setCount] = useState<number>(0);

  const [toggle, setToggle] = useState<boolean>(false);

  const timerRef = useRef<number | null>(null);

  const toggleStop = () => {
    setToggle(!toggle);
  };

  const resetStop = () => {
    setToggle(false);
    setCount(0);
  };

useEffect(() => {
    timerRef.current = setInterval(() => {
      if (toggle) setCount((event) => event + 1);
    }, 1000);

    return () => {
      if (timerRef.current) clearInterval(timerRef.current);
    };
  }, [toggle]);

  return (
    <div>
      <div>Timer: {count}s</div>

      <button onClick={toggleStop}>
        {toggle ? 'Stop' : 'Start'}
      </button>

      <button onClick={resetStop}>Reset</button>
    </div>
  );
};

See the Pen React with Typescript useRef 可変値 timer by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.


下記のようにuseRefが提供された型に属する値で初期化される場合、refのcurrentプロパティは不変から変更可能、つまり可変にする事ができます。

const Ref = useRef<number | null>(null);

useRefフックをnullで初期化し、実際の型に基づいてユニオン型を使用し、型引数にnullを使用する必要があります。

refは、状態が変化しても影響を受けない状態のオブジェクトに基づいて定義できる変数です。

何か他のことをするように指示するまで、その値を保持します。

下記のようにしても同じ事ができます。

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

const App = () => {
const [count, setCount] = useState<number>(0);
const timerRef = useRef<number>(null);

const startHandler = () => {
    if (timerRef.current) { return; }
    timerRef.current = setInterval(() => setCount(e => e + 1), 1000);
  };

const stopHandler = () => {
    clearInterval(timerRef.current);
    timerRef.current = 0;
  };

const resetStop = () => {

    setCount(0);
  };

useEffect(() => {
return () => clearInterval(timerRef.current);
  }, []);

return (
    <div>
<div>Timer: {count}s</div>
      <div>
<button onClick={startHandler}>Start</button>
<button onClick={stopHandler}>Stop</button>
<button onClick={resetStop}>Reset</button>

      </div>
    </div>
  );
};

See the Pen React with Typescript useRef 可変値 timer ② by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.


この場合では、useRefフックの初期値をnullにした場合、戻り値のrefオブジェクトは読み取り専用になるため、エラーをスローします。

currentにtimerRef.current = 0;として代入する事でtimerRefを参照し保持可能にする事が可能です。

const timerRef = useRef<number>(null);

const stopHandler = () => {
    clearInterval(timerRef.current);
    timerRef.current = 0;
  };

startHandler()関数は、Startボタンが押されたときに呼び出されます。

呼び出された後にタイマーを開始し、timerRefを参照し保存します。

ストップウォッチを停止するには、Stopボタンをクリックします。

stopHandler()関数は、参照からtimerRefにアクセスし、タイマーを停止します。

そして、ストップウォッチがアクティブな状態でコンポーネントがアンマウントされると、useEffect()のクリーンアップ関数がタイマーも停止させることになります。

注意点

useRefフックで状態配列を入力するのを忘れた場合、またはフックの戻り値の型を入力しなかった場合に発生するエラーがあります。

『Property does not exist on type 'never'』

"never型にはプロパティが存在しません。"

このエラーを解決するには、Reactアプリケーションでジェネリックを使用し、明示的に状態配列またはrefの参照値を入力するようにしてください。

つまり、ジェネリックを使用して配列を型付けしてあげます。

const [arr, setArr] = useState<string[]>([]);

// object array
const [objArr, setObjArr] = useState<{name: string; age: number}[]>([]);

配列状態変数を入力するときはany型ではなく、より具体的な型を使用してください。

これでエラーが解決する理由は、配列を明示的に型指定しないと、暗黙的にnever[]の型が取得されるためです。

これは事実上、常に空になる配列です。

もう一つ注意すべきことは、これは単なるJavaScriptオブジェクトであるということです。

レンダリングを行わずに、更新して追跡する必要があるものをすべてを格納できます。

最後に

ReactとTypeScriptを使い始めたとき、変更可能な参照と不変の参照の違いが非常にわかりにくいと思います。

ですが、結局のところrefは要素のref属性を利用してHTML要素として使われるのか、Reactの再レンダリングを引き起こさない状態を追跡するためのインスタンス変数として使われるかのどちらかです。

コンポーネントの再レンダリングの間、参照の値は永続的です。

参照の更新は、状態の更新とは対照的にコンポーネントの再レンダリングをトリガーしません。

参照はDOM要素にもアクセス可能ですが、TypeScriptにはDOM要素の独自の型が組み込まれています。

本日は以上となります。

最後まで読んで頂きありがとうございます。

当記事がお役に立ち、いくつかの疑問が解消されたことを願っております。

プライバシーポリシー