本文へスキップ
useRef

Part 6 · 副作用 (useEffect)

38 / 64

useRef

覚えておきたいけれど、画面には関係ない値

ここまで、覚えておきたい値は全部 useState に入れてきました。

ですが覚えておきたいけれど、画面には関係ない値 というものがあります。 それを useState に入れると、意味のない描き直しが起きます

そういうときに使うのが useRef です。 出番は多くありませんが、知らないと既存のコードが読めません

1. 実物の要素に触りたいとき

いちばんよく見るのがこれです。入力欄にカーソルを移したい、というやつです。

React では画面を直接いじらないのが原則でした。 ですが「カーソルを移す」は画面の見た目ではなく、操作です。 state では表現できません。

const inputRef = useRef<HTMLInputElement>(null);

<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>移す</button>
入力欄にカーソルを移す

押すと入力欄が光ります。数字は増えません

数字が増えていないことに注目してください。カーソルは移ったのに、描き直しは起きていません。

?. は「中身があれば」という意味です。 画面に出る前は null なので、この書き方で守ります。

focus-input.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useRef } from "react";

export function FocusInput() {
  useTrackDemoRender();

  // 入れ物を用意する。中身は { current: ... } の形
  const inputRef = useRef<HTMLInputElement>(null);

  return (
    <div className="flex flex-col gap-3">
      {/* ref を渡すと、React が実物の要素を入れておいてくれる */}
      <Input ref={inputRef} placeholder="ここに入力" aria-label="ここに入力" />

      <Button
        size="sm"
        variant="outline"
        // .current に入っているのは、本物の <input> 要素
        onClick={() => inputRef.current?.focus()}
      >
        入力欄にカーソルを移す
      </Button>
    </div>
  );
}

2. 覚えたいけれど、画面には出さないとき

useRef は要素専用ではありません。ただの値も入れられます

そして useState との違いは、たったひとつです。

state と ref を並べる

ref 側を連打してから、state 側を 1 回押す

state: 0

ref: 0

「ref を増やす」を何回押しても、表示は変わりません。 そのあと「state を増やす」を押すと、まとめて追いつきます

「ref を増やす」を何回押しても、表示が変わりません。値は増えているのに、React が描き直していないからです。

そのあと「state を増やす」を 1 回押すと、ref のほうもまとめて追いつきます。 描き直しのついでに、いまの値が読まれたからです。

ref-vs-state.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useRef, useState } from "react";

/*
  「ref を画面に出すとどうなるか」を見せるためのファイル。
  lint は「描き直しの最中に ref を読むな」と正しく止めてくる。
  止めてくれること自体がこの章の教材なので、ここだけ黙らせている。
*/
/* eslint-disable react-hooks/refs */

export function RefVsState() {
  useTrackDemoRender();

  const [stateCount, setStateCount] = useState(0);

  // ref は書き換えても描き直しが起きない
  const refCount = useRef(0);

  return (
    <div className="flex flex-col gap-3">
      <div className="flex gap-2">
        <Button size="sm" onClick={() => setStateCount((current) => current + 1)}>
          state を増やす
        </Button>
        <Button
          size="sm"
          variant="outline"
          onClick={() => {
            refCount.current += 1;
          }}
        >
          ref を増やす
        </Button>
      </div>

      <div className="rounded-md border p-3 font-mono text-sm">
        <p>state: {stateCount}</p>
        {/* ref を変えても描き直されないので、この表示は古いまま */}
        <p>ref: {refCount.current}</p>
      </div>

      <p className="text-sm text-muted-foreground">
        「ref を増やす」を何回押しても、表示は変わりません。
        そのあと「state を増やす」を押すと、まとめて追いつきます
      </p>
    </div>
  );
}

使いどころ

useRef の出番は、はっきりしています。

  • 実物の要素に触る カーソルを移す、スクロール位置を測る、動画を再生する
  • 片付けるために覚えておく タイマーの番号。effect の中で始めて中で止めるなら変数で足りますが、 別の場所から止めたいときは ref に入れます
  • 前回の値を覚えておく 「増えたのか減ったのか」を知りたいとき
  • 数えるだけで、表示しない この教材のデモカードが、まさにこれです

逆に、この 4 つに当てはまらないならuseRef は要りません。ふだんは useState だけで足ります。

ref-vs-state.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useRef, useState } from "react";

/*
  「ref を画面に出すとどうなるか」を見せるためのファイル。
  lint は「描き直しの最中に ref を読むな」と正しく止めてくる。
  止めてくれること自体がこの章の教材なので、ここだけ黙らせている。
*/
/* eslint-disable react-hooks/refs */

export function RefVsState() {
  useTrackDemoRender();

  const [stateCount, setStateCount] = useState(0);

  // ref は書き換えても描き直しが起きない
  const refCount = useRef(0);

  return (
    <div className="flex flex-col gap-3">
      <div className="flex gap-2">
        <Button size="sm" onClick={() => setStateCount((current) => current + 1)}>
          state を増やす
        </Button>
        <Button
          size="sm"
          variant="outline"
          onClick={() => {
            refCount.current += 1;
          }}
        >
          ref を増やす
        </Button>
      </div>

      <div className="rounded-md border p-3 font-mono text-sm">
        <p>state: {stateCount}</p>
        {/* ref を変えても描き直されないので、この表示は古いまま */}
        <p>ref: {refCount.current}</p>
      </div>

      <p className="text-sm text-muted-foreground">
        「ref を増やす」を何回押しても、表示は変わりません。
        そのあと「state を増やす」を押すと、まとめて追いつきます
      </p>
    </div>
  );
}

理解できたか確かめる

確認クイズ

useState と useRef のいちばんの違いは?

確認クイズ

画面に表示する数を useRef で持つとどうなる?

確認クイズ

useRef が返すのが .current という入れ物なのはなぜ?

ref-vs-state.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useRef, useState } from "react";

/*
  「ref を画面に出すとどうなるか」を見せるためのファイル。
  lint は「描き直しの最中に ref を読むな」と正しく止めてくる。
  止めてくれること自体がこの章の教材なので、ここだけ黙らせている。
*/
/* eslint-disable react-hooks/refs */

export function RefVsState() {
  useTrackDemoRender();

  const [stateCount, setStateCount] = useState(0);

  // ref は書き換えても描き直しが起きない
  const refCount = useRef(0);

  return (
    <div className="flex flex-col gap-3">
      <div className="flex gap-2">
        <Button size="sm" onClick={() => setStateCount((current) => current + 1)}>
          state を増やす
        </Button>
        <Button
          size="sm"
          variant="outline"
          onClick={() => {
            refCount.current += 1;
          }}
        >
          ref を増やす
        </Button>
      </div>

      <div className="rounded-md border p-3 font-mono text-sm">
        <p>state: {stateCount}</p>
        {/* ref を変えても描き直されないので、この表示は古いまま */}
        <p>ref: {refCount.current}</p>
      </div>

      <p className="text-sm text-muted-foreground">
        「ref を増やす」を何回押しても、表示は変わりません。
        そのあと「state を増やす」を押すと、まとめて追いつきます
      </p>
    </div>
  );
}

この章のまとめ

  • useRef変えても描き直しが起きない入れ物
  • 中身は .current にある。 箱ごと渡すから、中身を差し替えられる
  • ref={...} と渡すと、実物の要素が入ってくる
  • 画面に出す値は入れない。表示が古いまま取り残される
  • 迷ったら useState。ref は必要になってから

focus-input.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useRef } from "react";

export function FocusInput() {
  useTrackDemoRender();

  // 入れ物を用意する。中身は { current: ... } の形
  const inputRef = useRef<HTMLInputElement>(null);

  return (
    <div className="flex flex-col gap-3">
      {/* ref を渡すと、React が実物の要素を入れておいてくれる */}
      <Input ref={inputRef} placeholder="ここに入力" aria-label="ここに入力" />

      <Button
        size="sm"
        variant="outline"
        // .current に入っているのは、本物の <input> 要素
        onClick={() => inputRef.current?.focus()}
      >
        入力欄にカーソルを移す
      </Button>
    </div>
  );
}