skip to content

In React Navigation 7, how do you avoid flashing the sign-in screen while a telehealth app restores a saved session token at launch?

level: middleimportance: should knowfreq 46%

answer

  1. three states, not a boolean
  2. restoring is not signed out
  3. splash instead of the navigator
  4. or a lone Splash screen
  5. failed restore falls back to signed out

basics

~20 s

Model auth as three states — restoring, signed out, signed in — and render a splash while restoring, either instead of the navigator or as its only screen, so React Navigation never mounts SignIn before the saved token has been read.

solid answer

~50 s

Reading a saved token is asynchronous, so on the first render the app does not know yet whether the patient is signed in. If that unknown is treated as signed out, the navigator renders `SignIn`, then swaps to `Home` a moment later: a visible flash, a replace animation, and sign-in screen effects that ran for nothing. I model a third state, `restoring`, start in it, and read the token in an effect that moves to signed in or signed out — signed out also when the read fails. While restoring I render a splash view instead of the navigator, or render a `Splash` screen as the navigator's only screen; when restoring ends, that screen's route is dropped and the stack starts on the right first screen. Whether the token is also checked with the server is a separate decision.

code

tsx · 55 lines
tsx
import * as React from 'react';
import { ActivityIndicator, View } from 'react-native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { readStoredToken } from './session';
import { HomeScreen, SignInScreen, SignUpScreen } from './screens';

type AuthState =
  | { status: 'restoring' }
  | { status: 'signedOut' }
  | { status: 'signedIn'; token: string };

const Stack = createNativeStackNavigator();

export function RootNavigator() {
  const [auth, setAuth] = React.useState<AuthState>({ status: 'restoring' });

  React.useEffect(() => {
    let cancelled = false;
    readStoredToken()
      .then((token) => {
        if (!cancelled) {
          setAuth(token ? { status: 'signedIn', token } : { status: 'signedOut' });
        }
      })
      .catch(() => {
        if (!cancelled) {
          setAuth({ status: 'signedOut' });
        }
      });
    return () => {
      cancelled = true;
    };
  }, []);

  if (auth.status === 'restoring') {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
        <ActivityIndicator />
      </View>
    );
  }

  return (
    <Stack.Navigator>
      {auth.status === 'signedIn' ? (
        <Stack.Screen name="Home" component={HomeScreen} />
      ) : (
        <>
          <Stack.Screen name="SignIn" component={SignInScreen} />
          <Stack.Screen name="SignUp" component={SignUpScreen} />
        </>
      )}
    </Stack.Navigator>
  );
}

go deeper

for a junior

Remember that reading the saved token is asynchronous, so the app needs a loading state and a splash before it shows either the sign-in or the home screens.

for a middle

Explain the three-state model, the two places a splash can render, and why the router drops a Splash route instead of keeping it in history.

for a senior

Cover the failure path and unmount guard, and discuss whether to validate a restored token with the server before leaving the splash or let the first failing request sign out.

for a principal

Weigh launch latency against showing stale signed-in UI, and set one rule for session state so restore, sign-in, expiry and sign-out all flow through the same place.

## Why the flash happens A saved session token lives in device storage, and reading it is **asynchronous**. On the very first render of a telehealth app, nothing has been read yet. The common bug is to model auth as a single boolean, `isSignedIn`, that starts as `false`: 1. The first render sees `isSignedIn === false`, so React Navigation renders the signed-out branch and mounts `SignIn`. 2. The storage read resolves a few hundred milliseconds later with a valid token. 3. `isSignedIn` becomes `true`, the signed-out routes are dropped and the stack starts on `Home`. The patient sees the sign-in form flash, then a replace animation. Worse, `SignIn` actually mounted, so its effects ran: an autofocused field may pop the keyboard, or a sign-in screen view may be logged for a user who never saw it. The root cause is that **"not known yet" was encoded as "signed out"**. ## Model three states | State | What is rendered | How you leave it | |---|---|---| | `restoring` | A splash (no auth screens at all) | The token read finishes | | `signedOut` | `SignIn`, `SignUp` | A successful sign-in or sign-up | | `signedIn` | `Home`, `Appointments`, `VisitRoom` | Sign-out or an expired session | React Navigation's own auth-flow example uses a reducer with an `isLoading` flag that starts `true`, a `RESTORE_TOKEN` action that clears it, and `SIGN_IN` / `SIGN_OUT` actions. A discriminated union such as `{ status: 'restoring' } | { status: 'signedOut' } | { status: 'signedIn'; token: string }` expresses the same thing in TypeScript and makes an impossible combination (loading *and* signed in) unrepresentable. ## Where to render the splash There are two valid placements, and both keep `SignIn` from mounting: - **Instead of the navigator.** While `status === 'restoring'`, the component that would render the navigator returns a splash view — typically a `View` with an `ActivityIndicator`. The navigator mounts only once the answer is known, so its first state is already the correct branch. The React Navigation example does exactly this. - **As the navigator's only screen.** While restoring, the navigator's children are a single `Splash` screen. When the status changes, `Splash` is no longer defined, the stack router drops its route and, with no route left, starts the stack on the navigator's `initialRouteName` if that screen is rendered, otherwise the first screen of the new branch. `Splash` never stays in history. Many apps also keep the platform's native launch screen up until restoring ends, so the splash component and the launch screen look like one continuous image. That is a presentation choice; the navigation rule is only that no auth branch renders before the state is known. ## The restore effect itself - Start in `restoring` and run the read **once**, in an effect on mount. - On success with a token, move to `signedIn`; with no token, move to `signedOut`. - **On failure, move to `signedOut`.** A storage error must not leave the app on the splash forever. - Guard against setting state after unmount (a `cancelled` flag in the effect's cleanup) so a fast remount in development does not apply a stale result. - Keep the restored state in the same place sign-in and sign-out write to, so every later flip goes through one source of truth. ## Validating the restored session A token that was read successfully may still be expired or revoked. Some apps treat "a token exists" as signed in and let the first failing API call trigger sign-out; others stay in `restoring` until a quick server check returns. That trade-off — faster launch against a moment of stale signed-in UI — belongs to session and credential handling rather than to navigation. From React Navigation's point of view, all that matters is that the app stays in one well-defined state until it knows which branch to render, and that leaving `restoring` flips the screen list exactly once. ## What the interviewer is checking The question sounds like UI polish, but it tests whether the candidate models asynchronous initial state honestly. The strong answer names the third state, says where the splash renders and why `SignIn` must not mount, handles a failed read, and notes that the router — not a manual `navigate` — moves the user on once the state is known.

  • If you render a Splash screen inside the navigator instead, does it end up in the back history?
    No. When restoring ends, `Splash` is no longer rendered, so the stack router drops its route. With no route left, it starts the stack on `initialRouteName` if that screen is in the new list, otherwise on the first screen of the new branch. The patient cannot go back to the splash.
  • What should happen if reading the stored token throws?
    Leave `restoring` for `signedOut`. Staying in `restoring` on an error leaves the patient on a spinner forever, and signing them out is recoverable: they can sign in again. The effect should also ignore results that arrive after the component unmounted.

saying these in an interview costs you the question

  • Start isSignedIn as false and flip it to true once the token read resolves.
  • Render the navigator immediately and call navigate('Home') when the token arrives.
  • Keep the splash on screen until the read succeeds, with no handling for a failed read.
  • A Splash screen inside the navigator always stays at the bottom of the back history.