ryer.io

The Loading Spinner That Wouldn't Leave

TL;DR

  • 401s from the backend traced to the client calling APIs before it held a valid token.
  • The spinner logic was spread across isLoading, isSigningIn, isSigningUp and refreshing, and the booleans overlapped.
  • Cutting it to just isSigningIn / isSigningUp removed most of the misbehaviour.
  • The reducer wasn’t applying the dispatched payload, so state never actually changed.
  • Callbacks from the context beat finally blocks and timeouts for knowing when auth is genuinely done.

The loading indicator bug that’s been lingering all week. An idea struck me last night and I spent today chasing it.

The 401s

First thread: the backend is returning 401s, and the pattern says the client is calling APIs without a valid token — asynchronous state glitches introduced by the recent refactoring. I noted every endpoint involved and they share a common context provider, which suggests the provider needs a readiness check rather than each caller guarding itself.

The plan: callbacks, not finally

My hypothesis for the spinner: when we change states — signing up, signing in — a callback exported from the context can explicitly reset the loader once authentication genuinely completes. That’s imperative control, and it unmounts the loader more precisely than a finally block can, because finally fires when the function ends rather than when the transition does.

The current approach is worse than that. There’s a useEffect depending on wasLogoutSuccessful with set timeouts managing UI state after logout, and the timeouts are pure guesswork.

Looking at it now, wasLogoutSuccessful was itself a workaround — it exists because we tried to order UI changes after authentication and didn’t have a clean signal to hang them on. A callback is what it was reaching for.

The edge cases are what make this non-trivial: cancelled logins, failed auth, credential checks. Each one needs the callback to fire, or the loader never comes down. AuthContext already exports the states I’d need — isSigningIn, isSigningUp, possibly wasLogoutSuccessful — so consolidation is available without adding success flags to signIn and signUp. The callback covers what those flags would have.

Too many booleans

Attacking the spinner directly, AppDataContext owns a loading overlay driven by isLoading, isSigningUp, and isSigningIn. My first hypothesis was wrong — the spinner stayed hidden except under specific conditions, which pointed at how I was initialising state rather than at the display logic.

Console logging found it: the boolean logic encompassed states it shouldn’t, so the spinner persisted after the work finished.

The cleanup that helped most was subtraction. I reduced everything to isSigningIn and isSigningUp, and removed the refreshing flag and an unnecessary isLoading check that the splash screen already covered. Excessive conditionals were obscuring the flow, and the unexpected behaviour on cancel and error paths came directly from that.

Error handling was compounding it — try-catch around the Auth0 call meant exceptions skipped the state reset, leaving flags stale.

The reducer wasn’t listening

Then a plainer bug. The spinner persisted after a successful sign-in no matter what I dispatched, because the reducer wasn’t handling the payload properly. State was never updating. All the logic above was correct and inert.

I suspected nested try-catch-finally blocks were skipping the outer finally on an early return from a catch, but a console log confirmed finally was executing. Not that.

With the reducer fixed, I chose separate dispatches for isSigningUp and isSigningIn rather than one unified state change, and moved every state reset into finally blocks consistently across both functions.

Not finished

The spinner is better behaved but isSigningIn and isSigningUp still aren’t synchronising the way I want, and after signup the interface doesn’t navigate even though authentication succeeds — the parent stack controller seems to misread the post-auth user state.

Also noted for later: client-side caching errors on logout, where setting cache keys returns device storage errors. getCachedImage and getCachedDescription are both affected. Not today’s problem.

Pausing here, on the subtle art of toggling async state.