AniUI Academy

Passing Params and Conditional Navigation

Passing params to a screen instead of prop-drilling through the navigator, and the root-level auth-stack-vs-app-stack pattern for conditional navigation.

8 min read

Two screens connected by a navigator usually need to share more than a simple "go here" — the destination screen almost always needs which thing to show, and the app as a whole often needs to show an entirely different set of screens depending on who's using it. Both of those are handled by patterns that are durable across navigation libraries and versions, even though this lesson deliberately won't pin down one exact current API signature.

Passing params

When you navigate to a screen, you can hand it data specific to that navigation — conceptually, something like:

// From a list screen, navigating to a detail screen for one specific item
navigation.navigate("Details", { id: 42 });

The destination screen reads that data back through a mechanism the navigator provides for exactly this — conceptually, a route object or hook tied to that screen, rather than the screen receiving it as an ordinary prop handed down by a parent component:

function DetailsScreen({ route }: { route: { params: { id: number } } }) {
  const { id } = route.params;
 
  const { data: product } = useProduct(id);
 
  return <ProductView product={product} />;
}

Why this beats prop-drilling

Without params, getting id to DetailsScreen would mean threading it as a prop through every component between wherever it originated and DetailsScreen itself — components that don't use id for anything, just forwarding it downward because something further down needs it. That's prop-drilling, and it's exactly the kind of coupling params sidestep: the navigator carries the value directly from the call site (navigate("Details", { id: 42 })) to the destination screen, with nothing in between required to know or care that id even exists.

This should feel familiar from plain React: it's the same problem useContext solves for state that many distant components need, applied here specifically to "the data one particular navigation needs to carry along with it."

Conditional navigation: auth flows

The other durable pattern is switching what the user can navigate to at all, based on some piece of app state — most commonly, whether they're logged in.

The tempting-but-wrong instinct is to guard screens one at a time: check isLoggedIn inside SettingsScreen, inside CheckoutScreen, inside every other screen that shouldn't be reachable while logged out, redirecting to login if the check fails. This works, technically, but it's a rule duplicated across every protected screen — and a rule duplicated everywhere is a rule that's eventually missed somewhere, the first time a new protected screen is added without remembering to add the same check.

The standard pattern instead switches the navigator itself, at the root, based on auth state:

function RootNavigator() {
  const { isLoggedIn } = useAuth();
 
  return isLoggedIn ? <AppNavigator /> : <AuthNavigator />;
}

AuthNavigator is its own stack — login, signup, forgot-password — and AppNavigator is the real app's navigator, tabs, stacks, and all. An unauthenticated user never has AppNavigator mounted in the first place, which means there's no protected screen anywhere inside it that could be reached by accident: there's structurally nothing to guard against, because none of it exists in the tree until isLoggedIn flips.

function useAuth() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  // ...sign-in / sign-out logic that flips isLoggedIn...
  return { isLoggedIn };
}

The moment isLoggedIn becomes true (say, right after a successful login call inside AuthNavigator), RootNavigator re-renders, swaps in AppNavigator, and the user lands in the real app — with no individual screen ever having had to ask "am I allowed to be here?"

What to remember

  • Pass data a destination screen needs as navigation params, rather than prop-drilling it through every screen in between.
  • The destination screen reads params back through the navigator's own mechanism (a route object/hook), not as an ordinary prop from its parent.
  • Auth flows are handled by rendering an entirely different navigator (an auth stack vs. the main app navigator), switched once at the root based on auth state.
  • Switching at the root is structurally safer than guarding individual screens, because there's nothing to forget to guard — the protected navigator simply isn't mounted for a logged-out user.
  • These are durable concepts across navigation library versions; the exact function names and signatures are the part that changes.

Check yourself

4 questions · pass 3/4 to unlock Deep Linking, Conceptually

up to 50
  1. 1.Why pass params when navigating to a screen, instead of prop-drilling the data through the whole navigator tree?

  2. 2.Conceptually, how does the destination screen read params back?

  3. 3.What's the standard real-world pattern for switching between authenticated and unauthenticated navigation?

  4. 4.What's a downside of guarding individual screens one-by-one with their own auth checks, compared to switching navigators at the root?

4 left to answer