Infinite Redirect Loop on Apprentice Onboarding
They were stuck. The app sent them to onboarding, then the onboarding page sent them right back.
The Problem
Users with an "apprentice" role who had not yet completed onboarding were getting stuck in a redirect loop. The app would send them to the onboarding page, and then the onboarding page would immediately redirect them elsewhere (e.g. back to the guard that had sent them to onboarding), which would send them to onboarding again. The browser would eventually stop with "too many redirects" or the page would never settle.
Investigation
I traced the flow. There was a route guard (e.g. in a layout or wrapper) that checked whether the user was an apprentice and whether they had completed onboarding. If not, it redirected to the onboarding route. So far so good. Then I looked at the onboarding page itself. It had its own logic: it checked something (e.g. onboarding status or a flag) and if that condition was true, it redirected to the main app (e.g. dashboard or home). The problem was that the two sides disagreed on "has the user completed onboarding" or "should the user be on this page."
For example, the guard might be reading from one place (e.g. a field on the user object) and the onboarding page might be reading from another (e.g. a different field or a cached value). So the guard thought "not completed, send to onboarding," and the onboarding page thought "already completed (or not an apprentice), send to dashboard." So the user bounced between the two. Another possibility was that the onboarding page was redirecting when it thought the user was not an apprentice, but the guard had already decided they were an apprentice and sent them to onboarding. So the conditions were inverted or out of sync.
I checked where each side got its data. The guard ran early and used one API or store; the onboarding page might have run after a different fetch or used a different slice of state. So we had two sources of truth or two points in time, and they disagreed.
Root Cause
The guard and the onboarding page were making independent redirect decisions based on conditions that were not the same. So the guard said "go to onboarding" and the onboarding page said "leave onboarding," and the user had no stable place to land. The root cause was inconsistent or asymmetric logic for "user is in onboarding" vs "user has finished onboarding" across the guard and the onboarding route.
The Fix
We defined a single place that knew "should this user see the onboarding flow right now." That could be a helper that considered role, onboarding status, and any feature flags. Both the guard and the onboarding page used that same helper. The guard: if the user should see onboarding, redirect to the onboarding route; otherwise let them through. The onboarding page: if the user should not see onboarding (e.g. already completed or not an apprentice), redirect to the main app; otherwise render the onboarding UI. So both sides used the same condition, and we could not get into a state where one sent the user to onboarding and the other sent them out.
We also made sure both sides read from the same data source (e.g. the same user object or the same API response) so that timing or cache did not cause them to see different facts. If the data was loaded asynchronously, we made the guard and the onboarding page wait for that data before deciding, and we avoided showing a wrong redirect based on stale or missing data.
Lessons Learned
- Redirect logic in a guard and in the target page must use the same condition. If the guard says "go to A" and the page for A says "go to B," you get a loop. One source of truth for "user should be on this route" prevents that.
- When "in onboarding" depends on role and completion status, both the guard and the onboarding page should use the same helper and the same data. Duplicated or slightly different checks are a common cause of redirect loops.
- For role- or state-dependent redirects, ensure the data is loaded before deciding. Redirecting on stale or undefined state can cause flaky or contradictory behaviour.
Have thoughts on this story or questions? Get in touch.