Jos olet tehnyt React Native -sovelluksia parin viime vuoden aikana, olet luultavasti törmännyt Expo Routeriin — ja vuoteen 2026 mennessä siitä on tullut tosiasiallinen standardi mobiilinavigaatiossa. Se tuo Next.js-tyylisen tiedostopohjaisen reitityksen mobiilipuolelle, yhdistää web- ja natiivialustat yllättävän saumattomasti ja rakentuu silti React Navigationin vakaalle pohjalle. Tässä oppaassa käydään läpi Expo Router 4:n keskeiset käsitteet, asennus, layoutit, syvät linkit, autentikoinnin suojaama reitti ja muutama yleinen sudenkuoppa — kaikki ajantasaisilla esimerkeillä Expo SDK 53–55 -ympäristössä.
Miksi Expo Router korvasi perinteisen React Navigationin konfiguraation
Muistan vielä projektin pari vuotta sitten, jossa puolet App.tsx-tiedostosta oli pelkkää navigaattorin rekisteröintiä. Aiemmin React Native -sovellusten navigaatio rakennettiin nimittäin manuaalisesti NavigationContainer-komponentin ja createNativeStackNavigator-funktioiden ympärille. Se toimi kyllä, mutta tuotti satoja rivejä boilerplate-koodia, vaikeutti syvien linkkien hallintaa ja teki kooditarkistuksesta tuskaa. Expo Router ratkaisee nuo ongelmat melko tyylikkäästi:
- Tiedostot ovat reittejä. Tiedosto
app/profile.tsxmuodostaa automaattisesti reitin/profile— ei tarvitse rekisteröidä komponentteja erikseen. - Tyypitetyt reitit. Expo Router 3.5+ tuottaa TypeScript-tyypit kaikille reiteille automaattisesti, jolloin
router.push("/profille")antaa kääntäjävirheen (kyllä, juuri tuollaiseen kirjoitusvirheeseen olen sortunut useammin kuin haluan myöntää). - Universaalit linkit. Sama
<Link href="/settings" />toimii iOS:llä, Androidilla ja webissä ilman muutoksia. - Ladattavat layout-rajat. Suspense ja virherajat ovat sisäänrakennettuja jokaiseen segmenttiin.
Asennus ja projektirakenne
Uusi Expo-projekti tulee Expo Router -valmiina, kun käytät virallista mallia. Tässä nopein tapa:
npx create-expo-app@latest my-app --template tabs
cd my-app
npx expo start
Olemassa olevaan projektiin lisäys vaatii muutaman riippuvuuden ja entry pointin vaihdon — ei mitään dramaattista, mutta lue ohjeet huolellisesti:
npx expo install expo-router react-native-safe-area-context \
react-native-screens expo-linking expo-constants expo-status-bar
# package.json
"main": "expo-router/entry"
Lisää sen jälkeen app.json-tiedostoon scheme ja Expo Router -plugin:
{
"expo": {
"scheme": "myapp",
"plugins": ["expo-router"],
"experiments": { "typedRoutes": true }
}
}
Tyypillinen app/-hakemisto näyttää suunnilleen tältä:
app/
├── _layout.tsx # juuri-layout (Stack, ThemeProvider jne.)
├── index.tsx # reitti "/"
├── (tabs)/ # ryhmä — ei näy URL:ssa
│ ├── _layout.tsx # Tab-navigaattori
│ ├── home.tsx # "/home"
│ └── settings.tsx # "/settings"
├── product/
│ └── [id].tsx # "/product/123"
└── +not-found.tsx # 404-näkymä
Layoutit, ryhmät ja Stack-navigaattorit
Jokainen _layout.tsx-tiedosto määrittelee, miten sen sisarustiedostot renderöidään. Ylimmän tason layout asettaa yleensä Stack-navigaattorin:
// app/_layout.tsx
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
export default function RootLayout() {
return (
<>
<StatusBar style="auto" />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" />
<Stack.Screen name="product/[id]" options={{ presentation: "modal" }} />
</Stack>
</>
);
}
Sulkujen sisällä olevat hakemistot, kuten (tabs), ovat ryhmiä: ne organisoivat tiedostoja näkymättä URL:ssa. Käytännössä tämä on tosi kätevä tapa pitää välilehdet erillään modaaleista tai autentikointivirroista — etenkin kun sovellus kasvaa.
Tab-navigaattorin määrittely
// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
export default function TabsLayout() {
return (
<Tabs screenOptions={{ tabBarActiveTintColor: "#0a84ff" }}>
<Tabs.Screen
name="home"
options={{
title: "Etusivu",
tabBarIcon: ({ color, size }) =>
<Ionicons name="home" color={color} size={size} />,
}}
/>
<Tabs.Screen
name="settings"
options={{
title: "Asetukset",
tabBarIcon: ({ color, size }) =>
<Ionicons name="settings" color={color} size={size} />,
}}
/>
</Tabs>
);
}
Dynaamiset reitit ja parametrit
Hakasulkeissa oleva tiedostonimi luo dynaamisen segmentin. Eli app/product/[id].tsx kaappaa kaikki polut muotoa /product/123. Parametri luetaan useLocalSearchParams-hookilla:
// app/product/[id].tsx
import { Text, View } from "react-native";
import { useLocalSearchParams } from "expo-router";
export default function ProductScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
return (
<View>
<Text>Tuote {id}</Text>
</View>
);
}
Pieni mutta tärkeä vinkki: suosi useLocalSearchParams-hookia useGlobalSearchParams:n sijaan. Paikallinen versio päivittyy vain silloin, kun reitti on aktiivinen, mikä vähentää tarpeettomia uudelleenrenderöintejä — etenkin syvemmissä navigaatiopinoissa ero näkyy heti.
Catch-all-reitit
Tiedostonimi [...slug].tsx kaappaa kaikki alipolut. Tämä on hyödyllinen dokumentaatiosivuille ja vararatkaisuille:
// app/docs/[...slug].tsx
const { slug } = useLocalSearchParams<{ slug: string[] }>();
// /docs/getting-started/install -> slug = ["getting-started", "install"]
Navigointi: Link, router ja Redirect
Expo Router tarjoaa kolme tapaa siirtyä reittien välillä. Käytä <Link>-komponenttia deklaratiivisessa JSX:ssä, router-objektia imperatiivisia kutsuja varten ja <Redirect>-komponenttia ehdollisiin uudelleenohjauksiin. Kaikki kolme käytössä yhden tiedoston sisällä on ihan tavallista.
import { Link, router, Redirect } from "expo-router";
// Deklaratiivinen
<Link href="/product/42" asChild>
<Pressable><Text>Avaa tuote</Text></Pressable>
</Link>
// Imperatiivinen
const onSubmit = () => {
router.push({ pathname: "/product/[id]", params: { id: "42" } });
};
// Ehdollinen
if (!user) return <Redirect href="/login" />;
Tyypitettyjen reittien (typedRoutes: true) ansiosta yllä olevat href-arvot tarkistetaan käännösaikana. Kirjoitusvirhe estää kääntämisen kokonaan, ja rehellisesti sanottuna tämä on yksi parhaita tuntumia, mitä TypeScript on koskaan tuonut React Native -kehitykseen.
Autentikoinnin suojaama reitti
Tämä on ehkä yleisin kysymys, jonka aloittelijoilta kuulen: miten kirjautumattomat käyttäjät ohjataan kirjautumisnäkymään? Vuoden 2026 suositeltu kuvio on Route Group + _layout.tsx, joka palauttaa <Redirect>:n tilan perusteella.
app/
├── _layout.tsx
├── (auth)/
│ ├── _layout.tsx
│ └── login.tsx
└── (app)/
├── _layout.tsx # tarkistaa session
├── home.tsx
└── profile.tsx
// app/(app)/_layout.tsx
import { Redirect, Stack } from "expo-router";
import { useAuth } from "../../hooks/useAuth";
export default function AppLayout() {
const { session, isLoading } = useAuth();
if (isLoading) return null; // tai SplashScreen
if (!session) return <Redirect href="/login" />;
return <Stack />;
}
Tämä kuvio on selvästi parempi kuin manuaalinen useEffect + router.replace -yhdistelmä, koska se estää sen ärsyttävän välähdyksen, jossa suojattu sisältö ehtii vilahtaa hetken ennen uudelleenohjausta. Pieni yksityiskohta, iso ero käyttötuntumassa.
Syvät linkit ja universaalit linkit
Expo Router määrittää syvien linkkien skeeman automaattisesti app.json:n scheme-kentän perusteella. Sovelluksen sisäisen syvän linkin myapp://product/42 testaaminen onnistuu suoraan terminaalista:
# iOS-simulaattori
xcrun simctl openurl booted myapp://product/42
# Android-emulaattori
adb shell am start -W -a android.intent.action.VIEW -d "myapp://product/42"
Universal Links (iOS) ja App Links (Android) edellyttävät hieman lisää säätöä — käytännössä associatedDomains- ja intentFilters-konfiguraatiota. Onneksi Expo Router 4:ssa lisäys onnistuu pelkästään app.json:n kautta:
{
"expo": {
"ios": {
"associatedDomains": ["applinks:myapp.com"]
},
"android": {
"intentFilters": [{
"action": "VIEW",
"autoVerify": true,
"data": [{ "scheme": "https", "host": "myapp.com" }],
"category": ["BROWSABLE", "DEFAULT"]
}]
}
}
}
Tila- ja lataustilanteet: Suspense ja virherajat
Jokainen Expo Router -segmentti voi määritellä oman lataus- ja virhetilansa. Tämä on yksi niistä piirteistä, joita en osannut arvostaa ennen kuin näin ne tuotannossa. Lisää esimerkiksi näin:
// app/(tabs)/_layout.tsx
export const unstable_settings = {
initialRouteName: "home",
};
export function ErrorBoundary({ error, retry }) {
return (
<View>
<Text>Jokin meni pieleen: {error.message}</Text>
<Pressable onPress={retry}><Text>Yritä uudelleen</Text></Pressable>
</View>
);
}
Yhdistettynä TanStack Queryn useSuspenseQuery:hin saat sujuvan lataustilan ilman manuaalisia isLoading-tarkistuksia. Tästä syntyy yllättävän siisti koodi.
Suorituskykyvinkit Expo Router 4:lle
- Lazy-load raskaat näytöt. Käytä
Stack.Screen-asetustalazy: true, jotta välilehtiä ei renderöidä ennen niiden avaamista. - Vältä globaaleja parametreja.
useGlobalSearchParamsaiheuttaa uudelleenrenderöinnin kaikissa segmenteissä — käytä paikallista versiota aina kun mahdollista. - Pidä juuri-layout kevyenä. Tarjoa kontekstit (theme, query client) juuressa, mutta vältä isoja laskentaoperaatioita siellä.
- Käytä
react-native-screens-asetuksia.enableFreeze()jäädyttää taustanäytöt ja säästää muistia muille animaatioille — yksi rivi koodia, mitattava ero.
Yleisimmät virheet ja niiden korjaus
"Unmatched Route" -ilmoitus
Tämä tarkoittaa, ettei app/-hakemistossa ole vastaavaa tiedostoa. Tarkista tiedostonimen kirjoitusasu (kirjainkoolla on väliä) ja varmista, että +not-found.tsx on olemassa varareittinä.
Tyypitetyt reitit eivät päivity
Käynnistä Metro-paketointitoiminto uudelleen komennolla npx expo start --clear. Tyyppigenerointi tapahtuu kehityspalvelimen käynnistyksen yhteydessä, joten ilman uudelleenkäynnistystä se ei vain tapahdu.
Pinottu modaali ei sulkeudu eleellä
Varmista, että ylätason Stack.Screen on määritelty asetuksilla presentation: "modal", ei pelkkä card. Lisäksi iOS:llä eleen alkamisalue määräytyy gestureResponseDistance-arvosta.
Expo Router vs. React Navigation 7 — milloin valita kumpi?
React Navigation 7 julkaistiin alkuvuodesta 2026, ja se on edelleen Expo Routerin moottori sisäisesti. Pelkkä React Navigation on järkevä valinta, jos:
- sinulla on olemassa oleva, suuri sovellus, jossa navigaatio on jo modulaarinen;
- tarvitset täysin imperatiivisen, ei-tiedostopohjaisen rakenteen (esim. dynaamisesti palvelimelta tuleva reittikonfiguraatio);
- et halua Expon työkaluketjua etkä tarvitse web-yhteensopivuutta.
Kaikissa muissa tapauksissa Expo Router on vuonna 2026 mielestäni selkeästi parempi valinta: vähemmän koodia, valmis syvien linkkien tuki ja universaali web-renderöinti samalla koodikannalla. Rehellisesti sanottuna en muista milloin viimeksi aloitin uuden projektin pelkällä React Navigationilla.
Usein kysytyt kysymykset
Voiko Expo Routerin lisätä jo olemassa olevaan React Navigation -projektiin?
Kyllä, mutta se vaatii navigaattorirakenteen siirtämisen app/-hakemistoon ja entry pointin vaihtamisen. Suosittelemme tekemään muutoksen erillisessä haarassa ja siirtämään näkymät yksi kerrallaan — sieltä ei kannata yrittää oikoa. Käytä expo-router/entry päästäksesi alkuun.
Tukeeko Expo Router server-side renderingiä?
Kyllä. Expo Router 4 tukee staattista esirenderöintiä webille (web.output: "static") ja API-reittejä (app/api/-hakemisto). Mobiilisovelluksessa SSR ei luonnollisesti ole merkityksellinen, mutta sama koodi ajaa myös palvelinpäätä.
Miten käytän modaaleja Expo Routerissa?
Lisää Stack.Screen-asetus presentation: "modal" tai "formSheet". iOS 17+ tukee myös "pageSheet"-tyyliä mukautetuilla detenteilla, mikä toimii natiivisti React Native Screens 4 -kirjastolla.
Miksi router.push ei tee mitään?
Yleisin syy on, että Stack-navigaattori ei ole vielä monteerattu. Tarkista, että juuri-layoutissa on Stack-komponentti ja että reitin kohde löytyy app/-hakemistosta. Tyypitetyt reitit ovat paras tapa nähdä virhe käännösaikana.
Mikä on (group)-syntaksin tarkoitus?
Sulkeissa olevat hakemistot eivät näy URL-osoitteessa, joten voit organisoida tiedostoja loogisesti ilman polkuvaikutuksia. Tyypillisiä käyttötapauksia ovat (tabs), (auth) ja (modal) — kaikki samalla URL-tasolla, mutta omilla layouteillaan.
Yhteenveto
Expo Router 4 on vuonna 2026 ylivoimaisesti tuottavin tapa rakentaa React Native -navigaatio. Tiedostopohjainen rakenne, tyypitetyt reitit, valmiit syvät linkit ja Suspense-tuki säästävät satoja rivejä konfiguraatiokoodia (kokemuksesta tiedän). Yhdistettynä Expo SDK 53–55:n uuteen arkkitehtuuriin ja modernin tilanhallinnan kirjastoihin saat sovelluksen, joka käynnistyy nopeasti, navigoi sulavasti ja jakaa koodin saumattomasti web-version kanssa. Ei hassumpi paketti yhdelle kansiolle nimeltä app/.