Expo Router 2026: Tiedostopohjainen reititys React Native -sovelluksissa

Käytännönläheinen 2026-opas Expo Router 4:ään: tiedostopohjainen reititys, layoutit, dynaamiset reitit, autentikoinnin suojaus, syvät linkit ja suorituskykyvinkit React Native -sovelluksiin.

Expo Router 4 Opas 2026: Reititys & Syvät Linkit

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.tsx muodostaa 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-asetusta lazy: true, jotta välilehtiä ei renderöidä ennen niiden avaamista.
  • Vältä globaaleja parametreja. useGlobalSearchParams aiheuttaa 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/.

Tietoa Kirjoittajasta Editorial Team

Our team of expert writers and editors.