Next.js course Β· Module 2: Routing and Layouts

Loading UI and layout.tsx

6 min read
In this lesson6

In the Quantum Metropolis, every district has permanent infrastructure - lighting, navigation systems, information panels - that remains active regardless of who is currently in the zone. When a resident moves between sectors, holographic screens display a loading animation while the surrounding infrastructure remains unchanged.

In Next.js 16 App Router, these concepts have their equivalents: layout.tsx is the permanent infrastructure that wraps pages and is not re-rendered during navigation, and loading.tsx is the automatic loading indicator displayed during page transitions.

What is layout.tsx?

The layout.tsx file is one of the most important special files in the App Router. It defines a shared layout that wraps pages in a given segment and all its subsegments. The most important feature of a layout: it is not re-rendered when navigating between pages that share the same layout. It's like the infrastructure of a Quantum district - it stays in place, only the content inside changes.

Root Layout - The Required Main Layout

Every Next.js application must have a Root Layout in the app/layout.tsx file. This is the only layout that must contain the <html> and <body> tags:

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="en">
11      <body>
12        <nav className="quantum-nav">
13          <Link href="/">Home</Link>
14          <Link href="/dashboard">Dashboard</Link>
15          <Link href="/settings">Settings</Link>
16        </nav>
17        <main>{children}</main>
18        <footer>Metropolis Quantum Β© 2150</footer>
19      </body>
20    </html>
21  );
22}

Links in layouts are Link components from next/link. A plain <a href> would reload the whole page: the layout would mount again and lose its state.

The Root Layout is rendered once and remains active throughout the application's lifetime. Navigation between /dashboard and /settings will only change the {children} content - the navigation and footer will stay in place without re-rendering.

Nested Layouts

Layouts can be nested at any level of the folder structure. Each path segment can have its own layout.tsx:

1app/
2  β”œβ”€β”€ layout.tsx              # Root Layout (html + body + navigation)
3  β”œβ”€β”€ page.tsx                # Home /
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 nests inside the Root Layout:

1// app/dashboard/layout.tsx - Nested 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>Quantum Panel</h2>
13        <ul>
14          <li><Link href="/dashboard">Overview</Link></li>
15          <li><Link href="/dashboard/analytics">Analytics</Link></li>
16          <li><Link href="/dashboard/settings">Settings</Link></li>
17        </ul>
18      </aside>
19      <section className="content">
20        {children}
21      </section>
22    </div>
23  );
24}

When the user navigates from /dashboard to /dashboard/analytics, the Root Layout and Dashboard Layout are not re-rendered. Only the {children} in the Dashboard Layout changes. This is a crucial performance optimization - the state of layout components (e.g., open sidebar, scroll position) is preserved.

Preserving Layout State During Navigation

One of the greatest advantages of layouts is state preservation during navigation. Imagine a control panel in the Quantum Metropolis - when you switch between sections, you don't want the entire interface to reload:

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  // This state WILL SURVIVE navigation between /dashboard/* pages!
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>Quantum Panel</h2>
20          <nav>
21            <Link href="/dashboard">Overview</Link>
22            <Link href="/dashboard/analytics">Analytics</Link>
23          </nav>
24        </aside>
25      )}
26      <div className="flex-1">
27        <button onClick={() => setSidebarOpen(!sidebarOpen)}>
28          {sidebarOpen ? 'Hide' : 'Show'} sidebar
29        </button>
30        {children}
31      </div>
32    </div>
33  );
34}

If the user closes the sidebar on the /dashboard page and navigates to /dashboard/analytics, the sidebar will remain closed - the sidebarOpen state is not reset.

loading.tsx - Automatic Loading States

The loading.tsx file is a special file that Next.js automatically displays as a fallback while page content is loading. It's like a holographic loading screen in the Quantum Metropolis - it appears automatically when the system is preparing data.

How Does loading.tsx Work?

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

Next.js automatically wraps page.tsx in a <Suspense> boundary, using loading.tsx as the fallback:

1// This is what Next.js does "under the hood":
2<Layout>
3  <Suspense fallback={<Loading />}>
4    <Page />
5  </Suspense>
6</Layout>

Thanks to this:

  • The layout is displayed immediately (doesn't wait for page data)
  • The loading component appears automatically during loading
  • The page replaces the loading component when data is ready
  • The user sees a responsive interface instead of a blank screen

loading.tsx at Different Levels

Each segment can have its own loading.tsx:

1app/
2  β”œβ”€β”€ loading.tsx                # Loading for the home page
3  β”œβ”€β”€ dashboard/
4  β”‚   β”œβ”€β”€ loading.tsx            # Loading for /dashboard/*
5  β”‚   β”œβ”€β”€ analytics/
6  β”‚   β”‚   └── loading.tsx        # Analytics-specific loading
7  β”‚   └── settings/
8  β”‚       └── page.tsx           # Will use loading from /dashboard/

template.tsx vs layout.tsx

Next.js offers one more special file - template.tsx. It looks identical to layout.tsx, but has one crucial difference: the template is re-mounted (a new instance is created) with each navigation.

1// app/dashboard/template.tsx
2// New instance with EVERY navigation!
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}

When to use template.tsx instead of layout.tsx?

Featurelayout.tsxtemplate.tsx
Re-renderingNO - preserves stateYES - new instance
Component statePreserved between navigationsReset on navigation
useEffectDoes not re-runRuns on every navigation
Entry animationsDo not repeatRepeat on every entry

Use cases for template.tsx:

  • Entry/exit animations on pages (fade-in on every navigation)
  • Page view logging (useEffect with analytics on every entry)
  • Forms that should reset on navigation

Practical Applications

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: 'Overview' },
16    { href: '/dashboard/analytics', label: 'Analytics' },
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}

Summary

Layouts and loading UI are fundamental concepts of the App Router in Next.js 16:

  1. layout.tsx - a shared layout that wraps pages and is not re-rendered during navigation. It preserves component state.
  2. Root Layout - the required main layout with <html> and <body> tags.
  3. Nested layouts - each segment can have its own layout, creating a layout hierarchy.
  4. loading.tsx - an automatic loading indicator. Next.js wraps the page in <Suspense> with loading as the fallback.
  5. template.tsx - like a layout, but creates a new instance with each navigation. Ideal for animations and logging.

These mechanisms, inspired by the permanent infrastructure of the Quantum Metropolis districts, allow building efficient and responsive interfaces where the user never sees a blank screen, and navigation is smooth and instantaneous.

Code for this lesson: 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 and layout.tsx',
12};
13
14export default function RootLayout({ children }: RootLayoutProps) {
15  return (
16    <html lang="en">
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' }}>Home</Link>
26          <Link href="/dashboard" style={{ color: '#bd93f9', textDecoration: 'none' }}>Dashboard</Link>
27          <Link href="/settings" style={{ color: '#ff79c6', textDecoration: 'none' }}>Settings</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}

Spotted a mistake in this lesson?

Useful articles