In Expo Router, what does rendering <Redirect href> do, and how does it differ from calling router.replace directly in a screen?
answer
- renders nothing
- replace, not push
- runs on focus, not in render
- every refocus redirects again
- imperative twin: replace in focus effect
basics
~20 sRedirect renders nothing and calls router.replace(href) when its screen gains focus, so the target takes the current history entry; calling router.replace yourself needs the same timing, in a focus effect or handler, never during render.
solid answer
~50 s`<Redirect href="/cart" />` from `expo-router` returns `null` and, inside a **focus effect**, calls `router.replace(href)`, so the redirecting screen is swapped out rather than left in history, and pressing back does not bounce the user into it again. Because it runs on **focus**, it fires each time that screen becomes focused, not only on first mount. It also accepts `withAnchor` and `relativeToDirectory`, passed through to replace. Calling `router.replace` yourself is equivalent **if** you do it at the same moment: Expo's redirects guide shows it inside `useFocusEffect`. Calling it during render is a side effect in render and can fire before navigation is ready, and using `push` instead of replace leaves the redirecting screen in history, creating a back-button loop. Redirect suits "this screen should not be shown in this state"; a handler call suits "go there after this action".
code
tsx · 20 lines// src/app/checkout.tsx
import { Redirect } from 'expo-router';
import { Text, View } from 'react-native';
export default function CheckoutScreen() {
const items = useBasketItems();
if (items.length === 0) {
// Replaces this entry with /cart each time checkout gains focus while empty.
return <Redirect href="/cart" />;
}
return (
<View>
<Text>{items.length} items to pay for</Text>
</View>
);
}
declare function useBasketItems(): { id: string }[];go deeper
Recall that Redirect renders nothing and sends the user to its href by replacing the current screen, so back does not return to it.
Explain that Redirect calls router.replace in a focus effect, so it re-runs on every focus, and how to reproduce that with useFocusEffect.
Avoid redirect loops between layouts and targets, keep navigation out of render, and choose Redirect, handler calls or layout guards by what the condition protects.
Set rules for URL retirement and state-based redirects so stale links from releases and campaigns land somewhere sensible without back-button traps.
## What the component actually does `Redirect` is a small component exported from `expo-router`. Its implementation is short enough to describe completely: 1. It reads the router with `useRouter()`. 2. It registers a **focus effect** that calls `router.replace(href, { relativeToDirectory, withAnchor })` when the screen gains focus. 3. It **renders `null`**. The screen that renders `<Redirect>` therefore never shows its own UI in that branch, and its history entry is **replaced** by the target. ## Why replace and not push A redirect means "this URL should really be that URL". If it pushed, the redirecting screen would stay underneath the target, and pressing back would land on it, which would immediately redirect forward again: a **back-button loop**. Replace swaps the entry, so back goes to whatever was before the redirecting screen. The Expo docs describe Redirect as functioning like the `replace` imperative call and navigating without rendering the current page. ## Why focus and not mount Because the call lives in a **focus effect**, it runs whenever the screen becomes the focused screen, including when the user returns to it later. For a condition that can change, such as an empty basket, that is the desired behaviour: every visit re-checks. It also means a Redirect that renders unconditionally makes the screen unreachable in practice. ## The imperative twin You can get the same effect without the component: ```tsx useFocusEffect(() => { if (basketIsEmpty) router.replace('/shop'); }); ``` Expo's redirects guide uses exactly this pattern, with `useFocusEffect` from `expo-router`, so the redirect happens every time the screen is focused. The rules for doing it by hand: - **Not during render.** Navigating in the render body is a side effect in render; it can run before navigation is ready and repeats on every re-render. - **Use replace, not push**, for the loop reason above. - **Keep the condition cheap and stable**, or the screen can redirect back and forth as state changes. | Situation | Better tool | |---|---| | Screen must not show in some state (empty basket, retired URL) | `<Redirect href>` in that branch | | Navigate after a user action or request | `router.push` / `router.replace` in the handler | | Re-check a condition on every visit, with extra logic | `useFocusEffect` plus `router.replace` | | Keep signed-out users away from a whole group | a guard in the layout, a separate mechanism | ## Checklist for a redirecting screen Before shipping a screen that redirects, check: 1. **Is the condition known at first render?** If it depends on data still loading, render a loading state first; redirecting on an undefined value sends everyone away. 2. **Does the target avoid the same condition?** A cart that redirects to checkout, and a checkout that redirects to the cart, loop forever. 3. **Is the redirect needed on every focus?** If only the first visit matters, an explicit handler call is clearer than a focus-based component. 4. **Does back go somewhere sensible?** Replace removes the redirecting screen; confirm the entry beneath it is where the user should return. 5. **Is this really a guard?** When many screens share the same rule, such as "signed-in only", a layout-level guard is easier to reason about than a Redirect in each screen. ## Pitfalls - **Redirecting in a layout that also matches the target**, so the redirect runs again on arrival and loops. - **Relative hrefs**: `./checkout` resolves against the current route; pass `relativeToDirectory` if the directory, not the document, should be the base. - **Missing back target**: replace keeps the stack shallow; if the target sits in a stack that should have a screen beneath it, `withAnchor` asks the router to load that stack's initial route underneath. - **Flash of content**: the redirecting branch renders `null`, so render Redirect instead of your UI rather than alongside it. ## In the pharmacy app `src/app/checkout.tsx` renders `<Redirect href="/cart" />` when the basket is empty, because an empty checkout is meaningless. The user who opens a stale checkout link lands on the cart, and back goes to wherever they came from, not to a checkout that would bounce them again.
- Why is <Redirect> built on replace rather than push?With push, the redirecting screen would remain beneath the target, and pressing back would return to it and trigger the redirect again, trapping the user. Replace swaps the history entry, so back skips the redirecting screen entirely.
- A screen renders <Redirect> only while a basket is empty; when does the check run again?Every time that screen gains focus, because Redirect calls replace from a focus effect. Returning to the screen later re-evaluates the condition and redirects again if the basket is still empty.
saying these in an interview costs you the question
- Redirect pushes the target so the user can go back to the old screen.
- Redirect runs only once, on the screen's first mount.
- Calling router.replace directly in the render body is equivalent.
- Redirect renders its children briefly before navigating.
- Redirect is the recommended way to protect every screen in a group.