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 との違いは、たったひとつです。
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>
);
}