Closed8

「0を0にする」でも再レンダーされちゃうのなんで?

Yug (やぐ)Yug (やぐ)

なぜこれが無限ループして永遠にrenderが出力されてしまうんだ?という疑問。
Object.is(0, 0)はtrue->つまり値が変わっていないと認識される->renderはスキップされる
のはずでは...?

export default function App() {
    const [num, setNum] = useState(0);
    setNum(0);
    console.log('render');
}

以下のようにイベントハンドラ内にセッターを移せば当然無限ループは回避できるけれども。

export default function App() {
    const [num, setNum] = useState(0);
    console.log('render');

    return <button onClick={() => setNum(0)}>ボタン</button>;
}
Yug (やぐ)Yug (やぐ)

わからんなぁ...。コンポーネントのトップレベルで、つまりレンダー中にsetterを呼び出すのは良くないよ、みたいな常識?はありそうだなぁとは思うけど、「でもなんで?」状態。

Yug (やぐ)Yug (やぐ)

メモ:
useStateだけでなくuseEffectでも似たような事象が発生した(無限ループではないけど)

export default function App() {
    const [val, setVal] = useState(0);
    console.log('valは', val);

    useEffect(() => {
        console.log('effectのvalは', val);
        setVal(1);
    }, [val]);

    return (
        <>
            val: {val}
        </>
    )
}

これで結果は↓のようになる。最後に1を1にsetするという段階でrenderは無視されるはずなのにrenderが発火して再度2回コンソール出力されてる...。

Yug (やぐ)Yug (やぐ)

というか、確かにbailoutの関数は走っていないことは確認できた。なのであとはなぜスキップしないのかという思想の面が気になる

事実(ソース)は掴んだので、あとは理由(思想)。

Yug (やぐ)Yug (やぐ)

akfm.satoさんとDaishi KatoさんとDiscordで議論させてもらって、かなり考えが固まってきた
https://discord.com/channels/1313457938962190368/1321429602387759126

要は、単純な話で、「Reactは純粋関数が原則なんだからレンダー中にsetStateを呼び出すという行為はおかしいやろ、たとえ0を0にするという行為でも関係ない、setStateを呼び出した時点でアウトや。だからbailout処理なんか別に用意しないで。そんなもん必要ないわ!」てことではなかろうか。

ちなみに、「レンダー中にsetStateを呼び出してもええで」ということをReact公式が言っている驚きの事実があるが、これはちゃんとif文使ってsetStateが毎回走らないようにしてるのでセーフということだろう。
https://ja.react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes

if (items !== prevItems) {
  setPrevItems(items);
  setSelection(null);
}

あと、Daishi Katoさんにコメントいただいた内容に関してはじっくり考えたい↓

  • レンダー中にsetState(0)をすると、再レンダーをトリガー
  • その際に、実行中のレンダーの結果は捨てられると考えると良いのではないか
  • なので値が変わってなくても、前回の値のレンダー結果は残っていない、ので再計算するしかない

すごく面白い視点。...というかこれが事実かも?いつかソースコードで検証したい。

あと、「スキップしちゃうと困るかもしれないよ、具体例は思いつかないけど。多分ツリーの計算が途中で止まっているからかな」ということも仰っていて、困るケースがあるのかどうかは自分も気になるところ。

このスクラップは2025/01/12にクローズされました