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);
一般的なユースケースは、要素の参照を取得しボタンがクリックされるたびにその要素にフォーカスすることです。
下記のアプローチは、単純に見えるかもしれませんが型や関数コンポーネントに慣れていない人にとっては難しいと感じるかもしれません。
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()のクリーンアップ関数がタイマーを停止させることになります。
最後に
ReactとTypeScriptを使い始めたとき、変更可能な参照と不変の参照の違いが非常にわかりにくいと思います。
ですが、結局のところuseRefは要素のref属性を利用してHTML要素として使われるのか、Reactの再レンダリングを引き起こさない状態を追跡するためのインスタンス変数として使われるかのどちらかです。
参照の更新は、状態の更新とは対照的にコンポーネントの再レンダリングをトリガーしません。
参照はDOM要素にもアクセス可能ですが、TypeScriptにはDOM要素の独自の型が組み込まれています。
本日は以上となります。
最後まで読んで頂きありがとうございます。
当記事がお役に立ち、いくつかの疑問が解消されたことを願っております。