本文書ではReact Hookの中のuseRef Hookの使い方をReact初心者を対象に説明を行っていきます。

useRef Hookの主な利用方法は本文書でも最初に説明を行っているDOMの参照だと思います。Reactの公式のドキュメントでもコード付きで説明されているので非常にシンプルでわかりやすいです。DOMへの参照以外の利用方法としては値の保持です。useRefはuseStateのように値を保持することができます。useStateで値が保持できるのになぜuseRefを利用するのかという疑問もあるかと思います。その疑問に対しても読み進めることで理解することができます。useRefによる値の保持についてはドキュメントでもコード入りの説明がないため初心者の人にとってはわかりにくいと思います。本文書ではuseRefでの値の保持の説明についてもシンプルなコードで説明しているのでぜひこの機会にuseRefの使用方法をしっかり理解してください。

input要素のフォーカスに利用

useRefの利用方法することでDOMを参照することができ、直接DOMノード(例:input要素)にアクセスすることが可能です。useRefを使ってinput要素にアクセスすることができるためJavaScriptで要素を扱う時と同様の方法で操作することができます。useRefを使ってinput要素への参照を行い、focusメソッドを実行することでinput要素にフォーカスすることが可能です。

idにmyTextFiledを持つiinput要素にフォーカスしたい場合、JavaScriptではdocument.getElementById(‘myTextField’).focus()で行うことが可能です。これと同様なことをReact上で行いたい場合にuseRefを利用することができます。

コンポーネントに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要素にフォーカスが当たると文字列を入力することができ、入力した文字はinput要素の下に表示されます。

入力した文字がinput要素の下に表示される
入力した文字がinput要素の下に表示される

次にボタンを用意して、ボタンをクリックするとinput要素にフォーカスが当たるようにuseRefを利用します。

useRefを利用してinput要素の参照を取得するためにはseRefを利用してinputElを定義します。useRefの引数には初期値を設定します。ここではnullを設定します。値はinputElそのものに入るのではなくinputElオブジェクトが持つcurrentプロパティに設定されます。下記の設定を行うとinputEl.currentの値がnullとなります。


import {useState, useRef } from 'react'
//略
const inputEl = useRef(null)

作成したinputElはref属性を使ってinput要素に設定をおこないます。この設定によりuseRefで設定したinputElとinput要素が紐づきます。


<input ref={inputEl} type="text" value={name} onChange={handleOnChange} />

ボタンをクリックするとinput要素にフォーカスが当てることができるようにボタンにclickイベントを設定します。


<button onClick={handleOnClick}>フォーカスを当てる</button>
ボタンを追加
ボタンを追加

ボタンに設定したクリックイベントのhandelOnClickのメソッドの中でinput要素にカーソルを当てる処理を設定します。

クリックした時にinputElの中身に何が入っているかconsole.log(inputEl)を利用して確認してみましょう。


const handleOnClick = () => console.log(inputEl)

ボタンをクリックするとコンソールにはオブジェクトが表示されcurrentプロパティにinputが入っていることがわかります。

inputElの中身を確認
inputElの中身を確認

さらにcurrentプロパティの中身を確認してみましょう。


 const handleOnClick = () => console.log(inputEl.current)

currentプロパティにはinput要素が入っていることがわかります。初期値はnullでしたが要素に変わっています。

input要素が表示
input要素が表示

currentプロパティに入っているのはinputタグの文字列ではなく参照なのでinput要素に対してfocusメソッドを実行するとその要素に対してフォーカスを当てることができます。

handleClick関数の中でinputEl.currentで取得したinput要素にfocusメソッドを実行します。


 const handleOnClick = () => inputEl.current.focus()

ここまでの設定でボタンをクリックするとinput要素がフォーカスされることが確認できます。

useRefを利用してinput要素がフォーカスされる
useRefを利用してinput要素がフォーカスされる

このようにuseRefを利用することでref属性で設定した要素への参照を取得できることがわかりました。要素の参照を利用することで要素に対して直接focusメソッドを利用してフォーカスさせることができることも確認できました。


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;

参照を利用してDOMにアクセスできるということはアクセスした要素の情報をgetBoundingClientRectメソッドを利用して取得することができます。


const handleOnClick = () => console.log(inputEl.current.getBoundingClientRect())

handleOnClickメソッドの内容を書き換えてボタンをクリックするとコンソールに要素の情報が表示されます。

getBoundingClientRectで取得した値
getBoundingClientRectで取得した値

useRefを使うことで要素にアクセスすることができるのでstyle属性を使って文字の色を変更するといったことも可能になります。


inputEl.current.style.color='red'

inputのファイル選択ダイアログを表示

input要素へのフォーカスとは異なる例を利用してさらにuseRefの使い方を確認します。

input要素のファイル選択ボタンではなくアイコンをクリックするとファイル選択のダイアログが表示されるアプリケーションを見かけることがあるかと思います。この機能もuseRefを利用して実装することができます。


function App() {
  return (
    <div style={{ margin: '2em' }}>
      <input type="file" />
    </div>
  );
}

export default App;

画面にはファイル選択ボタンが表示されクリックするとファイル選択ダイアログが表示されます。

input要素にtypeをfileに設定
input要素にtypeをfileに設定

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でもファイルの情報を取得することができます。input要素へのフォーカス以外の例を確認しましたがuseRefを利用するとDOMにアクセスすることができるので他にもさまざまなことに活用できると思います。

useRefに値を保持する

ここまではuseRefの要素への参照の機能を確認してきましたがuseRefのもう一つの利用方法である値の保持について動作確認を行なっていきます。

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の値が更新されるようにします。useStateを利用したCountアップボタンとuseRefを利用したCount2アップボタンを設定しています。


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が正常に動作していないのではと不安になるかもしれません。

useRefの動作確認
useRefの動作確認
最初の1回に表示される再描写は無視してください。

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

useStateで再描写
useStateで再描写

Count2アップボタンでcountRef.currentが更新されていなかったわけではなくcountRef.currentが更新されてはいたもののコンポーネントの再描写が行われないためブラウザ上に反映されていかなっただけだったのです。また再描写は更新した値を表示させるために必要な処理であることも理解できました。もしuseStateで値を更新しても再描写が行われないのであればブラウザ上には更新された値は表示されないということになります。

この動作確認からuseRefで定義した変数を更新してもuseStateとは異なりコンポーネントの再描写が行われないということが理解できたかと思います。

フォームでのuseStateとuseRef

useStateを利用して入力フォームを作成することができますがuseRefでも入力フォームを作成することができます。最初にuseStateでの入力フォームの作成方法を確認してその後useRefでの作成方法を確認します。

useStateでの方法

入力した値を保持するためにuseStateでemailとpasswordを定義します。

input要素を利用してemailとpasswordの入力欄を追加しonChangeイベントを設定して入力が行われるとhandleChangeEmail/handleChangePasswordが実行されemailとpasswordに入力した値が保存されます。ログインボタンを押すとhandleSubmit関数が実行されコンソールに入力した値が表示されます。


import { useState } from 'react';
import './App.css';

function App() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(`emai:${email}, password:${password}`);
  };

  const handleChangeEmail = (e) => {
    setEmail(e.target.value);
  };
  const handleChangePassword = (e) => {
    setPassword(e.target.value);
  };
  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" value={email} onChange={handleChangeEmail} />
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input
            id="password"
            value={password}
            onChange={handleChangePassword}
            type="password"
          />
        </div>
        <div>
          <button>ログイン</button>
        </div>
      </form>
    </div>
  );
}

export default App;

useRefでの方法

上記のコードをuseStateからuseRefに書き換えます。useRefでemailRefとpasswordRefを定義しinput要素のref属性に設定を行っています。input要素に入力した値はemailであればemailRef.current.valueで取得することができます。


import { useRef } from 'react';
import './App.css';

function App() {
  const emailRef = useRef(null);
  const passwordRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(
      `emai:${emailRef.current.value}, password:${passwordRef.current.value}`
    );
  };

  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" ref={emailRef} />
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input id="password" ref={passwordRef} type="password" />
        </div>
        <div>
          <button>ログイン</button>
        </div>
      </form>
    </div>
  );
}

どちらも画面上に違いはなくemailにjohn@example.com、passwordにpasswordを入力すると同じ値がコンソールに表示されます。

外側からでは違いはわかりませんがすでに確認している通りuseStateを利用した場合は文字を入力する度に”再描写”が行われます。

emailはuseRef, passwordはuseStateで設定を行うと違いがはっきりとわかります。


import { useState, useRef } from 'react';
import './App.css';

function App() {
  const emailRef = useRef(null);
  // const passwordRef = useRef(null);
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(`emai:${emailRef.current.value}, password:${password}`);
  };

  const handleChangePassword = (e) => {
    setPassword(e.target.value);
  };
  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" ref={emailRef} />
          {emailRef.current && <div>{emailRef.current.value}</div>}
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input
            id="password"
            value={password}
            onChange={handleChangePassword}
            type="password"
          />
          <div>{password}</div>
        </div>

        <div>
          <button>ログイン</button>
        </div>
      </form>
    </div>
  );
}

export default App;

emailを入力しても何も表示されませんがpasswordを入力した瞬間にemail.current.valueに保存されていた値が再描写により表示されます。passwordは文字を入力する度に再描写が行われるので入力した値がすぐに表示されます。

コンポーネント内で値を保持したいがブラウザ上に更新した内容をリアルタイムで表示させる必要がない場合などにuseRefを利用できるということが本文書の動作確認からわかります。

useStateとuseRefの違いもわかったと思うのでその違いを理解した上でuseRefを利用する必要があります。