Taming Logout Jank: Waiting for the Transition to Actually Finish
TL;DR
- iOS shows an OS-level alert when Auth0 opens a web view, which backgrounds the app and triggers state resets.
- Those resets fire before navigation completes, so users see menu items and theme colours flicker.
- Backend logs show unauthorized API calls during logout — 401s with no token, meaning requests outrun the teardown.
- Replaced a promise-plus-one-second-timeout hack with resets keyed on
wasLogoutSuccessful. - Some jank remains. Sub-300ms and post-logout, I’ll take it for now.
Logging out looks bad. Not broken, but visibly janky, and the causes turned out to be layered.
The alert we can’t remove
On iOS, transitioning to an external resource — Auth0’s web view — makes the OS prompt the user with an auth0.com alert. That’s a system-level behaviour Auth0’s maintainers can’t circumvent, so it’s a constraint rather than a bug.
The consequence is what matters: the alert briefly backgrounds the app, and backgrounding triggers state changes. Navigation menu items appear based on login state that’s mid-flight. I’ve controlled most of those transitions now, though one card still shows up because of backend state misalignment.
Requests outrunning the teardown
The backend logs told a sharper story. During logout, unauthorized API calls hit the backend and return 401s because they carry no authorization token. Checking the programs, user interface, and cohort endpoints, the requests genuinely lack user data.
So token removal is running before the requests that depend on those tokens have finished. The teardown and the in-flight work aren’t synchronised, which is the same class of problem as the spinner — code assuming a transition is instantaneous when it isn’t.
The hack I started with
My first fix was a promise resolving after a one-second timeout, in a useEffect in AppDataContext watching wasLogoutSuccessful. It’s duct tape and it felt wrong while I was writing it. An arbitrary second is a guess about how long rendering takes, and it’ll be wrong on a slower device.
The microtask queue is the more honest tool — a promise resolving after the current operations rather than setTimeout or setImmediate guessing at a duration.
What I settled on
Reset on the result, not the attempt. wasLogoutSuccessful becomes the cue to reset vision and palette state, and only when it’s genuinely true.
Concretely:
- Stored the timeout ID in a variable and cleaned it up inside
useEffect, so nothing dangles. - Went through
useVisionandthemeContextreplacing unreliable state checks withwasLogoutSuccessful. - Set
isSigningOuttrue when sign-out begins, and only confirm success once credentials are fully cleared.
The theme reset was its own contributor — resetting the palette inside the useEffect in the theme context controller caused visible flicker. Removing the reset reduced the jank but delayed navigation, which is the trade that led me to the sequencing above. Rather than letting palette values go undefined, keeping a coherent palette structure through the transition avoids the disruption entirely.
The right architecture is logout methods provided by the context providers, so state resets happen from one place after the whole process completes rather than being scattered across components that each guess at timing.
Good enough, for now
Testing on iOS, the transitions are close to jank-free. Some remains, and by that point the user is on their way out and probably doesn’t notice.
That’s a real judgement rather than a cop-out: fleeting jank under ~300ms during a logout transition is an acceptable threshold. I’d rather ship this improvement and keep going than hold it for perfection on a screen users see for half a second on their way to the door.
Next on the UX list: make the spinner more prominent during async operations, and deactivate buttons during sign-in and sign-out so people can’t fire a second request into the middle of the first.
ryer.io