初心者にもわかるReact HookのuseRefの使い方

本文書ではReact Hookの中のuseRefの使い方をReact初心者を対象に説明を行っていきます。
useRefの主な利用方法は本文書でも最初に説明を行っているDOMへのアクセスだと思います。Reactのドキュメントでもコード付きで説明されているので非常にシンプルでわかりやすいです。しかしuseRefはuseStateのように変数に値を保持させることも可能です。これについてはドキュメントでもコード入りの説明がないため初心者の人にとってはわかりにくいと思います。本文書ではuseRefでの値の保持の説明についてもシンプルなコードで説明しているのでぜひこの機会にuseRefの使用方法をしっかり理解してください。
input要素のフォーカスに利用
useRefの利用方法することでDOMノード(例:input要素)にアクセスすることが可能です。useRefを使ってinput要素にアクセスすることができるためJavaScriptと同様にfocusメソッドを実行することでinput要素にフォーカスすることが可能です。

コンポーネントにinput要素を追加し、useStateを利用してnameを定義します。input要素に文字を入力するとinput要素の下部に入力した文字列が表示されるシンプルなコードです。
import {useState} from 'react'
function App() {
const [name, setName] = useState('')
const handleOnChange = (e) => setName(e.target.value)
return (
<div style={{'margin':'2em'}}>
<input type="text" value={name} onChange={handleOnChange} />
<p>名前:{name}</p>
</div>
);
}
export default App;

ブラウザで確認するとinput要素は表示されますが、表示されているinput要素にはフォーカスはありません。フォーカスを当てるためには自分でカーソルをinput要素に持っていく必要があります。input要素にフォーカスが当たると文字列を入力することができ、入力した文字はinput要素の下に表示されます。

次にボタンを用意して、ボタンをクリックするとinput要素にフォーカスが当たるようにuseRefを利用します。
input要素を参照するためuseRefを利用してinputElを定義します。useRefの引数には初期値を設定します。ここではnullを設定しています。
import {useState, useRef } from 'react'
//略
const inputEl = useRef(null)
作成したinputElはref属性を使ってinput要素に設定をおこないます。
<input ref={inputEl} type="text" value={name} onChange={handleOnChange} />
ボタンをクリックするとinput要素にフォーカスが当たるようにボタンを追加し、clickイベントを設定します。
<button onClick={handleOnClick}>フォーカスを当てる</button>

ボタンに設定したクリックイベントのhandelOnClickのメソッドの中でカーソルを当てる処理を設定します。
クリックした時にinputElの中身は何が入っているかconsole.log(inputEl)を利用して確認してみましょう。
const handleOnClick = () => console.log(inputEl)
ボタンをクリックするとコンソールにはオブジェクトが表示されcurrentプロパティにinputが入っていることがわかります。

さらにcurrentプロパティの中身を確認してみましょう。
const handleOnClick = () => console.log(inputEl.current)
currentプロパティにはinput要素が入っていることがわかります。

JavaScriptを使って要素に対してfocusメソッドを実行するとその要素に対してフォーカスを当てることができるのでinputEl.currentで取得したinput要素にfocusメソッドを実行します。
const handleOnClick = () => inputEl.current.focus()
ここまでの設定でボタンをクリックするとinput要素がフォーカスされることが確認できます。
このようにuseRefを利用することでref属性で設定した要素の情報を取得できることがわかりました。またinput要素にref属性を設定した場合はfocusメソッドを利用してフォーカスさせることができることも確認できました。

また動作確認の中でuseRefはcurrentプロパティに値が保存されることがわかりました。
import {useState, useRef } from 'react'
function App() {
const inputEl = useRef(null)
const [name, setName] = useState('')
const handleOnChange = (e) => setName(e.target.value)
const handleOnClick = () => inputEl.current.focus()
return (
<div style={{'margin':'2em'}}>
<input ref={inputEl} type="text" value={name} onChange={handleOnChange} />
<p>名前:{name}</p>
<button onClick={handleOnClick}>フォーカスを当てる</button>
</div>
);
}
export default App;
input要素にフォーカスすることができましたがDOMにアクセスできるということはアクセスした要素の情報をgetBoundingClientRectメソッドを利用して取得することができます。
const handleOnClick = () => console.log(inputEl.current.getBoundingClientRect())
handleOnClickメソッドの内容を書き換えてボタンをクリックするとコンソールに要素の情報が表示されます。

useRefを使うことで要素にアクセスすることができるのでstyle属性を使って文字の色を変更するといったことも可能になります。
inputEl.current.style.color='red'
inputのファイル選択ダイアログを表示
フォーカスより実用的な例としてuseRefの使い方を確認します。input要素のファイル選択ボタンではなくアイコンをクリックするとファイル選択のダイアログが表示されるアプリケーションを見かけることがあるかと思います。この機能もuseRefを利用して実装することができます。
function App() {
return (
<div style={{ margin: '2em' }}>
<input type="file" />
</div>
);
}
export default App;
画面にはファイル選択ボタンが表示されクリックするとファイル選択ダイアログが表示されます。

input要素に対してuseRefを設定します。useRefを設定しても何も変換はありません。
import { useRef } from 'react';
function App() {
const inputEl = useRef(null);
return (
<div style={{ margin: '2em' }}>
{/* <div>
<button onClick={() => inputEl.current.click()}>ファイル</button>
</div> */}
<input ref={inputEl} type="file" />
</div>
);
}
export default App;
新たにボタンを追加してclickイベントを設定してクリックを押すとinputEl.current.click()を実行します。
import { useState, useRef } from 'react';
function App() {
const inputEl = useRef(null);
return (
<div style={{ margin: '2em' }}>
<div>
<button onClick={() => inputEl.current.click()}>ファイル</button>
</div>
<input ref={inputEl} type="file" />
</div>
);
}
export default App;
ファイルボタンを押してもファイル選択ボタンを押してどちらでもファイル選択ダイアログが開くようになります。ここではファイルボタンにしていますがアイコンなどに変更しても動作は変わりません。

ファイル選択ボタンは必要ないので画面上から非表示にします。input要素にhiddenを設定することで非表示にすることができます。
<input ref={inputEl} type="file" hidden />


選択したファイルの情報についてはinput要素にonChangeイベントを設定してeventから取得することができます。
import { useState, useRef } from 'react';
function App() {
const inputEl = useRef(null);
const selectedFile = (e) => {
console.log(e.target.files);
};
return (
<div style={{ margin: '2em' }}>
<div>
<button onClick={() => inputEl.current.click()}>ファイル</button>
</div>
<input ref={inputEl} type="file" hidden onChange={selectedFile} />
</div>
);
}
export default App;
ファイルを選択ダイアログから選択してブラウザのデベロッパーツールのコンソールを確認すると選択したファイルの情報を取得することができます。

eventを利用せずにinput.current.filesでもファイルの情報を取得することができます。
useRefに値を保持する
useRefはuseStateと同様に値を保持することができます。useRefは値を保持することが可能ですがuseStateとの違いは値を更新してもコンポーネントの再描写を行いません。
再描写(Re-render)とは何か?
useStateの値を更新すると再描写を行うことがどういうことかまず最初に確認しておきましょう。
useStateでcountを定義し、ボタンをクリックするとcountの値が1増えるコードを記述します。再描写されているか確認するためにconsole.log(‘再描写’)をコードに入れておきます。
import {useState} from 'react'
function App() {
const [count, setCount] = useState(0)
const handleOnClick = () => setCount(count + 1)
console.log('再描写');
return (
<div style={{'margin':'2em'}}>
<div>{count}</div>
<button onClick={handleOnClick}>Countアップ</button>
</div>
);
}
export default App;
ブラウザで表示すると下記のように表示されます。”Countアップ”ボタンをクリックして再描写が行われているか確認します。ボタンを押す度に再描写のメッセージがコンソール上に表示され再描写が行われていることが確認できます。

ここまでの動作確認でuseStateでcountの値を更新すると再描写が行われていることがわかりました。
useRefでは再描写しないとは
先ほどの動作確認ではuseStateで定義した変数を更新をすると再描写が行われることが確認できました。ここではuseRefで定義した変数を更新しても再描写が行われないことを確認します。
useRefによって新たにcountRefを定義し、ボタンをクリックするとcountRefの値が更新されるようにします。
import {useState, useRef} from 'react'
function App() {
const [count, setCount] = useState(0)
const countRef = useRef(0)
const handleOnClick = () => setCount(count + 1);
const handleOnClick2 = () => countRef.current++;
console.log('再描写');
return (
<div style={{'margin':'2em'}}>
<div>{count}</div>
<button onClick={handleOnClick}>Countアップ</button>
<div>{countRef.current}</div>
<button onClick={handleOnClick2}>Count2アップ</button>
</div>
);
}
export default App;
useRefを利用して初期値を0にしています。ここでinput要素を利用した時に思い出して欲しいのがuseRefを利用するとcurrentプロパティに値を保持するということです。初期値を0に設定したということはcountRef.currentの値を0に設定したことになります。
更新もcountRef.currentでおこない、表示したい値もcountRef.currentで表示させることができます。
実際にブラウザ上でcount2アップボタンを3回クリックし、countアップボタンを1回クリックします。
どうなると思いますか?
Count2アップボタンを3回押しましたがブラウザ上にもコンソール上にも何も変化はありません。Count2アップボタンのclickイベントに設定したhandleOnClick2が正常に動作していないのではと不安になるかもしれません。

次にCountアップボタンを押してください。Countアップボタンを押すとsetCountメソッドでcountが1更新されコンポーネントの再描写が行われます。そのためコンソールには再描写が表示されます。先ほどまで何回押しても更新されなかったcountRef.currentの値が再描写と同時に更新され、これまでボタンを押した回数が表示されます。

Count2アップボタンでcountRef.currentが更新されていなかったわけではなくcountRef.currentが更新されていたがコンポーネントの再描写が行われないためブラウザ上に反映されていかなっただけだったのです。
この動作確認からuseRefで定義した変数を更新してもuseStateとは異なりコンポーネントの再描写が行われないということが理解できたかと思います。
useStateとuseRefの違いもわかったと思うのでその違いを理解した上でuseRefを利用する必要があります。