Norsk

Frigjør kraften i Next.js App Router med vår dybdegående guide til filbasert ruting. Lær hvordan du strukturerer applikasjonen din, lager dynamiske ruter, håndterer layouter og mer.

Next.js App Router: En Omfattende Guide til Filbasert Ruting

Next.js App Router, introdusert i Next.js 13 og standard i senere versjoner, revolusjonerer hvordan vi strukturerer og navigerer i applikasjoner. Den introduserer et kraftig og intuitivt filbasert rutingsystem som forenkler utvikling, forbedrer ytelse og hever den generelle utvikleropplevelsen. Denne omfattende guiden vil dykke dypt inn i App Routerens filbaserte ruting, og gi deg kunnskapen og ferdighetene til å bygge robuste og skalerbare Next.js-applikasjoner.

Hva er Filbasert Ruting?

Filbasert ruting er et rutingsystem der strukturen på applikasjonens ruter bestemmes direkte av organiseringen av filene og mappene dine. I Next.js App Router definerer du ruter ved å opprette filer i `app`-mappen. Hver mappe representerer et rutesegment, og spesielle filer i disse mappene definerer hvordan det rutesegmentet skal håndteres. Denne tilnærmingen gir flere fordeler:

Komme i Gang med App Router

For å bruke App Router, må du opprette et nytt Next.js-prosjekt eller migrere et eksisterende prosjekt. Sørg for at du bruker Next.js versjon 13 eller nyere.

Opprette et Nytt Prosjekt:

Du kan opprette et nytt Next.js-prosjekt med App Router ved å bruke følgende kommando:

npx create-next-app@latest my-app --example with-app

Migrere et Eksisterende Prosjekt:

For å migrere et eksisterende prosjekt, må du flytte sidene dine fra `pages`-mappen til `app`-mappen. Du må kanskje justere rutingslogikken din tilsvarende. Next.js tilbyr en migreringsguide for å hjelpe deg med denne prosessen.

Kjernekonsepter for Filbasert Ruting

App Router introduserer flere spesielle filer og konvensjoner som definerer hvordan rutene dine håndteres:

1. `app`-mappen

`app`-mappen er roten for applikasjonens ruter. Alle filer og mapper i denne mappen vil bli brukt til å generere ruter. Alt utenfor `app`-mappen (som `pages`-mappen hvis du migrerer) vil bli ignorert av App Router.

2. `page.js`-filen

`page.js` (eller `page.jsx`, `page.ts`, `page.tsx`)-filen er den mest grunnleggende delen av App Router. Den definerer UI-komponenten som skal rendres for et spesifikt rutesegment. Det er en **påkrevd** fil for ethvert rutesegment du vil ha direkte tilgjengelig.

Eksempel:

Hvis du har en filstruktur som dette:

app/
  about/
    page.js

Komponenten som eksporteres fra `app/about/page.js` vil bli rendret når en bruker navigerer til `/about`.

// app/about/page.js
import React from 'react';

export default function AboutPage() {
  return (
    <div>
      <h1>Om Oss</h1>
      <p>Lær mer om vårt selskap.</p>
    </div>
  );
}

3. `layout.js`-filen

`layout.js` (eller `layout.jsx`, `layout.ts`, `layout.tsx`)-filen definerer et brukergrensesnitt som deles på tvers av flere sider innenfor et rutesegment. Layouter er nyttige for å lage konsistente headere, footere, sidebarer og andre elementer som skal være til stede på flere sider.

Eksempel:

La oss si at du vil legge til en header på både `/about`-siden og en hypotetisk `/about/team`-side. Du kan opprette en `layout.js`-fil i `app/about`-mappen:

// app/about/layout.js
import React from 'react';

export default function AboutLayout({ children }) {
  return (
    <div>
      <header>
        <h1>Om Vårt Selskap</h1>
      </header>
      <main>{children}</main>
    </div>
  );
}

`children`-propen vil bli erstattet med UI-et som rendres av `page.js`-filen i samme mappe eller i eventuelle nestede mapper.

4. `template.js`-filen

`template.js`-filen ligner på `layout.js`, men den oppretter en ny instans av komponenten for hver barn-rute. Dette er nyttig for scenarier der du vil opprettholde komponenttilstand eller forhindre re-rendringer når du navigerer mellom barn-ruter. I motsetning til layouter, vil maler re-rendre ved navigering. Bruk av maler er flott for å animere elementer ved navigering.

Eksempel:

// app/template.js
'use client'

import { useState } from 'react'

export default function Template({ children }) {
  const [count, setCount] = useState(0)

  return (
    <main>
      <p>Mal: {count}</p>
      <button onClick={() => setCount(count + 1)}>Oppdater mal</button>
      {children}
    </main>
  )
}

5. `loading.js`-filen

`loading.js` (eller `loading.jsx`, `loading.ts`, `loading.tsx`)-filen lar deg lage et laste-UI som vises mens et rutesegment lastes. Dette er nyttig for å gi en bedre brukeropplevelse når du henter data eller utfører andre asynkrone operasjoner.

Eksempel:

// app/about/loading.js
import React from 'react';

export default function Loading() {
  return <p>Laster informasjon om oss...</p>;
}

Når en bruker navigerer til `/about`, vil `Loading`-komponenten vises til `page.js`-komponenten er fullstendig rendret.

6. `error.js`-filen

`error.js` (eller `error.jsx`, `error.ts`, `error.tsx`)-filen lar deg lage et tilpasset feil-UI som vises når en feil oppstår i et rutesegment. Dette er nyttig for å gi en mer brukervennlig feilmelding og forhindre at hele applikasjonen krasjer.

Eksempel:

// app/about/error.js
'use client'

import React from 'react';

export default function Error({ error, reset }) {
  return (
    <div>
      <h2>En feil har oppstått!</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>Prøv igjen</button>
    </div>
  );
}

Hvis en feil oppstår under renderingen av `/about`-siden, vil `Error`-komponenten vises. `error`-propen inneholder informasjon om feilen, og `reset`-funksjonen lar brukeren forsøke å laste siden på nytt.

7. Rutegrupper

Rutegrupper `(gruppenavn)` lar deg organisere rutene dine uten å påvirke URL-strukturen. De opprettes ved å pakke et mappenavn i parentes. Dette er spesielt nyttig for å organisere layouter og delte komponenter.

Eksempel:

app/
  (marketing)/
    about/
      page.js
    contact/
      page.js
  (shop)/
    products/
      page.js

I dette eksempelet er `about`- og `contact`-sidene gruppert under `marketing`-gruppen, og `products`-siden er under `shop`-gruppen. URL-ene forblir henholdsvis `/about`, `/contact` og `/products`.

8. Dynamiske Ruter

Dynamiske ruter lar deg lage ruter med variable segmenter. Dette er nyttig for å vise innhold basert på data hentet fra en database eller API. Dynamiske rutesegmenter defineres ved å pakke segmentnavnet i hakeparenteser (f.eks. `[id]`)

Eksempel:

La oss si at du vil lage en rute for å vise individuelle blogginnlegg basert på deres ID. Du kan lage en filstruktur som dette:

app/
  blog/
    [id]/
      page.js

`[id]`-segmentet er et dynamisk segment. Komponenten som eksporteres fra `app/blog/[id]/page.js` vil bli rendret når en bruker navigerer til en URL som `/blog/123` eller `/blog/456`. Verdien av `id`-parameteren vil være tilgjengelig i `params`-propen til komponenten.

// app/blog/[id]/page.js
import React from 'react';

export default async function BlogPost({ params }) {
  const { id } = params;

  // Hent data for blogginnlegget med den gitte ID-en
  const post = await fetchBlogPost(id);

  if (!post) {
    return <p>Blogginnlegg ikke funnet.</p>;
  }

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

async function fetchBlogPost(id) {
  // Simulerer henting av data fra en database eller API
  return new Promise((resolve) => {
    setTimeout(() => {
      const posts = {
        '123': { title: 'Mitt Første Blogginnlegg', content: 'Dette er innholdet i mitt første blogginnlegg.' },
        '456': { title: 'Et Annet Blogginnlegg', content: 'Dette er noe mer spennende innhold.' },
      };
      resolve(posts[id] || null);
    }, 500);
  });
}

Du kan også bruke flere dynamiske segmenter i en rute. For eksempel kan du ha en rute som `/blog/[category]/[id]`.

9. Catch-all-segmenter

Catch-all-segmenter lar deg lage ruter som matcher et hvilket som helst antall segmenter. Dette er nyttig for scenarioer som å lage et CMS der URL-strukturen bestemmes av brukeren. Catch-all-segmenter defineres ved å legge til tre prikker før segmentnavnet (f.eks. `[...slug]`)

Eksempel:

app/
  docs/
    [...slug]/
      page.js

`[...slug]`-segmentet vil matche et hvilket som helst antall segmenter etter `/docs`. For eksempel vil det matche `/docs/getting-started`, `/docs/api/users` og `/docs/advanced/configuration`. Verdien av `slug`-parameteren vil være en matrise som inneholder de matchede segmentene.

// app/docs/[...slug]/page.js
import React from 'react';

export default function DocsPage({ params }) {
  const { slug } = params;

  return (
    <div>
      <h1>Dokumentasjon</h1>
      <p>Slug: {slug ? slug.join('/') : 'Ingen slug'}</p>
    </div>
  );
}

Valgfrie catch-all-segmenter kan opprettes ved å legge til segmentnavnet i doble hakeparenteser `[[...slug]]`. Dette gjør rutesegmentet valgfritt. Eksempel:

app/
  blog/
    [[...slug]]/
      page.js

Denne oppsettet vil rendre page.js-komponenten både på `/blog` og `/blog/any/number/of/segments`.

10. Parallelle Ruter

Parallelle ruter lar deg samtidig rendre en eller flere sider i samme layout. Dette er spesielt nyttig for komplekse layouter, som dashboards, der forskjellige deler av siden kan lastes uavhengig. Parallelle ruter defineres ved hjelp av `@`-symbolet etterfulgt av et slot-navn (f.eks. `@sidebar`, `@main`).

Eksempel:

app/
  @sidebar/
    page.js  // Innhold for sidebaren
  @main/
    page.js  // Innhold for hovedseksjonen
  default.js // Påkrevd: Definerer standardlayouten for parallelle ruter

`default.js`-filen er påkrevd når du bruker parallelle ruter. Den definerer hvordan de forskjellige slot-ene kombineres for å lage den endelige layouten.

// app/default.js
export default function RootLayout({ children: { sidebar, main } }) {
  return (
    <div style={{ display: 'flex' }}>
      <aside style={{ width: '200px', backgroundColor: '#f0f0f0' }}>
        {sidebar}
      </aside>
      <main style={{ flex: 1, padding: '20px' }}>
        {main}
      </main>
    </div>
  );
}

11. Avskjærende Ruter

Avskjærende ruter (Intercepting Routes) lar deg laste en rute fra en annen del av applikasjonen din innenfor den nåværende layouten. Dette er nyttig for å lage modaler, bildegallerier og andre UI-elementer som skal vises oppå eksisterende sideinnhold. Avskjærende ruter defineres ved hjelp av `(..)`-syntaksen, som indikerer hvor mange nivåer opp i mappestrukturen man skal gå for å finne den avskårne ruten.

Eksempel:

app/
  (.)photos/
    [id]/
      page.js  // Den avskårne ruten
  feed/
    page.js  // Siden der fotomodalen vises

I dette eksempelet, når en bruker klikker på et bilde på `/feed`-siden, blir `app/(.)photos/[id]/page.js`-ruten avskåret og vist som en modal oppå `/feed`-siden. `(.)`-syntaksen forteller Next.js å se ett nivå opp (til `app`-mappen) for å finne `photos/[id]`-ruten.

Datahenting med App Router

App Router gir innebygd støtte for datahenting ved hjelp av Server Components og Client Components. Server Components rendres på serveren, mens Client Components rendres på klienten. Dette lar deg velge den beste tilnærmingen for hver komponent basert på dens krav.

Serverkomponenter

Serverkomponenter er standarden i App Router. De lar deg hente data direkte i komponentene dine uten behov for separate API-ruter. Dette kan forbedre ytelsen og forenkle koden din.

Eksempel:

// app/products/page.js
import React from 'react';

export default async function ProductsPage() {
  const products = await fetchProducts();

  return (
    <div>
      <h1>Produkter</h1>
      <ul>
        {products.map((product) => (
          <li key={product.id}>{product.name}</li>
        ))}
      </ul>
    </div>
  );
}

async function fetchProducts() {
  // Simulerer henting av data fra en database eller API
  return new Promise((resolve) => {
    setTimeout(() => {
      const products = [
        { id: 1, name: 'Produkt A' },
        { id: 2, name: 'Produkt B' },
        { id: 3, name: 'Produkt C' },
      ];
      resolve(products);
    }, 500);
  });
}

I dette eksempelet kalles `fetchProducts`-funksjonen direkte i `ProductsPage`-komponenten. Komponenten rendres på serveren, og dataene hentes før HTML-en sendes til klienten.

Klientkomponenter

Klientkomponenter rendres på klienten og lar deg bruke klient-side funksjoner som hendelseslyttere, tilstand og nettleser-API-er. For å bruke en klientkomponent må du legge til `'use client'`-direktivet øverst i filen.

Eksempel:

// app/counter/page.js
'use client'

import React, { useState } from 'react';

export default function CounterPage() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Teller</h1>
      <p>Antall: {count}</p>
      <button onClick={() => setCount(count + 1)}>Øk</button>
    </div>
  );
}

I dette eksempelet er `CounterPage`-komponenten en klientkomponent fordi den bruker `useState`-hooken. `'use client'`-direktivet forteller Next.js å rendre denne komponenten på klienten.

Avanserte Rutingsteknikker

App Router tilbyr flere avanserte rutingsteknikker som kan brukes til å lage komplekse og sofistikerte applikasjoner.

1. Rutehåndterere (Route Handlers)

Rutehåndterere lar deg opprette API-endepunkter i `app`-mappen din. Dette fjerner behovet for en separat `pages/api`-mappe. Rutehåndterere defineres i filer som heter `route.js` (eller `route.ts`) og eksporterer funksjoner som håndterer forskjellige HTTP-metoder (f.eks. `GET`, `POST`, `PUT`, `DELETE`).

Eksempel:

// app/api/users/route.js
import { NextResponse } from 'next/server'

export async function GET(request) {
  // Simulerer henting av brukere fra en database
  const users = [
    { id: 1, name: 'John Doe' },
    { id: 2, name: 'Jane Doe' },
  ];

  return NextResponse.json(users);
}

export async function POST(request) {
  const body = await request.json()
  console.log('Mottatt data:', body)
  return NextResponse.json({ message: 'Bruker opprettet' }, { status: 201 })
}

Dette eksempelet definerer en rutehåndterer på `/api/users` som håndterer både `GET`- og `POST`-forespørsler. `GET`-funksjonen returnerer en liste over brukere, og `POST`-funksjonen oppretter en ny bruker.

2. Rutegrupper med Flere Layouter

Du kan kombinere rutegrupper med layouter for å lage forskjellige layouter for forskjellige deler av applikasjonen din. Dette er nyttig for scenarioer der du vil ha en annen header eller sidebar for forskjellige deler av nettstedet ditt.

Eksempel:

app/
  (marketing)/
    layout.js  // Markedsføringslayout
    about/
      page.js
    contact/
      page.js
  (admin)/
    layout.js  // Admin-layout
    dashboard/
      page.js

I dette eksempelet vil `about`- og `contact`-sidene bruke `marketing`-layouten, mens `dashboard`-siden vil bruke `admin`-layouten.

3. Middleware

Middleware lar deg kjøre kode før en forespørsel håndteres av applikasjonen din. Dette er nyttig for oppgaver som autentisering, autorisasjon, logging og omdirigering av brukere basert på deres plassering eller enhet.

Middleware defineres i en fil som heter `middleware.js` (eller `middleware.ts`) i roten av prosjektet ditt.

Eksempel:

// middleware.js
import { NextResponse } from 'next/server'

export function middleware(request) {
  // Sjekk om brukeren er autentisert
  const isAuthenticated = false; // Erstatt med din autentiseringslogikk

  if (!isAuthenticated && request.nextUrl.pathname.startsWith('/admin')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

// Se "Matching Paths" nedenfor for å lære mer
export const config = {
  matcher: '/admin/:path*',
}

Dette eksempelet definerer middleware som sjekker om brukeren er autentisert før den lar dem få tilgang til noen rute under `/admin`. Hvis brukeren ikke er autentisert, blir de omdirigert til `/login`-siden.

Beste Praksis for Filbasert Ruting

For å få mest mulig ut av App Routerens filbaserte rutingsystem, bør du vurdere følgende beste praksis:

Eksempler på Internasjonalisering med Next.js App Router

Next.js App Router forenkler internasjonalisering (i18n) gjennom filbasert ruting. Slik kan du implementere i18n effektivt:

1. Sub-path Ruting

Organiser rutene dine basert på locale ved hjelp av sub-paths. For eksempel:

app/
  [locale]/
    page.tsx         // Hjemmeside for locale
    about/
      page.tsx     // Om-side for locale
// app/[locale]/page.tsx
import { getTranslations } from './dictionaries';

export default async function HomePage({ params: { locale } }) {
  const t = await getTranslations(locale);
  return (<h1>{t.home.title}</h1>);
}

// dictionaries.js
const dictionaries = {
  en: () => import('./dictionaries/en.json').then((module) => module.default),
  es: () => import('./dictionaries/es.json').then((module) => module.default),
};

export const getTranslations = async (locale) => {
  try {
    return dictionaries[locale]() ?? dictionaries.en();
  } catch (error) {
    console.error(`Klarte ikke å laste oversettelser for locale ${locale}`, error);
    return dictionaries.en();
  }
};

I dette oppsettet håndterer det dynamiske rutesegmentet `[locale]` forskjellige locales (f.eks. `/en`, `/no`). Oversettelsene lastes dynamisk basert på locale.

2. Domeneruting

For en mer avansert tilnærming kan du bruke forskjellige domener eller underdomener for hver locale. Dette innebærer ofte ekstra konfigurasjon med din hostingleverandør.

3. Middleware for Locale-deteksjon

Bruk middleware for automatisk å oppdage brukerens foretrukne locale og omdirigere dem deretter.

// middleware.js
import { NextResponse } from 'next/server';
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';

let locales = ['en', 'no', 'fr'];

function getLocale(request) {
  const negotiatorHeaders = {};
  request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
  let languages = new Negotiator({ headers: negotiatorHeaders }).languages();

  try {
      return match(languages, locales, 'en'); // Bruk "en" som standard locale
  } catch (error) {
      console.error("Feil ved matching av locale:", error);
      return 'en'; // Fallback til engelsk hvis matching feiler
  }
}

export function middleware(request) {
  const pathname = request.nextUrl.pathname;
  const pathnameIsMissingLocale = locales.every(
    (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
  );

  if (pathnameIsMissingLocale) {
    const locale = getLocale(request);

    return NextResponse.redirect(
      new URL(
        `/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`,
        request.url
      )
    );
  }
}

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

Denne middleware sjekker om den forespurte stien har et locale-prefiks. Hvis ikke, oppdager den brukerens foretrukne locale ved hjelp av `Accept-Language`-headeren og omdirigerer dem til den passende locale-spesifikke stien. Biblioteker som `@formatjs/intl-localematcher` og `negotiator` brukes til å håndtere locale-forhandling.

Next.js App Router og Global Tilgjengelighet

Å lage globalt tilgjengelige webapplikasjoner krever nøye vurdering av tilgjengelighetsprinsipper (a11y). Next.js App Router gir et solid fundament for å bygge tilgjengelige opplevelser, men det er viktig å implementere beste praksis for å sikre at applikasjonen din er brukbar for alle, uavhengig av deres evner.

Viktige Tilgjengelighetshensyn

  1. Semantisk HTML: Bruk semantiske HTML-elementer (f.eks. `<article>`, `<nav>`, `<aside>`, `<main>`) for å strukturere innholdet ditt. Dette gir mening til hjelpemidler og hjelper brukere med å navigere på nettstedet ditt enklere.
  2. ARIA-attributter: Bruk ARIA (Accessible Rich Internet Applications)-attributter for å forbedre tilgjengeligheten til tilpassede komponenter og widgets. ARIA-attributter gir tilleggsinformasjon om rollen, tilstanden og egenskapene til elementer for hjelpemidler.
  3. Tastaturnavigasjon: Sørg for at alle interaktive elementer er tilgjengelige via tastaturet. Brukere skal kunne navigere gjennom applikasjonen din med `Tab`-tasten og samhandle med elementer med `Enter`- eller `Mellomrom`-tasten.
  4. Fargekontrast: Bruk tilstrekkelig fargekontrast mellom tekst og bakgrunn for å sikre lesbarhet for brukere med synshemninger. Web Content Accessibility Guidelines (WCAG) anbefaler et kontrastforhold på minst 4.5:1 for normal tekst og 3:1 for stor tekst.
  5. Alternativ tekst for bilder: Gi beskrivende alternativ tekst (alt-tekst) for alle bilder. Alt-tekst gir et tekstalternativ for bilder som kan leses av skjermlesere.
  6. Skjemaetiketter: Knytt skjemaetiketter til deres korresponderende inndatafelt ved hjelp av `<label>`-elementet. Dette gjør det klart for brukere hvilken informasjon som forventes i hvert felt.
  7. Skjermlesertesting: Test applikasjonen din med en skjermleser for å sikre at den er tilgjengelig for brukere med synshemninger. Populære skjermlesere inkluderer NVDA, JAWS og VoiceOver.

Implementere Tilgjengelighet i Next.js App Router

  1. Bruk Next.js Link-komponenten: Bruk `<Link>`-komponenten for navigasjon. Den gir innebygde tilgjengelighetsfunksjoner, som prefetching og fokushåndtering.
  2. Fokushåndtering: Når du navigerer mellom sider eller åpner modaler, sørg for at fokuset håndteres riktig. Fokuset bør settes til det mest logiske elementet på den nye siden eller modalen.
  3. Tilgjengelige Tilpassede Komponenter: Når du lager tilpassede komponenter, sørg for at de er tilgjengelige ved å følge prinsippene beskrevet ovenfor. Bruk semantisk HTML, ARIA-attributter og tastaturnavigasjon for å gjøre komponentene dine brukbare for alle.
  4. Linting og Testing: Bruk linting-verktøy som ESLint med tilgjengelighetsplugins for å identifisere potensielle tilgjengelighetsproblemer i koden din. Bruk også automatiserte testverktøy for å teste applikasjonen din for tilgjengelighetsbrudd.

Konklusjon

Next.js App Router sitt filbaserte rutingsystem tilbyr en kraftig og intuitiv måte å strukturere og navigere i applikasjonene dine på. Ved å forstå kjernekonseptene og beste praksis som er beskrevet i denne guiden, kan du bygge robuste, skalerbare og vedlikeholdbare Next.js-applikasjoner. Eksperimenter med de forskjellige funksjonene i App Router og oppdag hvordan den kan forenkle utviklingsflyten din og forbedre brukeropplevelsen.