Zašto je Expo Router promijenio pravila igre
Navigacija je oduvijek bila jedna od bolnih točaka React Native razvoja. Iskreno, tko se ne sjeća onih dana kad ste morali ručno konfigurirati navigatore, pisati tone boilerplate koda i boriti se s deep linkingom koji je zahtijevao posebnu konfiguraciju za svaku platformu zasebno? Dok su web developeri uživali u eleganciji file-based routinga u Next.js-u, mi na mobilnoj strani mogli smo samo zavidjeti.
Expo Router to je promijenio iz temelja.
Verzija 6, koja dolazi uz Expo SDK 55 (siječanj 2026.), donosi stvarno impresivne mogućnosti: Liquid Glass tabove za iOS 26, zaštićene rute s deklarativnom autentifikacijom, Link.Preview za kontekstualne menije na Appleu, server middleware i puno toga više. Navigacija u React Native aplikacijama nikad nije bila bliža pravom nativnom iskustvu.
U ovom vodiču proći ćemo sve — od osnova file-based routinga pa do naprednih obrazaca poput role-based pristupa i universal deep linkova. Uz praktične primjere koda koje možete odmah primijeniti u svojim projektima. Ajmo.
Osnove file-based routinga
Kako Expo Router mapira datoteke u rute
Temeljni koncept je zapravo banalno jednostavan: svaka datoteka u app/ direktoriju automatski postaje ruta u vašoj aplikaciji. Struktura mapa odražava URL strukturu — i to na svim platformama (Android, iOS, web). Kad jednom shvatite taj princip, sve ostalo dolazi prirodno.
app/
├── _layout.tsx → Root layout (obavija cijelu aplikaciju)
├── index.tsx → Ruta: /
├── about.tsx → Ruta: /about
├── settings/
│ ├── _layout.tsx → Layout za settings grupu
│ ├── index.tsx → Ruta: /settings
│ └── profile.tsx → Ruta: /settings/profile
└── blog/
├── [id].tsx → Dinamička ruta: /blog/123
└── [...slug].tsx → Catch-all ruta: /blog/a/b/c
Nema ručne konfiguracije navigatora. Nema linking.ts datoteka. Struktura direktorija jest vaša navigacijska konfiguracija — i to je cijela poanta.
Kreiranje novog Expo projekta s Routerom
Novi Expo projekti kreirani s SDK 55 automatski uključuju Expo Router, pa tu nema nikakve dodatne konfiguracije:
npx create-expo-app@latest --template default@sdk-55 MojaAplikacija
cd MojaAplikacija
npx expo start
To je doslovno sve. Tri naredbe i spremni ste za razvoj.
Root Layout — temelj svake aplikacije
Datoteka app/_layout.tsx je ulazna točka vaše navigacije. Ovdje definirate globalnu strukturu aplikacije — stack navigator, tab navigator ili neku kombinaciju. Osobno volim krenuti s jednostavnim stackom i nadograđivati po potrebi:
// app/_layout.tsx
import { Stack } from "expo-router";
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: "Početna" }} />
<Stack.Screen name="about" options={{ title: "O aplikaciji" }} />
<Stack.Screen name="settings" options={{ title: "Postavke" }} />
</Stack>
);
}
Navigacija između ekrana
Deklarativna navigacija s Link komponentom
Najjednostavniji način navigacije je <Link> komponenta. Radi poput <a> taga na webu, ali s nativnim prijelazima. Ako ste radili bilo kakav web razvoj, ovo će vam biti jako poznato:
import { View, Text } from "react-native";
import { Link } from "expo-router";
export default function HomeScreen() {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text style={{ fontSize: 24 }}>Početna stranica</Text>
<Link href="/about" style={{ marginTop: 20, color: "blue" }}>
O aplikaciji
</Link>
<Link href="/blog/42" style={{ marginTop: 10, color: "blue" }}>
Članak #42
</Link>
<Link
href={{
pathname: "/settings/profile",
params: { tab: "sigurnost" },
}}
style={{ marginTop: 10, color: "blue" }}
>
Postavke profila
</Link>
</View>
);
}
Programatska navigacija s useRouter
Za navigaciju iz event handlera, API poziva ili drugih imperativnih konteksta, tu je useRouter hook. Ovo koristite kad vam treba više kontrole — recimo nakon uspješnog logina ili obrade forme:
import { useRouter } from "expo-router";
import { Button, View } from "react-native";
export default function LoginScreen() {
const router = useRouter();
async function handleLogin() {
const uspjeh = await prijaviKorisnika();
if (uspjeh) {
// Zamijeni trenutni ekran (korisnik se ne može vratiti na login)
router.replace("/(app)/home");
} else {
// Navigiraj na ekran za pogrešku (može se vratiti nazad)
router.push("/error");
}
}
return (
<View style={{ flex: 1, justifyContent: "center", padding: 20 }}>
<Button title="Prijavi se" onPress={handleLogin} />
<Button title="Nazad" onPress={() => router.back()} />
</View>
);
}
Evo pregled ključnih metoda useRouter hooka:
- router.push(ruta) — dodaje novi ekran na stack (korisnik se može vratiti)
- router.replace(ruta) — zamjenjuje trenutni ekran (nema povratka, korisno za login flow)
- router.back() — vraća se na prethodni ekran
- router.navigate(ruta) — pametna navigacija koja izbjegava duplikate na stacku
- router.dismiss() — zatvara modalni ekran
Stack navigacija — temelj mobilnih aplikacija
Stack navigator je najosnovniji (i vjerojatno najkorišteniji) obrazac navigacije u mobilnim aplikacijama. Svaki novi ekran se animirano postavlja na vrh prethodnog, a korisnik se vraća swipeom ili gumbom za nazad. Jednostavno, intuitivno, provjereno.
// app/products/_layout.tsx
import { Stack } from "expo-router";
export default function ProductsLayout() {
return (
<Stack
screenOptions={{
headerStyle: { backgroundColor: "#1a1a2e" },
headerTintColor: "#e94560",
headerTitleStyle: { fontWeight: "bold" },
animation: "slide_from_right",
}}
>
<Stack.Screen
name="index"
options={{ title: "Proizvodi" }}
/>
<Stack.Screen
name="[id]"
options={{ title: "Detalji proizvoda" }}
/>
<Stack.Screen
name="cart"
options={{
title: "Košarica",
presentation: "modal",
}}
/>
</Stack>
);
}
Dinamičke rute s parametrima
Dinamičke rute koriste uglate zagrade u imenu datoteke — baš kao u Next.js-u. Parametri se čitaju pomoću useLocalSearchParams hooka:
// app/products/[id].tsx
import { useLocalSearchParams } from "expo-router";
import { View, Text, ActivityIndicator } from "react-native";
import { useEffect, useState } from "react";
interface Proizvod {
id: string;
naziv: string;
cijena: number;
opis: string;
}
export default function ProductDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const [proizvod, setProizvod] = useState<Proizvod | null>(null);
const [ucitavanje, setUcitavanje] = useState(true);
useEffect(() => {
async function dohvatiProizvod() {
try {
const res = await fetch(`https://api.example.com/products/${id}`);
const data = await res.json();
setProizvod(data);
} catch (error) {
console.error("Greška pri dohvaćanju:", error);
} finally {
setUcitavanje(false);
}
}
dohvatiProizvod();
}, [id]);
if (ucitavanje) return <ActivityIndicator size="large" />;
if (!proizvod) return <Text>Proizvod nije pronađen</Text>;
return (
<View style={{ flex: 1, padding: 20 }}>
<Text style={{ fontSize: 28, fontWeight: "bold" }}>
{proizvod.naziv}
</Text>
<Text style={{ fontSize: 22, color: "#e94560", marginTop: 10 }}>
{proizvod.cijena} €
</Text>
<Text style={{ marginTop: 15, lineHeight: 24 }}>
{proizvod.opis}
</Text>
</View>
);
}
Tab navigacija i Liquid Glass tabovi
Klasični tabovi s Expo Routerom
Tab navigacija je drugi najčešći obrazac u mobilnim appovima. Grupe ruta unutar zagrada (tabs) definiraju tab strukturu:
app/
├── (tabs)/
│ ├── _layout.tsx → Tab navigator layout
│ ├── index.tsx → Tab: Početna
│ ├── search.tsx → Tab: Pretraži
│ ├── favorites.tsx → Tab: Favoriti
│ └── profile.tsx → Tab: Profil
// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: "#e94560",
tabBarInactiveTintColor: "#999",
tabBarStyle: {
backgroundColor: "#0f0f23",
borderTopColor: "#1a1a2e",
},
}}
>
<Tabs.Screen
name="index"
options={{
title: "Početna",
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="search"
options={{
title: "Pretraži",
tabBarIcon: ({ color, size }) => (
<Ionicons name="search" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="favorites"
options={{
title: "Favoriti",
tabBarIcon: ({ color, size }) => (
<Ionicons name="heart" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: "Profil",
tabBarIcon: ({ color, size }) => (
<Ionicons name="person" size={size} color={color} />
),
}}
/>
</Tabs>
);
}
NativeTabs i Liquid Glass — pravi nativni tabovi
E sad dolazimo do stvarno zanimljivog dijela. Expo Router v6 donosi potpuno novu vrstu tab navigacije: NativeTabs. Umjesto da React Native rekreira izgled tab bara u JavaScriptu (što je donekle uvijek izgledalo "skoro nativno, ali ne baš"), NativeTabs koristi UITabBarController na iOS-u i nativnu tab komponentu na Androidu.
Na iOS 26 automatski dobivate Liquid Glass efekt — onu prozirnu, dinamičku traku koja se prilagođava sadržaju ispod nje. Izgleda fenomenalno.
// app/(tabs)/_layout.tsx
import { NativeTabs } from "expo-router/unstable-native-tabs";
import { PlatformColor } from "react-native";
export default function TabLayout() {
return (
<NativeTabs
minimizeBehavior="onScrollDown"
sidebarAdaptable
>
<NativeTabs.Trigger name="index">
<NativeTabs.Tab
title="Početna"
systemIcon="house"
badge={3}
/>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="search">
<NativeTabs.Tab
title="Pretraži"
systemIcon="magnifyingglass"
/>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="favorites">
<NativeTabs.Tab
title="Favoriti"
systemIcon="heart"
/>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="profile">
<NativeTabs.Tab
title="Profil"
systemIcon="person.crop.circle"
/>
</NativeTabs.Trigger>
</NativeTabs>
);
}
Ključne prednosti NativeTabs komponente:
- Liquid Glass na iOS 26 — prozirna traka s efektom koji se automatski prilagođava pozadini, bez ikakvog JavaScript koda za blur
- minimizeBehavior — tab bar se automatski minimizira pri scrollanju (možete postaviti
onScrollDown,onScrollUp,automaticilinever) - Nativne performanse — blur se renderira na GPU-u, tako da nema utjecaja na frame rate
- SF Symbols na iOS-u — koristite Apple sistemske ikone putem
systemIconpropsa - sidebarAdaptable — automatski se prilagođava iPadu s bočnom navigacijom
Jedna stvar koju treba imati na umu: NativeTabs API je trenutno u alpha fazi (dostupan od SDK 54+) i trebat će vam Xcode 26 za testiranje Liquid Glass efekta. Ali čak i bez Liquid Glassa, nativni tabovi su zamjetno bolji od JavaScript implementacije.
Route grupe — organizacija bez utjecaja na URL
Direktoriji s imenom u zagradama, poput (auth) ili (app), su route grupe. Oni služe za logičku organizaciju ruta bez da utječu na URL strukturu. Ovo je jedan od onih koncepata koji je jednostavan ali nevjerojatno koristan u praksi:
app/
├── _layout.tsx → Root layout
├── (auth)/ → Grupa za autentifikaciju
│ ├── _layout.tsx
│ ├── login.tsx → Ruta: /login
│ └── register.tsx → Ruta: /register
├── (app)/ → Grupa za glavnu aplikaciju
│ ├── _layout.tsx
│ ├── (tabs)/
│ │ ├── _layout.tsx
│ │ ├── home.tsx → Ruta: /home
│ │ └── profile.tsx → Ruta: /profile
│ └── settings.tsx → Ruta: /settings
Primjetite da se (auth) i (app) ne pojavljuju u URL-u. Datoteka (app)/(tabs)/home.tsx ima rutu /home, ne /app/tabs/home. Organizirate kod prema logičkim cjelinama, a URL-ovi ostaju čisti. Win-win.
Zaštićene rute i autentifikacija
Stack.Protected — deklarativna zaštita ruta
Ovo je po mom mišljenju jedna od najboljih stvari u Expo Routeru. Od SDK 53, postoje Stack.Protected i Tabs.Protected komponente koje drastično pojednostavljuju autentifikacijske tokove. Nema više ručnog preusmjeravanja u useEffect-u — zaštita se definira deklarativno, direktno u layoutu:
// contexts/AuthContext.tsx
import { createContext, useContext, useState, useEffect } from "react";
import * as SecureStore from "expo-secure-store";
interface AuthState {
korisnik: { id: string; ime: string; uloga: string } | null;
token: string | null;
ucitavanje: boolean;
}
interface AuthContextType extends AuthState {
prijava: (email: string, lozinka: string) => Promise<void>;
odjava: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState<AuthState>({
korisnik: null,
token: null,
ucitavanje: true,
});
useEffect(() => {
async function provjeriSesiju() {
try {
const token = await SecureStore.getItemAsync("auth_token");
if (token) {
const res = await fetch("https://api.example.com/me", {
headers: { Authorization: `Bearer ${token}` },
});
const korisnik = await res.json();
setState({ korisnik, token, ucitavanje: false });
} else {
setState({ korisnik: null, token: null, ucitavanje: false });
}
} catch {
setState({ korisnik: null, token: null, ucitavanje: false });
}
}
provjeriSesiju();
}, []);
async function prijava(email: string, lozinka: string) {
const res = await fetch("https://api.example.com/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, lozinka }),
});
const { token, korisnik } = await res.json();
await SecureStore.setItemAsync("auth_token", token);
setState({ korisnik, token, ucitavanje: false });
}
async function odjava() {
await SecureStore.deleteItemAsync("auth_token");
setState({ korisnik: null, token: null, ucitavanje: false });
}
return (
<AuthContext.Provider value={{ ...state, prijava, odjava }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error("useAuth mora biti unutar AuthProvidera");
return context;
}
Implementacija zaštićenih ruta u layoutu
A sad pogledajte koliko je elegantna sama implementacija u root layoutu:
// app/_layout.tsx
import { Stack } from "expo-router";
import { AuthProvider, useAuth } from "../contexts/AuthContext";
import { ActivityIndicator, View } from "react-native";
function RootNavigator() {
const { korisnik, ucitavanje } = useAuth();
if (ucitavanje) {
return (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator size="large" />
</View>
);
}
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Protected guard={!!korisnik}>
<Stack.Screen name="(app)" />
</Stack.Protected>
<Stack.Protected guard={!korisnik}>
<Stack.Screen name="(auth)" />
</Stack.Protected>
</Stack>
);
}
export default function RootLayout() {
return (
<AuthProvider>
<RootNavigator />
</AuthProvider>
);
}
Kako ovo funkcionira u praksi:
- Kada se aplikacija pokrene,
ucitavanjejetrue— prikazuje se loading indikator - Ako je korisnik prijavljen (
!!korisnik === true),(app)grupa je dostupna, a(auth)nije - Ako korisnik nije prijavljen, obrnuto —
(auth)je dostupna,(app)nije - Promjena stanja automatski navigira na prvi dostupni ekran — bez eksplicitnog
router.replace()poziva - Deep link na zaštićeni ekran automatski preusmjerava neprijavljene korisnike
Nema if/else u svakoj komponenti. Nema redirect logike razbacane po cijeloj aplikaciji. Čisto i pregledno.
Zaštićeni tabovi prema ulogama korisnika
Tabs.Protected omogućuje prikaz ili skrivanje pojedinih tabova na temelju korisničkih uloga. Recimo da imate VIP sekciju i admin panel — samo relevantni korisnici trebaju vidjeti te tabove:
// app/(app)/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
import { useAuth } from "../../../contexts/AuthContext";
export default function AppTabsLayout() {
const { korisnik } = useAuth();
const jeAdmin = korisnik?.uloga === "admin";
const jeVip = korisnik?.uloga === "vip" || jeAdmin;
return (
<Tabs>
<Tabs.Screen name="home" options={{ title: "Početna" }} />
<Tabs.Screen name="search" options={{ title: "Pretraži" }} />
<Tabs.Protected guard={jeVip}>
<Tabs.Screen name="exclusive" options={{ title: "VIP" }} />
</Tabs.Protected>
<Tabs.Protected guard={jeAdmin}>
<Tabs.Screen name="admin" options={{ title: "Admin" }} />
</Tabs.Protected>
<Tabs.Screen name="profile" options={{ title: "Profil" }} />
</Tabs>
);
}
Važna napomena: zaštićene rute funkcioniraju samo na klijentskoj strani. Nisu zamjena za autentifikaciju na serveru! Uvijek validirajte pristup na backend API-ju — ovo je isključivo UX poboljšanje.
Deep linking — univerzalne veze do svakog ekrana
Automatski deep linking
Jedna od najjačih strana Expo Routera je što je svaki ekran automatski deep linkable. Budući da je URL struktura izravno mapirana iz datotečne strukture, svaki ekran ima jedinstveni URL koji radi na svim platformama. Bez ikakve dodatne konfiguracije. Ozbiljno.
Konfiguracija u app.json:
{
"expo": {
"scheme": "mojaaplikacija",
"plugins": [
[
"expo-router",
{
"origin": "https://mojaaplikacija.com"
}
]
]
}
}
S ovom konfiguracijom, vaša aplikacija automatski reagira na:
- Custom URL scheme:
mojaaplikacija://blog/42 - Universal Links (iOS):
https://mojaaplikacija.com/blog/42 - App Links (Android):
https://mojaaplikacija.com/blog/42
Universal Links i App Links
Za produkcijske aplikacije, svakako koristite Universal Links (iOS) i App Links (Android) jer koriste standardne https:// URL-ove. Velika prednost je što se, ako aplikacija nije instalirana, korisnik preusmjerava na web stranicu ili u trgovinu aplikacija.
Konfiguracija za iOS zahtijeva apple-app-site-association (AASA) datoteku na vašem domenu:
{
"applinks": {
"apps": [],
"details": [
{
"appIDs": ["TEAM_ID.com.mojafirma.mojaaplikacija"],
"components": [
{ "/": "/blog/*" },
{ "/": "/products/*" },
{ "/": "/profile" }
]
}
]
}
}
Za Android, potrebna je verifikacija putem Digital Asset Links datoteke (/.well-known/assetlinks.json) na vašem domenu.
Testiranje deep linkova
Deep linkove možete (i trebate!) testirati korištenjem CLI-ja:
# iOS Simulator
npx uri-scheme open "mojaaplikacija://blog/42" --ios
# Android Emulator
npx uri-scheme open "mojaaplikacija://blog/42" --android
# Ili s Expo alatima
npx expo start --deep-link "mojaaplikacija://blog/42"
Nove mogućnosti u Expo Router v6
Link.Preview i Link.Menu — kontekstualni menije na iOS-u
Expo Router v6 uvodi Link.Preview i Link.Menu za Apple platforme. Znate onaj prepoznatljivi iOS UX kad dugo pritisnete link i dobijete preview s kontekstualnim menijem? E, sad to možete implementirati s par linija koda:
import { Link } from "expo-router";
import { Text, View, Image } from "react-native";
export default function ArticleCard({ article }) {
return (
<Link.Trigger>
<Link href={`/blog/${article.id}`}>
<View style={{ padding: 16 }}>
<Text style={{ fontSize: 18, fontWeight: "bold" }}>
{article.naslov}
</Text>
<Text style={{ color: "#666", marginTop: 4 }}>
{article.izvadak}
</Text>
</View>
</Link>
<Link.Preview>
<View style={{ width: 300, padding: 20 }}>
<Image
source={{ uri: article.slika }}
style={{ width: "100%", height: 200, borderRadius: 12 }}
/>
<Text style={{ fontSize: 20, fontWeight: "bold", marginTop: 12 }}>
{article.naslov}
</Text>
<Text style={{ marginTop: 8, color: "#444" }}>
{article.sadrzaj.substring(0, 200)}...
</Text>
</View>
</Link.Preview>
<Link.Menu>
<Link.Action title="Spremi" systemIcon="bookmark" />
<Link.Action title="Dijeli" systemIcon="square.and.arrow.up" />
<Link.Action
title="Prijavi"
systemIcon="flag"
destructive
/>
</Link.Menu>
</Link.Trigger>
);
}
Apple Zoom Transition
SDK 55 donosi i nativne shared element prijelaze na iOS-u, omogućene prema zadanim postavkama. Kad korisnik dodirne karticu, element se fluidno "zumira" u puni ekran. Bez biblioteka trećih strana, bez komplicirane konfiguracije:
// U Stack navigatoru
<Stack.Screen
name="details"
options={{
animation: "zoom",
}}
/>
Da, to je stvarno sve što trebate napisati. Jedna linija za animaciju.
Stack.Toolbar — nativna alatna traka na iOS-u
Nova Stack.Toolbar komponenta koristi nativni UIToolbar za postavljanje akcija na dno ekrana. Savršeno za editore, chat ekrane i slične use caseove:
import { Stack } from "expo-router";
import { ToolbarItem } from "expo-router";
export default function EditorScreen() {
return (
<>
<Stack.Screen
options={{
title: "Uređivač",
toolbar: () => (
<Stack.Toolbar>
<ToolbarItem
title="Podebljaj"
systemIcon="bold"
onPress={() => primijeniStil("bold")}
/>
<ToolbarItem
title="Kurziv"
systemIcon="italic"
onPress={() => primijeniStil("italic")}
/>
<ToolbarItem
title="Spremi"
systemIcon="checkmark"
onPress={spremi}
/>
</Stack.Toolbar>
),
}}
/>
{/* Sadržaj ekrana */}
</>
);
}
Server Middleware (eksperimentalno)
Expo Router v6 uvodi i eksperimentalnu podršku za server middleware — prvi korak prema punom SSR-u na webu. Ako vam treba server-side logika poput provjere autentifikacije ili preusmjeravanja, ovo je pravi alat za to:
// app/+middleware.ts
import { type ExpoRequest, type ExpoResponse } from "expo-router/server";
export function middleware(request: ExpoRequest): ExpoResponse | void {
const authHeader = request.headers.get("Authorization");
// Provjera autentifikacije na serverskoj strani
if (request.url.includes("/api/admin")) {
if (!authHeader || !validirajToken(authHeader)) {
return new Response("Neovlašten pristup", { status: 401 });
}
}
// Preusmjeravanje
if (request.url.includes("/stari-url")) {
return Response.redirect(new URL("/novi-url", request.url), 301);
}
}
function validirajToken(header: string): boolean {
const token = header.replace("Bearer ", "");
// Vaša logika validacije tokena
return token.length > 0;
}
Middleware se izvršava samo na serverskoj strani — klijentska navigacija (nativna ili web putem <Link>) ne prolazi kroz server. To je bitno za razumjeti kako biste znali gdje staviti koju logiku.
Tipski sigurne rute s TypeScriptom
Ako koristite TypeScript (a trebali biste, pogotovo u većim projektima), Expo Router automatski generira tipove za sve rute u vašem projektu. To znači autosugestije i provjeru tipova pri navigaciji — nema više literalnih stringova koji se tiho pokvare nakon preimenovanja datoteke:
// Expo Router automatski generira tipove u .expo/types/
// Nakon pokretanja: npx expo customize tsconfig.json
import { Link, useRouter } from "expo-router";
export default function NavigationExample() {
const router = useRouter();
return (
<>
{/* TypeScript javlja grešku ako ruta ne postoji */}
<Link href="/blog/42">Ispravan link</Link>
{/* Tipski sigurna programatska navigacija */}
<Button
title="Idi na profil"
onPress={() => router.push("/profile")}
/>
{/* Dinamički parametri su također tipski sigurni */}
<Link
href={{
pathname: "/products/[id]",
params: { id: "abc123" },
}}
>
Proizvod
</Link>
</>
);
}
Za aktiviranje generiranja tipova, pokrenite:
npx expo customize tsconfig.json
Expo Router će automatski ažurirati tipove svaki put kad dodate ili uklonite datoteku iz app/ direktorija. Jedna manje stvar o kojoj morate razmišljati.
Modalna navigacija
Modalni ekrani su odlični za forme, dijaloge i sadržaj koji se prikazuje privremeno iznad glavnog ekrana. Implementacija je prilično direktna:
// app/_layout.tsx
import { Stack } from "expo-router";
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{
presentation: "modal",
animation: "slide_from_bottom",
headerTitle: "Nova objava",
}}
/>
</Stack>
);
}
// app/modal.tsx
import { useRouter } from "expo-router";
import { View, Text, Button, TextInput, StyleSheet } from "react-native";
import { useState } from "react";
export default function ModalScreen() {
const router = useRouter();
const [tekst, setTekst] = useState("");
function handleSpremi() {
// Spremi podatke...
router.dismiss(); // Zatvori modal
}
return (
<View style={styles.container}>
<Text style={styles.naslov}>Kreiraj novu objavu</Text>
<TextInput
style={styles.input}
placeholder="Napiši nešto..."
value={tekst}
onChangeText={setTekst}
multiline
/>
<Button title="Spremi" onPress={handleSpremi} />
<Button title="Odustani" onPress={() => router.dismiss()} />
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 20, backgroundColor: "#fff" },
naslov: { fontSize: 24, fontWeight: "bold", marginBottom: 20 },
input: {
borderWidth: 1,
borderColor: "#ddd",
borderRadius: 8,
padding: 12,
minHeight: 120,
marginBottom: 20,
textAlignVertical: "top",
},
});
Najbolje prakse za navigaciju u 2026.
Evo sažetak onoga što sam naučio radeći s Expo Routerom na nekoliko projekata:
- Koristite route grupe za organizaciju — odvajajte autentifikacijske ekrane od glavne aplikacije pomoću
(auth)i(app)grupa. Čini strukturu projekta puno preglednijom - Držite layoute jednostavnima — svaki
_layout.tsxtrebao bi imati jednu jasnu odgovornost. Ako postane prekompleksan, vjerojatno trebate dodati podgrupu - Preferirajte Link nad router.push — deklarativna navigacija je lakša za održavanje i bolja za pristupačnost
- Aktivirajte TypeScript tipove — par minuta setup-a, a uštedi vam sate debuggiranja pogrešnih ruta
- Testirajte deep linkove rano — ne čekajte produkciju da biste otkrili pokvarene linkove (iz iskustva govorim)
- NativeTabs za nove projekte — ako ciljate iOS 26+, koristite NativeTabs za pravi nativni izgled
- Uvijek validirajte na serveru — zaštićene rute su klijentska funkcionalnost, API endpointovi moraju imati vlastitu autorizaciju
Često postavljana pitanja
Koja je razlika između Expo Routera i React Navigationa?
Expo Router je izgrađen na vrhu React Navigationa, tako da u pozadini koristi iste primitives. Ključna razlika je u pristupu: React Navigation koristi imperativnu konfiguraciju gdje ručno definirate navigatore i ekrane u kodu, dok Expo Router koristi file-based pristup gdje struktura datoteka automatski definira rute. Plus, Expo Router automatski podržava deep linking, tipski sigurne rute i universal navigation (Android, iOS, web) bez dodatne konfiguracije. Za nove projekte u 2026., Expo Router je definitivno preporučeni izbor.
Mogu li koristiti Expo Router bez Expo frameworka?
Kratki odgovor — ne. Expo Router se oslanja na Expo CLI, Metro bundler konfiguraciju i Expo module ecosystem. Ali to nije loša stvar — korištenje Expa ne znači ograničenja. S Expo prebuildingom možete pristupiti svim nativnim modulima i konfiguracijama jednako kao s bare React Native projektom. Expo je danas de facto standard za React Native razvoj, i to s dobrim razlogom.
Kako implementirati nested navigaciju sa stackom unutar tabova?
Jednostavno kreirate poddirektorij unutar tab grupe s vlastitim _layout.tsx. Recimo, za stack navigaciju unutar Home taba, napravite app/(tabs)/home/ direktorij s _layout.tsx (koji je Stack navigator), index.tsx (početni ekran) i dodatnim ekranima poput details.tsx. Stack navigacija unutar taba radi potpuno neovisno od tab navigacije.
Jesu li zaštićene rute sigurne za produkciju?
Zaštićene rute funkcioniraju isključivo na klijentskoj strani. Korisne su za UX — sprječavaju neovlaštene korisnike da vide ekrane na koje nemaju pravo. Međutim, nikad se ne oslanjajte samo na njih za sigurnost. Vaš backend API mora provjeravati autorizaciju za svaki zahtjev, neovisno o tome što klijentska strana radi. To je zlatno pravilo.
Kako NativeTabs rade na starijim verzijama iOS-a?
NativeTabs koriste nativne komponente na svim verzijama iOS-a. Na iOS 26+ automatski dobivate Liquid Glass efekt — prozirnu, dinamičku tab traku. Na starijim verzijama prikazuje se klasični izgled bez Liquid Glass efekta, ali i dalje s nativnim performansama i ponašanjem. Na Androidu, NativeTabs koriste vlastitu nativnu implementaciju s platformskim specifičnostima (npr. popover pri dugom pritisku na tab).