Iteration 1: The Naive Implementation
const [showFullLogo, setShowFullLogo] = useState(isHome);
useEffect(() => {
if (isHome) {
setShowFullLogo(true);
} else {
setTimeout(() => setShowFullLogo(false), 600);
}
}, [isHome]); Result: No delay. Logo contracted immediately.
Hidden assumption exposed: That the effect runs once per navigation.
React 18's strict mode runs effects twice, clearing the timeout.
Iteration 2-3: Refs for Persistence
We tried using refs to track state across effect runs. Still didn't work.
Hidden assumption exposed: That the component persists across
navigation. In Next.js App Router, the Header was remounting on each
route change, resetting all refs.
Iteration 4-5: sessionStorage
useEffect(() => {
if (isHome) {
sessionStorage.setItem('logoExpanded', 'true');
} else {
const wasExpanded = sessionStorage.getItem('logoExpanded');
sessionStorage.removeItem('logoExpanded'); // Too early!
if (wasExpanded) {
setTimeout(() => setShowFullLogo(false), 600);
}
}
}, [pathname]); Result: Still no delay.
Hidden assumption exposed: That we could remove the flag before
the timeout. When the component remounted (which we now knew happened), the flag
was already gone.
Iteration 6: The Console.log Revelation
At this point, we stopped coding and started observing. We added console
logs throughout the component:
[Logo] Coming from home - starting 600ms delay
[Logo Init] Object <-- REMOUNT
[Logo] Cleanup - clearing timer <-- Timer cleared!
[Logo] Not from home - no delay <-- Flag already removed
The logs revealed the complete picture: component remounting, cleanup running,
flags being cleared prematurely. One observation revealed what six iterations
of "clever" code could not.
Weniger, aber besser
"Less, but better." Console logs are crude, simple, old-fashioned. They're also
the fastest path to understanding. The hermeneutic circle favors observation
over speculation.
Iteration 7-8: Aligned Understanding
With our fore-structure now corrected—we understood the component lifecycle, the
remounting behavior, the timing of cleanups—the solution became clear:
// Initialize from sessionStorage (survives remounts)
const [showFullLogo, setShowFullLogo] = useState(() => {
if (typeof window !== 'undefined') {
if (isHome) {
const wasOnInternal = sessionStorage.getItem('wasOnInternal');
return !wasOnInternal; // Start contracted if coming from internal
}
return sessionStorage.getItem('logoExpanded') === 'true';
}
return isHome;
});
useEffect(() => {
if (isHome) {
const wasOnInternal = sessionStorage.getItem('wasOnInternal');
sessionStorage.removeItem('wasOnInternal');
if (wasOnInternal) {
// Coming from internal - animate expansion
setTimeout(() => setShowFullLogo(true), 600);
}
} else {
sessionStorage.setItem('wasOnInternal', 'true');
const wasExpanded = sessionStorage.getItem('logoExpanded');
if (wasExpanded) {
const currentPath = pathname;
setTimeout(() => {
// Only contract if still on same page
if (window.location.pathname === currentPath) {
sessionStorage.removeItem('logoExpanded');
setShowFullLogo(false);
}
}, 600);
}
}
}, [pathname, isHome]); The final solution accounts for: component remounting, strict mode double-invocation,
navigation during timeouts, bidirectional animation, and initial state hydration.
None of these were in our original fore-structure.