👀

focus / blur は地味だけど偉い ― 『入力おわり』を察する仕組みで、自動保存に挑戦した話

に公開

はじめに

こんにちは、YSです。
今日はたぶん 需要薄めの話 を書きます。focusblur です。

派手な機能ではないですが、最近これを真面目に使う機会があって、「地味だけど、設計次第で UX が結構変わるな」 と思ったので整理しておきます。


focus と blur とは

  • focus: inputtextareaフォーカスを得て、入力可能になった状態
  • 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つだけ:

  1. onChange はローカル state の更新だけ
  2. onBlur最後に保存した値 と比較し、差分があれば PUT
  3. 成功したら lastSavedRef を更新

やってみて難しかったところ

実装してみて、正直 「onBlur に乗せるだけで終わると思っていたら全然終わらなかった」 となりました。詰まったのは主に3点。

1. 概念の理解

そもそも「focus / blur というイベントがある」「React では onFocus / onBlur で拾える」という前提を、ちゃんと頭に入れるのに時間がかかりました。
ただのフォーム周りの話に見えて、「いつ値を確定したとみなすか」 という設計判断が絡んでくるので、単に API を覚えるだけでは足りない感じです。

2. コードの書き方

「どこに何を書くか」で迷いました。たとえば:

  • 現在値は useState でいいとして、"最後に保存した値" はどこに持つか
  • useState に持つと再レンダーを引き起こすので、useRef が向いている という選択
  • onChangeonBlur の責務をどこで切るか

API を叩くタイミング・比較用の値の置き場所・表示用 state の3つを、それぞれ別のものとして扱えるかどうか が鍵で、ここをサボると一瞬で絡まります。

3. API との連携

最後に、フロントで値が確定したあと API 側とどう噛み合わせるか

  • PUT で 全体を送る のか、差分だけ送る のか
  • 失敗したときに「保存失敗」と出すだけでいいのか、再試行するのか
  • 保存中に次のフィールドに移って編集したら、どのリクエストを優先するのか

フロント単体では解決できず、バックエンドの設計と一緒に考えないと決まらない 論点が多く、このあたりで一番時間を使いました。


おわりに

focusblur は、地味ながら「値の確定を察する」という重要な仕事を担うハンドラでした。
派手な hooks の影に隠れがちですが、フォームを作るたびに登場する常連なので、
地味な良い人 くらいの気持ちで付き合っていくと、そのうち救われる日が来る気がします。

Discussion