Kurs Next.js · Moduł 2: Routing i layouty

Loading UI i layout.tsx

6 min czytania
W tej lekcji6

W Metropolii Quantum każdy dystrykt posiada stałą infrastrukturę - oświetlenie, systemy nawigacyjne, panele informacyjne - która pozostaje aktywna niezależnie od tego, kto aktualnie przebywa w danej strefie. Kiedy mieszkaniec przemieszcza się między sektorami, holograficzne ekrany wyświetlają animację ładowania, podczas gdy otaczająca infrastruktura pozostaje niezmieniona.

W Next.js 16 App Router te koncepcje mają swoje odpowiedniki: layout.tsx to stała infrastruktura, która otacza strony i nie jest ponownie renderowana przy nawigacji, a loading.tsx to automatyczny wskaźnik ładowania wyświetlany podczas przejść między stronami.

Czym jest layout.tsx?

Plik layout.tsx to jeden z najważniejszych plików specjalnych w App Routerze. Definiuje on współdzielony układ (shared layout), który otacza strony w danym segmencie i wszystkich jego podsegmentach. Najważniejsza cecha layoutu: nie jest ponownie renderowany przy nawigacji między stronami, które współdzielą ten sam layout. To jak infrastruktura dystryktu Quantum - pozostaje na miejscu, zmienia się tylko zawartość wewnątrz.

Root Layout - obowiązkowy layout główny

Każda aplikacja Next.js musi posiadać Root Layout w pliku app/layout.tsx. Jest to jedyny layout, który musi zawierać tagi <html> i <body>:

1// app/layout.tsx - Root Layout
2import Link from 'next/link';
3
4export default function RootLayout({
5  children,
6}: {
7  children: React.ReactNode
8}) {
9  return (
10    <html lang="pl">
11      <body>
12        <nav className="quantum-nav">
13          <Link href="/">Strona główna</Link>
14          <Link href="/dashboard">Dashboard</Link>
15          <Link href="/settings">Ustawienia</Link>
16        </nav>
17        <main>{children}</main>
18        <footer>Metropolis Quantum © 2150</footer>
19      </body>
20    </html>
21  );
22}

Linki w layoutach to komponenty Link z next/link. Zwykły <a href> przeładowałby całą stronę: layout zamontowałby się od nowa i straciłby stan.

Root Layout jest renderowany raz i pozostaje aktywny przez cały czas życia aplikacji. Nawigacja między /dashboard a /settings zmieni tylko zawartość {children} - nawigacja i stopka pozostaną na miejscu bez ponownego renderowania.

Zagnieżdżone layouty (Nested Layouts)

Layouty można zagnieżdżać na dowolnym poziomie struktury folderów. Każdy segment ścieżki może mieć swój własny layout.tsx:

1app/
2  ├── layout.tsx              # Root Layout (html + body + nawigacja)
3  ├── page.tsx                # Strona główna /
4  └── dashboard/
5      ├── layout.tsx          # Dashboard Layout (sidebar + panel)
6      ├── page.tsx            # /dashboard
7      ├── analytics/
8      │   └── page.tsx        # /dashboard/analytics
9      └── settings/
10          └── page.tsx        # /dashboard/settings

Dashboard Layout zagnieżdża się wewnątrz Root Layout:

1// app/dashboard/layout.tsx - Zagnieżdżony layout
2import Link from 'next/link';
3
4export default function DashboardLayout({
5  children,
6}: {
7  children: React.ReactNode
8}) {
9  return (
10    <div className="dashboard-container">
11      <aside className="sidebar">
12        <h2>Panel Quantum</h2>
13        <ul>
14          <li><Link href="/dashboard">Przegląd</Link></li>
15          <li><Link href="/dashboard/analytics">Analityka</Link></li>
16          <li><Link href="/dashboard/settings">Ustawienia</Link></li>
17        </ul>
18      </aside>
19      <section className="content">
20        {children}
21      </section>
22    </div>
23  );
24}

Kiedy użytkownik nawiguje z /dashboard do /dashboard/analytics, Root Layout i Dashboard Layout nie są ponownie renderowane. Zmienia się tylko {children} w Dashboard Layout. To kluczowa optymalizacja wydajności - stan komponentów layoutu (np. otwarty sidebar, pozycja scrolla) jest zachowany.

Zachowanie stanu layoutu przy nawigacji

Jedną z największych zalet layoutów jest zachowanie stanu przy nawigacji. Wyobraź sobie panel kontrolny w Metropolii Quantum - kiedy przełączasz się między sekcjami, nie chcesz, żeby cały interfejs się przeładował:

1// app/dashboard/layout.tsx
2'use client';
3
4import Link from 'next/link';
5import { useState } from 'react';
6
7export default function DashboardLayout({
8  children,
9}: {
10  children: React.ReactNode
11}) {
12  // Ten stan PRZETRWA nawigację między /dashboard/* stronami!
13  const [sidebarOpen, setSidebarOpen] = useState(true);
14
15  return (
16    <div className="flex">
17      {sidebarOpen && (
18        <aside className="w-64 bg-gray-900 p-4">
19          <h2>Panel Quantum</h2>
20          <nav>
21            <Link href="/dashboard">Przegląd</Link>
22            <Link href="/dashboard/analytics">Analityka</Link>
23          </nav>
24        </aside>
25      )}
26      <div className="flex-1">
27        <button onClick={() => setSidebarOpen(!sidebarOpen)}>
28          {sidebarOpen ? 'Ukryj' : 'Pokaż'} sidebar
29        </button>
30        {children}
31      </div>
32    </div>
33  );
34}

Jeśli użytkownik zamknie sidebar na stronie /dashboard i przejdzie do /dashboard/analytics, sidebar pozostanie zamknięty - stan sidebarOpen nie jest resetowany.

loading.tsx - automatyczne stany ładowania

Plik loading.tsx to specjalny plik, który Next.js automatycznie wyświetla jako fallback podczas ładowania zawartości strony. To jak holograficzny ekran ładowania w Metropolii Quantum - pojawia się automatycznie, gdy system przygotowuje dane.

Jak działa loading.tsx?

1// app/dashboard/loading.tsx
2export default function DashboardLoading() {
3  return (
4    <div className="loading-screen">
5      <div className="spinner"></div>
6      <p>Ładowanie danych panelu Quantum...</p>
7    </div>
8  );
9}

Next.js automatycznie opakowuje page.tsx w granicę <Suspense>, używając loading.tsx jako fallback:

1// To jest to, co Next.js robi "pod maską":
2<Layout>
3  <Suspense fallback={<Loading />}>
4    <Page />
5  </Suspense>
6</Layout>

Dzięki temu:

  • Layout jest wyświetlany natychmiast (nie czeka na dane strony)
  • Komponent loading pojawia się automatycznie podczas ładowania
  • Strona zastępuje loading, gdy dane są gotowe
  • Użytkownik widzi responsywny interfejs zamiast pustego ekranu

loading.tsx na różnych poziomach

Każdy segment może mieć własny loading.tsx:

1app/
2  ├── loading.tsx                # Loading dla strony głównej
3  ├── dashboard/
4  │   ├── loading.tsx            # Loading dla /dashboard/*
5  │   ├── analytics/
6  │   │   └── loading.tsx        # Loading specyficzny dla analityki
7  │   └── settings/
8  │       └── page.tsx           # Użyje loading z /dashboard/

template.tsx vs layout.tsx

Next.js oferuje jeszcze jeden specjalny plik - template.tsx. Wygląda identycznie jak layout.tsx, ale ma jedną kluczową różnicę: template jest ponownie montowany (tworzona jest nowa instancja) przy każdej nawigacji.

1// app/dashboard/template.tsx
2// Nowa instancja przy KAŻDEJ nawigacji!
3export default function DashboardTemplate({
4  children,
5}: {
6  children: React.ReactNode
7}) {
8  return (
9    <div className="animate-fadeIn">
10      {children}
11    </div>
12  );
13}

Kiedy używać template.tsx zamiast layout.tsx?

Cechalayout.tsxtemplate.tsx
Ponowne renderowanieNIE - zachowuje stanTAK - nowa instancja
Stan komponentuZachowany między nawigacjamiResetowany przy nawigacji
useEffectNie uruchamia się ponownieUruchamia się przy każdej nawigacji
Animacje wejściaNie powtarzają sięPowtarzają się przy każdym wejściu

Przykłady użycia template.tsx:

  • Animacje wejścia/wyjścia na stronach (fade-in przy każdej nawigacji)
  • Logowanie wyświetleń strony (useEffect z analytics przy każdym wejściu)
  • Formularze, które powinny się resetować przy nawigacji

Praktyczne zastosowania

Nawigacja z aktywnym stanem

1// app/dashboard/layout.tsx
2'use client';
3
4import Link from 'next/link';
5import { usePathname } from 'next/navigation';
6
7export default function DashboardLayout({
8  children,
9}: {
10  children: React.ReactNode
11}) {
12  const pathname = usePathname();
13
14  const links = [
15    { href: '/dashboard', label: 'Przegląd' },
16    { href: '/dashboard/analytics', label: 'Analityka' },
17    { href: '/dashboard/settings', label: 'Ustawienia' },
18  ];
19
20  return (
21    <div className="dashboard">
22      <nav className="dashboard-nav">
23        {links.map((link) => (
24          <Link
25            key={link.href}
26            href={link.href}
27            className={pathname === link.href ? 'active' : ''}
28          >
29            {link.label}
30          </Link>
31        ))}
32      </nav>
33      <main>{children}</main>
34    </div>
35  );
36}

Podsumowanie

Layouty i loading UI to fundamentalne koncepcje App Router w Next.js 16:

  1. layout.tsx - współdzielony układ, który otacza strony i nie jest ponownie renderowany przy nawigacji. Zachowuje stan komponentów.
  2. Root Layout - obowiązkowy główny layout z tagami <html> i <body>.
  3. Zagnieżdżone layouty - każdy segment może mieć swój layout, tworząc hierarchię układów.
  4. loading.tsx - automatyczny wskaźnik ładowania. Next.js opakowuje stronę w <Suspense> z loading jako fallback.
  5. template.tsx - jak layout, ale tworzy nową instancję przy każdej nawigacji. Idealne do animacji i logowania.

Te mechanizmy, inspirowane stałą infrastrukturą dystryktów Metropolii Quantum, pozwalają budować wydajne i responsywne interfejsy, gdzie użytkownik nigdy nie widzi pustego ekranu, a nawigacja jest płynna i natychmiastowa.

Kod do tej lekcji: app/layout.tsx
1// Root Layout - Metropolis Quantum
2import { ReactNode } from 'react';
3import Link from 'next/link';
4
5interface RootLayoutProps {
6  children: ReactNode;
7}
8
9export const metadata = {
10  title: 'Metropolis Quantum',
11  description: 'Loading UI i layout.tsx',
12};
13
14export default function RootLayout({ children }: RootLayoutProps) {
15  return (
16    <html lang="pl">
17      <body style={{ margin: 0, fontFamily: 'monospace', background: '#0f0f23', color: '#e0e0e0' }}>
18        <nav style={{
19          display: 'flex',
20          gap: '16px',
21          padding: '12px 20px',
22          background: '#1a1a2e',
23          borderBottom: '1px solid #64ffda',
24        }}>
25          <Link href="/" style={{ color: '#64ffda', textDecoration: 'none' }}>Strona główna</Link>
26          <Link href="/dashboard" style={{ color: '#bd93f9', textDecoration: 'none' }}>Dashboard</Link>
27          <Link href="/settings" style={{ color: '#ff79c6', textDecoration: 'none' }}>Ustawienia</Link>
28        </nav>
29        <main style={{ padding: '20px' }}>
30          {children}
31        </main>
32        <footer style={{
33          textAlign: 'center',
34          padding: '12px',
35          borderTop: '1px solid #16213e',
36          color: '#8892b0',
37          fontSize: '12px',
38        }}>
39          Metropolis Quantum &copy; 2150
40        </footer>
41      </body>
42    </html>
43  );
44}

Widzisz błąd w tej lekcji?

Przydatne artykuły