The push() bug being silent is what makes it the right first example. Worth adding to the StrictMode point: the dev double-invoke also hits useState and useReducer initializers and useMemo factories, so an expensive or side-effectful initializer runs twice in dev and people assume something is looping. And on state surviving by tree position, the sneaky version is defining a component inside another component's render, it becomes a new type every render so React remounts it and its state resets on every parent update. On the teaching question I'd show the Object.is check explicitly up front, since "why didn't it re-render" is unanswerable until you know the comparison is by identity.
The push() bug being silent is what makes it the right first example. Worth adding to the StrictMode point: the dev double-invoke also hits useState and useReducer initializers and useMemo factories, so an expensive or side-effectful initializer runs twice in dev and people assume something is looping. And on state surviving by tree position, the sneaky version is defining a component inside another component's render, it becomes a new type every render so React remounts it and its state resets on every parent update. On the teaching question I'd show the Object.is check explicitly up front, since "why didn't it re-render" is unanswerable until you know the comparison is by identity.