1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ReactのsetState無限ループが画像プレビューで起きた原因

1
Posted at

ピクセル画像のプレビューでMaximum update depth exceededが発生しました。useEffectの依存配列ではなく、キャッシュ済み画像を補完するref callbackとonLoad内のsetStateが循環していました。

起きていた循環

画像の自然サイズをstateへ保存し、その値から表示倍率を決めています。

<PixelImg
  src={src}
  onLoad={(event) => {
    const img = event.currentTarget;
    setDims({ w: img.naturalWidth, h: img.naturalHeight });
  }}
/>

PixelImg側には、キャッシュ済み画像で通常のonLoadを取りこぼす場合の補完がありました。

ref={(node) => {
  if (node && onLoad && node.complete && node.naturalWidth > 0) {
    onLoad({ currentTarget: node } as SyntheticEvent<HTMLImageElement>);
  }
}}

ref callbackがonLoadを呼び、setDimsが新しいobjectを作ります。再レンダー後にref callbackが再び呼ばれ、同じサイズでもstate更新が続いていました。

URLごとに補完を1回へ制限する

PixelImgでは、補完済みのcurrentSrcをrefへ記録します。

const firedFor = useRef<string | null>(null);

ref={(node) => {
  if (
    node &&
    onLoad &&
    node.complete &&
    node.naturalWidth > 0 &&
    firedFor.current !== node.currentSrc
  ) {
    firedFor.current = node.currentSrc;
    onLoad({ currentTarget: node } as SyntheticEvent<HTMLImageElement>);
  }
}}

画像URLが変わればもう一度発火し、同じURLの再レンダーでは止まります。

同じ寸法ならstateを変えない

受け取り側にも防御を入れました。

setDims((previous) =>
  previous &&
  previous.w === img.naturalWidth &&
  previous.h === img.naturalHeight
    ? previous
    : { w: img.naturalWidth, h: img.naturalHeight },
);

値が同じときは以前の参照を返すため、Reactは再レンダーを省けます。発火元と更新先の両方で止めたのは、どちらかの実装が後から変わっても循環しにくくするためです。

画像読み込み周辺の無限ループでは、useEffectだけでなくcallback refも確認対象になります。refはDOM nodeを受け取る場所ですが、その中でstateを変えるとレンダーサイクルへ参加します。特にcompleteを見てイベントを手動発火する実装では、同じリソースへ何度発火したかを記録しておく必要があります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?