Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

【考察】use APIの内部構造を予想してみた

※この記事はuse APIへの自分の理解と、実際にコードを起こして実験した結果を記しただけの 考察記事 です。正しさは保証されていません。「多分こういうマインドセットを持ってるとええんやな〜」くらいの温度感でお楽しみください。

導入: Throw a Promiseテクと use API はどういう関係なのか?

react 19から導入されたuse APIによって、reactは自然にPromiseを扱えるようになりました。 しかし、react18まではPromiseをthrowすることでレンダリングを中断するやり方も提供されていました。2つはユースケースがとても似通っていて混乱します。

このユースケースが似た2つ(use APIとThrow a Promise)の関係は、uhyo さんが記事でわかりやすくまとめています。

ちなみに、Suspenseを前提とする以上、useの中身は良く知られた「Promiseをthrowする」実装になっています。ただし、useは生でPromiseをthrowするのではなく、Suspense ExceptionというErrorオブジェクトでラップしてthrowするようです。これは開発者をなるべく混乱させないための配慮でしょう。

use API|React 19の新機能まるわかり

つまり、Throw a Promise を使った洗練された API が use API とのことです。また、「"Throw a Promise"をdeprecatedにしてuse APIを推奨していこう」というissueも立てられています (当該issue)。このことからも、今後は開発者が Throw a Promise を自らする必要はなくなっていくと思われます。

でも、use API が内部で Promise を throw しているとはどういうことなんでしょうか? 実際に書けるんでしょうか? そう思ってトライしてみたのが本記事になります。

use API を自作してみる

use API の要件としては以下になるはずです。

  • Promiseを受け取る
  • Promiseがresolveしたらその値を返す
  • PromiseがまだresolveされてなかったらPromiseをthrowする

これを非async関数で実装するには、resolveされた値を保存する記憶領域を外部に確保する必要があります。幸いにもJSにはWeakMapというものがあるので、それを使いましょう。

const dataMap = new WeakMap(); // resolveされた値を保存しておく場所
const use = (promise) => {
  promise.then((data) => {
    dataMap.set(promise, data); // resolveされたら保存する
  });
  const data = dataMap.get(promise);
  if (!data) throw promise; // dataがまだない=resolveされてなかったらpromiseをthrowする
  return data; // dataがある=resolve済みの場合はdataを返す
};

これを使って実際に実装してみたコードがこちらになります。

ちゃんとreact19のuse APIと同じような動きが実装できています。

(uhyoさんがいってる「Suspense Exceptionでラップしてる」の部分はよくわかりませんでした。有識者求ム)

まとめ: 今後は use API だけ使えば良さそう

ここまでの実験によって、throw a Promiseによってレンダリングの中断・再開を実装するテクニックは、use API によってより洗練されたインターフェイスとなったことが分かりました。

use API で大体のユースケースはカバーできると思うので、今後は基本的に use API だけ使えば良さそうです。

Uターンして2ヶ月経ったので所感とか

書いた気がしてたけど書いてなかった。2ヶ月経ったのでまとめてみる。

気をつけた方が良い点

  • 移住支援金はとにかくめんどい
    • 移住後にしか申し込めない
      • なので引越し費用には使えない
      • (なら移住支援金じゃなくて移住お祝い金だろ)
    • 条件がややこしいしわかりづらい。当てはまってるかはよく確認する必要がある。
    • 転職後に移住する必要がある
      • 申請後に退職すると(転職含む)返金が求められるため
    • 住民票の除票がどこまで必要か確認する
      • 自分は直近5年以内に住んでた全自治体の除票が必要だった
    • 同じ 市内 に5年以上住む必要がある (他県では違うとかもあるかも)
      • 同じ県内での引越しもアウトらしいので注意
  • 転入届と一緒に印鑑登録をする
    • 車を買うときに必要だから

婚活編

  • マッチングアプリは 思ってたよりは過疎ってない が、でも過疎ってはいる
    • 同じ休日、未婚、タバコ吸わない位のフィルタでも100人程度になるので、いいねがメチャクチャ余る
    • 大体15人くらいとはサクッとマッチした
    • 1ヶ月で10人と会えたので、地方都市なら意外と大丈夫そう
    • 難易度自体は都内とあんま変わらないかも
    • 戻って2週間で付き合い始めたけど、付き合って1ヶ月で振られた。難しいね...
  • デートは難しい
    • さすがに車がないと何もできない
    • お店も少ないので何すればいいかわからん

車編

  • 銀行ローンは転職直後に使えない
    • 銀行ローン(年利2%前後)で買って浮いた金を積立(年利5%前後)に回すのが定石とされている
    • が、銀行ローンは「現職の勤続年数が1年以上」という条件がある。
  • 運転は結構すぐなれる
    • ペーパードライバーで免許取ってから5年以上ほぼ運転してなかったので怖かった
    • が、1ヶ月で800km運転したらだいぶ慣れた
    • バック駐車が苦手だったが、意外と1週間も運転してたら慣れて怖くなくなった
    • 運転は楽しい
  • 収納が少ない車は不便
    • アクアを買ったが、運転席から手を伸ばせる範囲に収納が少なすぎて不便 (ウェットティッシュを置く場所すらない!)
  • 中古車だとエアコンが臭いかも
    • 酢の匂いがめっちゃする...さすがに修理出したい
  • 外に酒を飲みに行けない
    • 帰りに運転できないから
    • 代行という手はあるが、父親から「代行を捕まえられなくて『ちょっとの距離だし自分で運転して帰るか』と言って飲酒運転してしまって捕まる人が多いから代行に頼りきりになるな」と釘を刺された

仕事部屋編

  • 実家から徒歩3分のとこに借りて大正解
    • 荷ほどきの往復が楽
    • 1人になりたい時(電話かける時とか)にサクッとそっちの部屋にいける
    • 仕事と生活で切り替えがしやすい
  • 駐車場はあったほうがよかったかも
    • 意外と駐車したい用事がちょいちょいある

映画編

  • 都内より変な客に当たる確率がだいぶ低い
    • というか都内がおかしい、絶対に
    • 今のところ7回いって全部大丈夫
      • 都内では10回連続でハズレ引いてたのに...
  • 都内より気軽に映画館にいける
    • 電車より車で行く方が俺の性に合ってたらしい
  • IMAXが1館しかない...
    • これは明確に悪い点
  • 話題の映画はだいたいやってる
    • 単館はほぼなくてシネコンしかないが、意外とそれで事足りてしまってる
  • 近所のTSUTAYAの映画コーナーが少なすぎる
    • ほとんど文房具とカフェになっていた
    • なんなら文庫本すら1棚しか売ってない (書店を名乗るのをやめたら???)

まとめ

  • めっちゃ良かった!!
    • メンタルがだいぶ安定したし、不眠もほなくなった
  • 意外と新潟もまだまだ人がいる
    • もっと過疎ってるかと思ったが、全然人が多い(まあ新潟市だからかもだけど)
  • Uターンいいぞ

inputのreadonlyは自動入力の住所、disabledは一戸建てのマンション名に使われる

inputのreadonlyとdisabledで混乱したので整理。

readonlyとdisabledの違い

readonlyとdisabledは機能上は以下のような違いがある。

readonly は論理属性で、存在する場合、要素が変更可能ではなくなり、ユーザーがそのコントロールを編集できなくなります。

HTML 属性: readonly - HTML | MDN

disabled は論理属性で、存在する場合、その要素は変更不可、フォーカス不可、フォームへの送信不可となります。

HTML 属性: disabled - HTML | MDN

ただ、これだけだとあんまり分からないので具体的な例を考えてみる。

具体的なユースケースの違い

readonlyのユースケース

具体的なreadonlyのユースケースとして、郵便番号を入力したら自動で住所が入力されるパターン が考えられる。

この場合、自動入力された住所情報自体は有効である (サーバーに送信される)。なのでreadonlyになる。

disabledのユースケース

disabledの具体的なユースケースとしては、「一戸建て」「マンション」で一戸建てを選んだときの「マンション名」フィールド が考えられる。

この場合、一戸建てなのだから マンション名は使われない無効な情報である。 なのでdisabledになる。

まとめ

  • readonly: 入力を受け付けないけど 値自体は使われる状態 (自動入力の住所)
  • disabled: 入力を受け付けないし 値も使われない状態 (一戸建てのマンション名)

ちなみにdisabledは基本的にreadonlyを包含してるので、つける際はどちらかで1つで良い (両方つけるとdisabledをつけた時とだいたい同じ挙動になるはず)。

useSyncExternalStore を使って常に最新のclientRectを参照する

意外とネットに記事として上がってなかったので書いた。

function useClientRectWidth<T extends HTMLElement>() {
  const ref = useRef<T>(null);
  const subscribe = useCallback((listener: () => void) => {
    if (!ref.current) return () => void 0;

    const resizeObserver = new ResizeObserver(listener);
    resizeObserver.observe(ref.current);
    return () => resizeObserver.disconnect();
  }, []);

  const width = useSyncExternalStore(
    subscribe,
    () => ref.current?.getBoundingClientRect().width ?? null,
  );

  return { ref, width };
}

使い方はこんな感じ↓

export function App() {
  const { ref, width } = useClientRectWidth<HTMLTextAreaElement>();
  return (
    <div>
      <textarea ref={ref} />
      width: {width}
    </div>
  );
}

DOMRect全体を返す場合

width単体ではなくrect全体を取得したい場合、ちょっと工夫が必要。というのも、getBoundingClientRectの返り値は常に新しいオブジェクトなので、 無限レンダリングが発生して画面が真っ白になってしまう (参照)。

これを回避するには、useSyncExternalStoreに渡す getSnapshot関数をメモ化する必要がある。 幸いDOMRectは比較的単純な構造なので、JSON.stringifyをキーにしたキャッシュを実装すればよい。

function useClientRect<T extends HTMLElement>() {
  const ref = useRef<T>(null);
  const subscribe = useCallback((listener: () => void) => {
    if (!ref.current) return () => void 0;

    const resizeObserver = new ResizeObserver(listener);
    resizeObserver.observe(ref.current);
    return () => resizeObserver.disconnect();
  }, []);

  const lastCacheKey = useRef('');
  const cache = useRef<DOMRect | null>(null);
  const rect = useSyncExternalStore(subscribe, () => {
    const rect = ref.current?.getBoundingClientRect() ?? null;
    // 前回のJSON.stringify(rect)と結果が同じならcacheを返す
    if (JSON.stringify(rect) === lastCacheKey.current) {
      return cache.current;
    }
    // 前回と異なるので、キャッシュを更新する
    lastCacheKey.current = JSON.stringify(rect);
    return (cache.current = rect);
  });

  return { ref, rect };
}

アニメーション絵文字ジェネレータを作った

slackで「ありがとうございます!」みたいな文字数の長い絵文字は潰れてしまって読みづらいです。しかし、視認性を高めるには高々6文字くらいしか入れられません。

ここで、 「それなら、アニメーションさせればいいじゃない」 と閃いたので、実際にツールを作りました。

アニメ絵文字ジェネレータ

こういう絵文字が作れます↓

使い方

そこまで複雑なUIじゃないので、基本的には見たままで操作できるはずです。

空白行を入れると、そこでフレームが区切ることができます。これを利用すると1つのテキストフィールドだけで完結できます。