focus / blur は地味だけど偉い ― 『入力おわり』を察する仕組みで、自動保存に挑戦した話
はじめに
こんにちは、YSです。
今日はたぶん 需要薄めの話 を書きます。focus と blur です。
派手な機能ではないですが、最近これを真面目に使う機会があって、「地味だけど、設計次第で UX が結構変わるな」 と思ったので整理しておきます。
focus と blur とは
-
focus:
inputやtextareaが フォーカスを得て、入力可能になった状態 - blur: そこから フォーカスが外れた状態
blur は英語で「ぼかし」の意味らしいです。フォーカスが合ってない=ぼやけている、というイメージっぽい。命名が意外と風情があります。
React だとこう書きます:
<input
onFocus={() => console.log("入りました")}
onBlur={() => console.log("出ていきました")}
/>
onBlur は 「入力おわったっぽいな」のタイミング をブラウザが教えてくれる仕組みです。人間で言うと、誰かが机を離れたのを自動で見てくれる機能。地味に便利です。
何が嬉しいのか
一番の使いどころは 「値が確定したタイミングでサーバーに伝えたい」 ときです。
onChange で逐一送るとサーバーに怒られるし、保存ボタンを置くとユーザーに手間がかかる。
onBlur ならその 中間の間合い が取れます。Google スプレッドシートや Notion の「セルから離れた瞬間に保存されました」というあの挙動は、たぶんこれの親戚です。
やってみる: onBlur で PUT を叩いて差分更新
サンプルのメモ欄を自動保存する 想定で書いてみます。
"use client";
import { useState, useRef } from "react";
type Sample = { id: string; note: string };
export function SampleNoteEditor({ initial }: { initial: Sample }) {
const [note, setNote] = useState(initial.note);
const lastSavedRef = useRef(initial.note);
const [status, setStatus] = useState<"idle" | "saving" | "saved" | "error">(
"idle",
);
const handleBlur = async () => {
if (note === lastSavedRef.current) return; // 差分なしなら何もしない
setStatus("saving");
try {
const res = await fetch(`/api/samples/${initial.id}`, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ note }),
});
if (!res.ok) throw new Error("save failed");
lastSavedRef.current = note;
setStatus("saved");
} catch {
setStatus("error");
}
};
return (
<textarea
value={note}
onChange={(e) => setNote(e.target.value)}
onBlur={handleBlur}
/>
);
}
ポイントは3つだけ:
-
onChangeはローカル state の更新だけ -
onBlurで 最後に保存した値 と比較し、差分があれば PUT - 成功したら
lastSavedRefを更新
やってみて難しかったところ
実装してみて、正直 「onBlur に乗せるだけで終わると思っていたら全然終わらなかった」 となりました。詰まったのは主に3点。
1. 概念の理解
そもそも「focus / blur というイベントがある」「React では onFocus / onBlur で拾える」という前提を、ちゃんと頭に入れるのに時間がかかりました。
ただのフォーム周りの話に見えて、「いつ値を確定したとみなすか」 という設計判断が絡んでくるので、単に API を覚えるだけでは足りない感じです。
2. コードの書き方
「どこに何を書くか」で迷いました。たとえば:
- 現在値は
useStateでいいとして、"最後に保存した値" はどこに持つか -
useStateに持つと再レンダーを引き起こすので、useRefが向いている という選択 -
onChangeとonBlurの責務をどこで切るか
API を叩くタイミング・比較用の値の置き場所・表示用 state の3つを、それぞれ別のものとして扱えるかどうか が鍵で、ここをサボると一瞬で絡まります。
3. API との連携
最後に、フロントで値が確定したあと API 側とどう噛み合わせるか。
- PUT で 全体を送る のか、差分だけ送る のか
- 失敗したときに「保存失敗」と出すだけでいいのか、再試行するのか
- 保存中に次のフィールドに移って編集したら、どのリクエストを優先するのか
フロント単体では解決できず、バックエンドの設計と一緒に考えないと決まらない 論点が多く、このあたりで一番時間を使いました。
おわりに
focus と blur は、地味ながら「値の確定を察する」という重要な仕事を担うハンドラでした。
派手な hooks の影に隠れがちですが、フォームを作るたびに登場する常連なので、
地味な良い人 くらいの気持ちで付き合っていくと、そのうち救われる日が来る気がします。
Discussion