おじんブログ

Webアプリに関する知見とか雑記です

useRefを実践例を挙げながら使う

React Hooks で追加されたuseRefは最初は使いどころがよく分からなかったのですが、実践例を見つつサンプルを書いてみたら使いどころが掴めてきたのでまとめます。

前提

ReactのuseStateuseEffectの使い方は覚えたけどuseRefについてはあまり公式例を見てもイメージがつかない人向けです。

サンプルコードはnpx create-react-app my-app --template typescriptで作成して、App.tsxに書いています。

https://create-react-app.dev/docs/adding-typescript

.current を利用して初回レンダーのuseEffect()を早期returnで終了させる

useRefには.currentプロパティがありますが、これはコンポーネントのレンダー時に毎回セットした初期値を返します。 Reactにとってのインスタンス変数のように扱えます。 この性質を利用して初期描画時にフラグを立てて、useEffectのマウント時の初回実行を早期returnで終了させることができます。 これによってStateが更新された時のみuseEffectに設定した関数を実行、というのが実現できます。

下記の例はFooコンポーネント内のstateが更新された時のみuseEffectに設定した関数を実行します。 ブラウザ上のconsole画面を見ながらボタンを押していくと分かりやすいです。

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

function App() {
  return (
    <div>
      <Foo />
    </div>
  );
}

function Foo() {
  const [value, setValue] = useState(0);

  const isFirstRender = useRef(true);

  const addValue = () => {
    setValue(value + 1);
  };

  const sameValue = () => {
    setValue(value);
  };

  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false;
      return;
    }
    console.log("updated");
  }, [value]);
  return (
    <>
      <h3>{`Now state : ${value}`}</h3>
      <button onClick={addValue}>+1</button>
      <button onClick={sameValue}>don't change</button>
    </>
  );
}

export default App;

isFirstRender.currentによる初回実行時の早期returnがない場合、初回マウント時にconsole.log("updated");が実行されてしまいます。

  useEffect(() => {
    // block exec when first render
    if (isFirstRender.current) {
      isFirstRender.current = false;
      return;
    }
    console.log("updated");
  }, [value]);

createRefのHooks版として使用してrefオブジェクトにアクセスする

これは公式でも触れられていたものです。これはcreateRef()でも実現できます。

https://ja.reactjs.org/docs/hooks-reference.html#useref

ただ、Hooks時代のReactから入門した人からするとこの説明だけだとfocus()だけなのか?になるので、深掘りして行きます。

公式ドキュメントのRef と DOMの項を見るとこう書いてあります。

いつ Ref を使うか

Ref に適した使用例は以下の通りです。

  • フォーカス、テキストの選択およびメディアの再生の管理
  • アニメーションの発火
  • サードパーティの DOM ライブラリとの統合

https://ja.reactjs.org/docs/refs-and-the-dom.html

フォーカス、テキストの選択とあるのでサンプルコードを書いてみます。 この例ではinputタグの値をコピーしてクリップボードに保存する処理になります。 ここではtargetRef.currentのオブジェクトはHTMLInputElement型なので、HTMLInputElementのオブジェクトが使えます。

import React, { useRef } from "react";

function App() {
  const targetRef = useRef<HTMLInputElement>(null);

  const copyFromRef = () => {
    if (targetRef) {
      targetRef.current?.focus();
      targetRef.current?.select();
      document.execCommand("copy");
      targetRef.current?.blur();
    }
  };

  return (
    <div>
      <button onClick={copyFromRef}>Copy input value</button>
      <input ref={targetRef} type="text" defaultValue="copy to clipboard" />
    </div>
  );
}

export default App;

例えば、会員登録の入力フォームにて、ボタンを押したら自動的に最初の入力項目にフォーカスするようにしてみたい場合を考えます。 その場合はPropsとしてRefオブジェクトを受け取ってフォーカスするボタンコンポーネントを設計した方が、取り回しが良いと思います。 その場合のサンプルコードは下記です。TypeScriptでの型情報を加えると下記の通りです。

import React, { useRef } from "react";

function App() {
  const targetInputRef = useRef<HTMLInputElement>(null);

  return (
    <div>
      <FocusButton target={targetInputRef} />
      <input ref={targetInputRef} type="text" defaultValue="focus" />
    </div>
  );
}

interface FocusButtonType {
  target: React.RefObject<HTMLInputElement>
}
function FocusButton(props: FocusButtonType) {
  const focusToTarget = () => {
    if(props.target) {
      props.target.current?.focus()
    }
  }
  return (
    <button onClick={focusToTarget}>Focus!</button>
  )
}

export default App;

アニメーションの発火の例では、こちらが参考になります。

CSSアニメーションの制御を行っています。

PIXIV TECH FES.のLPを支えるCSSアニメーションテクニック

https://inside.pixiv.blog/2020/01/21/180000

まとめ

最初にuseRefを見たときはユースケースが思い浮かばず、必要性を感じなかったのですが、実践例を見てイメージがついて行きました。 createRef()でできたことは置き換えられるみたいなので、Hooks時代から入った人はその辺りを見ながら参考にするといいかもしれません。

参考リンク

Make React useEffect hook not run on initial render

https://stackoverflow.com/questions/53253940/make-react-useeffect-hook-not-run-on-initial-render

useRef は何をやっているのか

https://qiita.com/uhyo/items/246fb1f30acfeb7699da#useref

HTMLInputElement(MDN)

https://developer.mozilla.org/ja/docs/Web/API/HTMLInputElement