IlmHamroh
JavaScript Full-stack/23-qism. Next.js4/43-dars20 daqiqa
Mundarija (33)

Next.js layout va template: umumiy qobiq, ichma-ich layout va template.tsx

Qisqacha: Layout (layout.tsx) — bir nechta sahifaga umumiy qobiq: header, menyu, footer. U children ni oladi va sahifa shu joyga tushadi. Sahifadan sahifaga o'tganda layout qayta yaratilmaydi: uning ichidagi state (yozilgan matn, ochiq menyu) saqlanadi. Eng tepadagi root layout majburiy va <html>, <body> ni o'zi chizadi. Template (template.tsx) ham qobiq, lekin u har o'tishda noldan yaratiladi — kirish animatsiyasi yoki har sahifada qayta boshlanadigan narsa uchun. Layout'ga searchParams kelmaydi: u har o'tishda qayta chizilmaydi, demak eskirgan qiymat ko'rsatib qolardi.

Bu darsda

  • Root layout yozasiz va unda nega <html> va <body> shartligini haqiqiy xato bilan ko'rasiz.
  • Ichma-ich layout'lar qanday ustma-ust o'ralishini chizib bera olasiz.
  • Layout'dagi state nega saqlanishini, template'dagi esa nega yo'qolishini tushuntirasiz (haqiqiy brauzer tajribasi bilan).
  • Layout'ga nega searchParams va joriy manzil kelmasligini va o'rniga nima qilinishini bilasiz.
  • portfolio ga umumiy header, footer va template kirish animatsiyasini qo'shasiz — va bu animatsiyaning narxini o'lchangan raqam bilan ko'rasiz.

Oldin bilishingiz kerak: Fayl-asosli routing, React Router II: layout marshrutlar, State'ning saqlanishi va key bilan reset, @keyframes va animation, prefers-reduced-motion.

1. Nega bu kerak?

Oldingi darsda portfolio ga beshta sahifa ko'chdi. Lekin ularda menyu ham, footer ham yo'q. Har page.tsx ga header nusxasini qo'yamizmi?

04-qismda aynan shunday qilgandingiz. v1 ning beshta HTML faylida bir xil <header> va <footer> turardi. Menyuga «Blog» qo'shish — beshta faylni tahrirlash edi. Bittasini unutsangiz, o'sha sahifada menyu boshqacha qolardi.

React Router bu muammoni layout marshrut va <Outlet /> bilan hal qildi. Next.js'da g'oya o'sha, faqat yana fayl bilan: papkaga layout.tsx qo'yasiz, va shu papkadagi hamma sahifa uning ichida chiziladi.

Hayotiy o'xshatish: avtobus. Avtobus — layout: haydovchi, o'rindiqlar, chiptachi. Bekatlar — sahifalar. Bekatdan bekatga o'tganda avtobus almashmaydi, yo'lovchilar o'z joyida o'tiraveradi. Template esa har bekatda yangi beriladigan chiptaga o'xshaydi: avtobus o'sha, chipta har safar yangi.

2. Root layout

2.1 children — sahifa tushadigan joy

app/layout.tsx — root layout: butun ilovaning eng tashqi qobig'i. create-next-app uni o'zi yaratgan. portfolio dagi to'liq holati:

tsx
import type { Metadata } from "next";
import { SaytPastki } from "@/komponentlar/SaytPastki";
import { SaytSarlavhasi } from "@/komponentlar/SaytSarlavhasi";
import "./globals.css";

export const metadata: Metadata = {
  title: "Aziz Karimov — portfolio",
  description: "Aziz Karimovning portfoliosi: loyihalar, blog va aloqa.",
};

// Root layout — barcha sahifalar uchun umumiy qobiq (N-04).
// Navigatsiyada qayta render bo'lmaydi.
export default function RootLayout({ children }: LayoutProps<"/">) {
  return (
    <html lang="uz">
      <body>
        <a className="otish" href="#asosiy">
          Asosiy kontentga o'tish
        </a>
        <SaytSarlavhasi />
        {children}
        <SaytPastki />
      </body>
    </html>
  );
}

Qatorma-qator:

Qator Nima qiladi
import "./globals.css" Butun sayt CSS'i — bir marta, shu yerda
metadata Sukut bo'yicha <title> va description; sahifa o'zinikini bersa — o'shanisi
LayoutProps<"/"> Global tur (typegen), PageProps ning layout'dagi juftligi
<html lang="uz"> Sahifa tili — ekran o'quvchi va Google uchun
<a className="otish"> «Asosiy kontentga o'tish» havolasi — v1 dagi skip link
{children} Joriy sahifa (page.tsx) shu yerga tushadi

{children} — React Router'dagi <Outlet /> ning o'rni. Farqi: Outlet'ni router qo'yadi, children esa oddiy React prop (children va kompozitsiya darsidagi children ning aynan o'zi).

Izohdagi (N-04) — kanon kodidagi dars belgisi. Uzun izoh telefonga sig'ishi uchun ikki qatorga bo'lindi.

2.2 <html> va <body> — faqat root layout'da

Oddiy React ilovada (Vite) <html> va <body> index.html faylida turardi, React esa <div id="root"> ichiga chizardi. Next.js'da index.html yo'q. Butun sahifani — <html> dan boshlab — React chizadi. Shuning uchun bu teglarni root layout yozishi shart.

Biz tajriba qildik: root layout'dan <html> va <body> ni olib tashladik (o'rniga <>…</>). Natija ikki xil chiqdi.

next dev da brauzerdagi xato oynasi:

text
Runtime Error
Missing <html> and <body> tags in the root layout.
Read more at https://nextjs.org/docs/messages/missing-root-layout-tags

Tarjimasi: "Root layout'da <html> va <body> teglari yo'q".

next build esa — xatosiz o'tdi. Build jadvali odatdagidek. Production'da (next start) sahifa ochiladi, lekin Chrome'da tekshirsak:

text
compatMode: BackCompat

BackCompat — brauzerning quirks mode (eski saytlarga moslashish rejimi). Sahifa boshida <!DOCTYPE html> yo'q, lang="uz" ham yo'q. CSS o'lchamlari eski qoidalar bilan hisoblanadi, ekran o'quvchi sahifa tilini bilmaydi. Xato jim: faqat dev'da ko'rinadi. Saboq — dev'dagi qizil oynani "keyin tuzataman" deb yopmang.

Tekshirib ko'ring: app/loyihalar/layout.tsx ichida ham <html> yozsak nima bo'ladi?

Javob

Ikkita <html> bo'lib qoladi: ichma-ich layout root layout'ning ichida chiziladi. <html> va <body> — faqat root layout'da. Ichki layout'lar oddiy elementlar qaytaradi (<section>, <div>, <>…</>).

3. Ichma-ich layout'lar

3.1 Har papka — o'z qobig'i

Layout faqat ildizda emas, istalgan papkada bo'lishi mumkin. Faraz qiling, «Loyihalar» bo'limida chap tomonda loyihalar ro'yxati turishi kerak — ro'yxat sahifasida ham, har loyiha sahifasida ham:

tsx
// app/loyihalar/layout.tsx
export default function LoyihalarLayout({
  children,
}: LayoutProps<"/loyihalar">) {
  return (
    <div className="ikki-ustun">
      <aside>{/* loyihalar ro'yxati */}</aside>
      {children}
    </div>
  );
}

Endi /loyihalar/bahor ochilganda Next.js uchta komponentni ichma-ich qo'yadi:

flowchart TB
  R["app/layout.tsx<br/>html, body, header, footer"] --> L["app/loyihalar/layout.tsx<br/>aside + children"]
  L --> P["app/loyihalar/[slug]/page.tsx<br/>«Bahor» sahifasi"]

JSX'da yozsak, aslida shunday chiziladi:

tsx
<RootLayout>
  <LoyihalarLayout>
    <LoyihaSahifasi params={…} />
  </LoyihalarLayout>
</RootLayout>

/haqimda ochilganda esa LoyihalarLayout umuman qatnashmaydi — u faqat loyihalar/ papkasi va uning ichidagi sahifalarni o'raydi. Qoida: sahifadan ildizgacha yo'ldagi hamma layout ustma-ust o'raladi.

portfolio kanonida hozircha ichki layout yo'q. U Parallel va intercepting routes darsida paydo bo'ladi: loyihalar/layout.tsx loyiha rasmini modal oynada ko'rsatish uchun. Header va footer esa root layout'da qoladi — sababini Route groups darsida ko'rasiz.

3.2 Layout'ga ham params keladi

Dinamik papkadagi layout o'z segmentining qiymatini oladi — page.tsx dagidek, Promise sifatida. Masalan app/loyihalar/[slug]/layout.tsx da LayoutProps<"/loyihalar/[slug]"> → params: Promise<{ slug: string }>. Ota layout esa bola segmentning params ini olmaydi: app/loyihalar/layout.tsx slug ni bilmaydi, chunki u /loyihalar ro'yxat sahifasini ham o'raydi — u yerda slug umuman yo'q.

4. Layout saqlanadi, sahifa almashadi

4.1 Nima saqlanadi?

Sahifadan sahifaga <Link> bilan o'tganda Next.js faqat o'zgargan qismni yangilaydi. / dan /loyihalar ga o'tsangiz, root layout umumiy — u joyida qoladi, faqat {children} almashadi. Bu qisman render (partial rendering) deyiladi.

Natijasi — layout ichidagi hamma narsa tirik qoladi:

  • inputga yozilgan matn (masalan header'dagi qidiruv maydoni);
  • useState qiymatlari (ochiq menyu, hisoblagich);
  • ijro etilayotgan audio yoki video;
  • scroll holati (layout'ning o'z scroll qismi bo'lsa).

State'ning saqlanishi darsidagi qoidani eslang: React komponentni daraxtdagi joyi va turi o'zgarmaguncha saqlaydi. Root layout har sahifada daraxtning tepasida, bir xil komponent — React uni hech qachon qayta yaratmaydi.

4.2 Template — har safar yangi

Template (template.tsx) — layout'ga o'xshash qobiq, bitta farq bilan: har o'tishda React uni yangidan yaratadi. Ichidagi state nolga qaytadi, effektlar qayta ishga tushadi.

Next.js buni qanday qiladi? Template'ga segment nomidan key beradi. State'ning saqlanishi darsidan bilasiz: key o'zgarsa, React eski komponentni o'chirib, yangisini yaratadi. Template — shu hiylaning fayl ko'rinishi.

Joyi — layout bilan sahifa orasida:

flowchart TB
  L["layout.tsx — saqlanadi"] --> T["template.tsx — har o'tishda yangi"]
  T --> P["page.tsx — joriy sahifa"]

4.3 Natija oynasida: layout va template modeli

Natija oynasida Next.js serveri yo'q (sababi oldingi darsda aytilgan: next/link — o'quv modeli). Lekin layout va template'ning xulqini oddiy React bilan aniq takrorlash mumkin, chunki Next.js ham aynan React'ning shu qoidalariga tayanadi. RootLayout — oddiy o'rovchi komponent, Template — key={pathname} bilan o'ralgan komponent.

Ikkalasida bittadan hisoblagich bor. «+1» tugmalarini bosing, keyin boshqa sahifaga o'ting:

tsx
import { useEffect, useState, type ReactNode } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";

function Counter({ place }: { place: string }) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log(`${place}: yaratildi`);
  }, [place]);
  return (
    <button
      aria-label={`${place} +1`}
      onClick={() => setCount(count + 1)}
    >
      {place}: {count}
    </button>
  );
}

function RootLayout({ children }: { children: ReactNode }) {
  return (
    <>
      <nav>
        <Link href="/">Bosh sahifa</Link>{" "}
        <Link href="/loyihalar">Loyihalar</Link>
      </nav>
      <Counter place="layout" />
      {children}
    </>
  );
}

function Template({ children }: { children: ReactNode }) {
  return (
    <div>
      <Counter place="template" />
      {children}
    </div>
  );
}

const PAGES: Record<string, string> = {
  "/": "Bosh sahifa",
  "/loyihalar": "Loyihalar",
};

export default function App() {
  const pathname = usePathname();
  return (
    <RootLayout>
      {/* Next.js template'ni shunday o'raydi: key — segment */}
      <Template key={pathname}>
        <h1>{PAGES[pathname]}</h1>
      </Template>
    </RootLayout>
  );
}

Konsolda:

text
layout: yaratildi
template: yaratildi
template: yaratildi

Ekranda:

text
Bosh sahifa Loyihalar
layout: 2
template: 0
Loyihalar

Nima bo'ldi, qadamma-qadam:

Qadam layout template Konsol
ochildi 0 0 ikkalasi "yaratildi"
2 + 1 bosish 2 1 —
«Loyihalar» 2 (saqlandi) 0 (yangi) faqat "template: yaratildi"

Layout hisoblagichi 2 da qoldi — komponent o'sha. Template hisoblagichi 0 ga qaytdi va effekt qayta ishladi — komponent yangi.

4.4 Haqiqiy Next.js'da ham shundaymi?

Model — model. Shuning uchun biz xuddi shu tajribani haqiqiy portfolio (holat 04) da qildik. Root layout'ga va template.tsx ga bittadan client hisoblagich qo'ydik, next build + next start, keyin headless Chrome'da: layout'ni 2 marta, template'ni 1 marta bosdik va menyudan o'tdik.

text
[konsol] layout: yaratildi
[konsol] template: yaratildi
ochildi: layout 0 | template 0 | / | navigation=1
3 bosish: layout 2 | template 1 | / | navigation=1
[konsol] template: yaratildi
Loyihalar: layout 2 | template 0 | /loyihalar | navigation=1
[konsol] template: yaratildi
Men haqimda: layout 2 | template 0 | /haqimda | navigation=1
chrome Chrome/155.0.8059.85

Natija model bilan bir xil: layout 2 da qoldi, template har o'tishda 0 dan boshladi. navigation=1 — brauzer sahifani bir marta yuklagan, keyingi o'tishlar to'liq qayta yuklashsiz. Client hisoblagich uchun fayl boshida "use client" qatori kerak bo'ldi — bu nima ekanini Client Components darsida ochamiz.

Tekshirib ko'ring: header'da qidiruv maydoni bor. Foydalanuvchi "react" deb yozdi va «Loyihalar» ga o'tdi. Maydon root layout'da bo'lsa matn qoladimi? template.tsx da bo'lsa-chi?

Javob

Layout'da — qoladi: komponent qayta yaratilmaydi, input state'i tirik. Template'da — yo'qoladi: template har o'tishda yangi key bilan noldan yaratiladi. Qidiruv kabi narsalar — layout'ga.

5. Layout'ga searchParams kelmaydi

5.1 Haqiqiy tur xatosi

Oldingi darsda sahifa params olishini ko'rdik. Sahifaga yana searchParams ham keladi — ?teg=React kabi query qismi (Navigatsiya darsida). Layout'ga-chi? Sinab ko'rdik:

tsx
export default async function LoyihalarLayout({
  children,
  searchParams,
}: LayoutProps<"/loyihalar">) {
  return <section>{children}</section>;
}

npm run tip:

text
src/app/loyihalar/layout.tsx(3,3): error TS2339: Property 'searchParams' does not exist on type 'LayoutProps<"/loyihalar">'.

Tarjimasi: "LayoutProps<"/loyihalar"> turida searchParams xususiyati yo'q". Bu tasodif emas, ataylab qilingan.

5.2 Nega?

Layout har o'tishda qayta chizilmaydi («Layout saqlanadi, sahifa almashadi» bo'limi). /loyihalar?teg=React dan /loyihalar?teg=CSS ga o'tdingiz — sahifa yangilanadi, layout esa joyida. Agar layout searchParams olganda, u eski teg=React ni ko'rsatib qolardi. Next.js bunday yolg'onga yo'l qo'ymaydi: layout'ga faqat o'zgarmaydigan narsa — children va o'z params i keladi.

Xuddi shu sababdan server layout joriy manzilni (pathname) ham bilmaydi. Menyuda "hozir qaysi sahifadasiz" belgisi (joriy havola) layout'da serverda chizilsa, birinchi sahifadagi belgi abadiy qolardi.

5.3 Yechim: kichik interaktiv komponent

Manzilni brauzerda o'qiydigan kichik komponent layout ichiga qo'yiladi. U usePathname() bilan joriy yo'lni oladi va manzil o'zgarganda o'zi qayta chiziladi — layout esa qayta chizilmaydi. portfolio da bu NavHavola komponenti, Navigatsiya darsida qo'shiladi. Natija oynasida g'oyasi:

tsx
import Link from "next/link";
import { usePathname } from "next/navigation";

function NavLink({ href, label }: { href: string; label: string }) {
  const pathname = usePathname();
  const active = pathname === href;
  return (
    <Link href={href} aria-current={active ? "page" : undefined}>
      {active ? `[${label}]` : label}
    </Link>
  );
}

// Layout o'zi manzilni bilmaydi — faqat NavLink biladi
function Header() {
  console.log("Header chizildi");
  return (
    <nav>
      <NavLink href="/" label="Bosh sahifa" />{" "}
      <NavLink href="/loyihalar" label="Loyihalar" />
    </nav>
  );
}

export default function App() {
  return <Header />;
}

Konsolda:

text
Header chizildi

Ekranda:

text
Bosh sahifa [Loyihalar]

Kvadrat qavs — joriy sahifa belgisi (haqiqiy saytda bu CSS bilan chiziladi, aria-current="page" ekran o'quvchiga "joriy sahifa" deydi). «Loyihalar» bosilganda belgi ko'chdi, lekin konsolda "Header chizildi" bir marta: Header qayta chizilmadi, faqat ikkita NavLink yangilandi. Layout ham aynan shunday ishlashi kerak — o'zgaruvchan qismni kichik komponentga ajrating.

6. Template: qachon kerak?

6.1 portfolio dagi template

Kanondagi template.tsx:

tsx
// template — layout'dan farqli, har navigatsiyada QAYTA
// yaratiladi (N-04): kirish animatsiyasi har sahifada boshlanadi.
export default function Template({
  children,
}: {
  children: React.ReactNode;
}) {
  return <div className="sahifa">{children}</div>;
}

Kanon kodida funksiya imzosi bitta qator (prettier printWidth 80); bu yerda telefonga sig'ishi uchun bo'lindi. LayoutProps o'rnida oddiy tur — template'ga faqat children keladi.

globals.css dagi juftligi:

css
@layer komponentlar {
  .sahifa {
    display: contents;
  }

  @media (prefers-reduced-motion: no-preference) {
    .sahifa > main {
      animation: sahifa-kirish 200ms ease-out;
    }
  }

  @keyframes sahifa-kirish {
    from {
      opacity: 0;
      translate: 0 0.5rem;
    }
  }
}

Uchta detal:

  • display: contents — <div className="sahifa"> o'zi quti chizmaydi, layout'ga xalaqit bermaydi; bolalari go'yo to'g'ridan root layout ichida turgandek joylashadi.
  • Animatsiya faqat prefers-reduced-motion: no-preference da — harakatdan boshi aylanadigan foydalanuvchi uchun yoqilmaydi (prefers-reduced-motion darsidagi qoida).
  • @keyframes da faqat from — to yozilmasa, brauzer elementning odatdagi holatini oladi.

Template har o'tishda yangi bo'lgani uchun <main> ham yangi element — animatsiya har sahifada qaytadan boshlanadi. Layout'ga qo'yilsa, faqat birinchi ochilishda bir marta o'ynardi.

6.2 Template'ning boshqa ishlari

Vazifa Nega template
Kirish animatsiyasi Har sahifada element yangidan yaratiladi
Har sahifa ochilishini yozish (analitika) useEffect har o'tishda qayta ishlaydi
Sahifaga xos forma yoki holatni tozalash State nolga qaytadi
Har o'tishda Suspense fallback'ini ko'rsatish Yangi Suspense chegarasi (Streaming)

Lekin ko'p hollarda template kerak emas. Next.js hujjati ham avval layout'ni tavsiya qiladi. Template — "har safar noldan" ataylab kerak bo'lganda.

Tekshirib ko'ring: app/ da ham layout.tsx, ham template.tsx bor. /aloqa ochilganda ular qaysi tartibda o'raladi?

Javob

RootLayout → Template → AloqaSahifasi. Template layout'ning {children} o'rnida turadi va o'zi sahifani o'raydi. Kanondagi HTML'da ham shunday: body > div.sahifa > main#asosiy.

7. Biz o'lchadik: animatsiyaning narxi

200 ms lik yengil kirish animatsiyasi — zararsizdek. Kanon portfolio ni oxirigacha qurgach, Performance bosqichida Lighthouse (mobil) bilan o'lchadi. Bosh sahifaning eng katta elementi (LCP, Largest Contentful Paint — ekrandagi eng katta matn yoki rasm chizilgan vaqt; Core Web Vitals) — kirish paragrafi. Lighthouse LCP'ni qismlarga ajratdi:

Bosh sahifa: LCP elementining chizilish kechikishi
  • template animatsiyasi bilanholat 37345 ms
  • animatsiyasiztajriba132 ms

Manba: IlmHamroh kanoni, Lighthouse 13.5.0 mobil, Chrome 155, 2026-10-21

"Element render delay" (elementni chizish kechikishi) 345 ms dan 132 ms ga tushdi. Sabab oddiy: paragraf opacity: 0 dan boshlanadi. Brauzer uchun shaffof matn hali "chizilmagan" — LCP animatsiya tugashini kutadi.

Lighthouse natijasi (bosh sahifa, mobil):

Performance LCP
animatsiya bilan 94 3.1 s
animatsiyasiz 97 2.6 s

Hamma o'lchangan sahifalarda shunday: animatsiya bilan Performance 93–96, LCP 2.7–3.2 s. Kanon qaror qildi: holat 38 da template.tsx va animatsiya olib tashlandi. Bu darsda (holat 04) esa template bor — o'quvchi uni ko'rsin, keyin narxini o'lchab, o'zi tanlasin.

Saboq ikki qavatli. Birinchisi: chiroyli effektning ham narxi bor, va u faqat o'lchanganda ko'rinadi. Ikkinchisi: narx har doim "olib tashla" degani emas. Animatsiyani <main> ga emas, LCP bo'lmaydigan elementga (masalan pastki bo'limlarga) qo'yish yoki faqat translate bilan (opacity siz) qilish — LCP'ni kechiktirmaydi. Buni Performance darsida sinab ko'ramiz.

8. Portfolio qadami: umumiy qobiq va commit

8.1 Fayllar

Oldingi darsdagi sahifalarga qo'shiladi:

Fayl Nima
app/layout.tsx lang="uz", skip link .otish, SaytSarlavhasi, {children}, SaytPastki
app/template.tsx <div className="sahifa"> — kirish animatsiyasi
app/globals.css v1 tokenlari, qatlamlar @layer tokenlar, reset, asos, layout, komponentlar
app/icon.svg v1 favicon — fayl nomi o'zi <link rel="icon"> ni qo'shadi
komponentlar/SaytSarlavhasi.tsx Ism + menyu: 4 ta <Link>
komponentlar/SaytPastki.tsx v1 footer: email, Telegram, ©

Menyu — massivdan:

tsx
const MENYU = [
  { href: "/", nomi: "Bosh sahifa" },
  { href: "/haqimda", nomi: "Men haqimda" },
  { href: "/loyihalar", nomi: "Loyihalar" },
  { href: "/aloqa", nomi: "Aloqa" },
] as const;

Endi menyuga «Blog» qo'shish — bitta qator, bitta fayl. v1 dagi beshta fayl o'rniga.

globals.css — v1 ning CSS'i (05-qism oklch palitrasi, qorong'i palitra, 06-qism qatlamlari) bitta faylda. Shrift hozircha tizim shrifti; v1 dagi Rubik next/font darsida qaytadi.

icon.svg — Next.js'ning fayl konvensiyasi: app/ ildiziga shu nom bilan qo'yilgan rasm favicon bo'ladi, HTML'ga hech narsa yozmaysiz. Skeletdagi favicon.ico 404 xatosi shu bilan yopildi.

8.2 Tekshiruv va commit

Lint, tip, format — 0. Build — 8 s, jadval oldingi darsdagidek (○ 5 sahifa + 3 loyiha, ◐ /loyihalar/[slug]). Kanon o'lchovi: axe 0 buzilish (/yoq dagi standart 404 dan tashqari), hydration ogohlantirishi 0, CSS 1.8 KB, JS 135.4 KB gzip — layout va template JS hajmini oshirmadi (ular serverda chiziladi).

bash
git add -A
git commit

Commit (kanon): Sahifalar: bosh, haqimda, loyihalar, loyihalar/[slug], aloqa; umumiy layout va template.

9. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Dev'da Missing <html> and <body> tags in the root layout. Root layout <html>/<body> siz Ikkalasini root layout'da yozing
Build o'tdi, sahifa quirks mode'da Xuddi shu — production'da jim Dev xatosini e'tiborsiz qoldirmang
Sahifada ikki <html> (noto'g'ri tuzilma) Ichki layout'da ham <html> Faqat root layout'da
Property 'searchParams' does not exist on type 'LayoutProps<…>' Layout'da searchParams Sahifada o'qing yoki client komponentda useSearchParams
Menyudagi joriy belgi yangilanmaydi Joriy manzil layout'da hisoblangan usePathname bilan kichik komponent
Input matni har o'tishda yo'qoladi Input template.tsx da Layout'ga ko'chiring
Animatsiya faqat birinchi marta Animatsiya layout'dagi elementda Template ichidagi elementga
LCP kechikdi opacity: 0 dan boshlanadigan kirish animatsiyasi O'lchang; LCP elementidan olib tashlang

10. Mashqlar

Mashqlar kurs/mashqlar/23/04-layout-template/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.

1-mashq (oson): layout yoki template?

  • Hamma sahifada bir xil header —
  • Har sahifa ochilganda "sahifa ko'rildi" yozuvi —
  • Musiqa pleyeri sahifadan sahifaga o'tganda to'xtamasin —
  • <html lang="uz"> qayerda yoziladi — root
  • Har sahifada qayta boshlanadigan kirish animatsiyasi —
Yechim
  • layout — umumiy, o'zgarmaydigan qobiq.
  • template — effekt har o'tishda qayta ishlashi kerak.
  • layout — pleyer state'i (qaysi qo'shiq, qayerda) saqlanishi uchun komponent qayta yaratilmasligi kerak.
  • layout — faqat root layout; template <html> chiza olmaydi.
  • template — yangi element = yangi animatsiya.

2-mashq (o'rta): «Loyihalar» bo'limi layout'i

«Layout va template modeli» blokini oling. Template'ni olib tashlang va /loyihalar bo'limi uchun ichki layout qo'shing: ProjectsLayout — ichida o'z Counter i (place="loyihalar"). Sahifalar: /, /loyihalar, /loyihalar/bahor (har birida havola bo'lsin). ProjectsLayout faqat /loyihalar bilan boshlanadigan manzillarda chizilsin (pathname.startsWith("/loyihalar") — satr metodlari). Tekshiring: /loyihalar da «+1» ni bosib, /loyihalar/bahor ga o'tsangiz — hisob qoladimi? Bosh sahifaga qaytsangiz-chi?

Yechim
tsx
import { useEffect, useState, type ReactNode } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";

function Counter({ place }: { place: string }) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log(`${place}: yaratildi`);
    return () => console.log(`${place}: o'chirildi`);
  }, [place]);
  return (
    <button
      aria-label={`${place} +1`}
      onClick={() => setCount(count + 1)}
    >
      {place}: {count}
    </button>
  );
}

// app/loyihalar/layout.tsx o'rnida
function ProjectsLayout({ children }: { children: ReactNode }) {
  return (
    <section>
      <Counter place="loyihalar" />
      {children}
    </section>
  );
}

const PAGES: Record<string, string> = {
  "/": "Bosh sahifa",
  "/loyihalar": "Loyihalar",
  "/loyihalar/bahor": "Bahor",
};

export default function App() {
  const pathname = usePathname();
  const page = <h1>{PAGES[pathname]}</h1>;
  return (
    <>
      <nav>
        <Link href="/">Bosh sahifa</Link>{" "}
        <Link href="/loyihalar">Loyihalar</Link>{" "}
        <Link href="/loyihalar/bahor">Bahor</Link>
      </nav>
      {pathname.startsWith("/loyihalar") ? (
        <ProjectsLayout>{page}</ProjectsLayout>
      ) : (
        page
      )}
    </>
  );
}

Konsolda:

text
loyihalar: yaratildi
loyihalar: o'chirildi

Ekranda:

text
Bosh sahifa Loyihalar Bahor
Bosh sahifa

/loyihalar → /loyihalar/bahor: ProjectsLayout joyida qoldi, hisob 1 da saqlandi (ekran("loyihalar: 1") qadami buni tekshiradi). Bosh sahifaga qaytganda layout daraxtdan chiqdi — "o'chirildi". Keyingi safar /loyihalar ga kirsangiz, u 0 dan boshlaydi. Haqiqiy Next.js'da ham ichki layout faqat o'z papkasi ichida yashaydi.

3-mashq (qiyin): sahifa ko'rishlar hisobi

Har sahifa ochilganda konsolga ko'rildi: /yo'l yozilsin — analitika uchun. Ikki variant yozing: LayoutTracker (layout ichida, usePathname siz, faqat useEffect(…, [])) va TemplateTracker (template ichida, key={pathname} bilan). Uch sahifani ketma-ket oching (/ → /haqimda → /aloqa) va qaysi variant uchta yozuv berishini ko'rsating. Har yozuvga [layout] yoki [template] belgisi qo'ying. Joriy yo'lni effekt ichida usePathname() dan oling.

Yechim
tsx
import { useEffect, type ReactNode } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";

function LayoutTracker() {
  const pathname = usePathname();
  useEffect(() => {
    console.log(`[layout] ko'rildi: ${pathname}`);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  return null;
}

function TemplateTracker() {
  const pathname = usePathname();
  useEffect(() => {
    console.log(`[template] ko'rildi: ${pathname}`);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  return null;
}

function Template({ children }: { children: ReactNode }) {
  return (
    <>
      <TemplateTracker />
      {children}
    </>
  );
}

export default function App() {
  const pathname = usePathname();
  return (
    <>
      <LayoutTracker />
      <nav>
        <Link href="/haqimda">Men haqimda</Link>{" "}
        <Link href="/aloqa">Aloqa</Link>
      </nav>
      <Template key={pathname}>
        <p>Sahifa: {pathname}</p>
      </Template>
    </>
  );
}

Konsolda:

text
[layout] ko'rildi: /
[template] ko'rildi: /
[template] ko'rildi: /haqimda
[template] ko'rildi: /aloqa

Ekranda:

text
Men haqimda Aloqa
Sahifa: /aloqa

Layout varianti faqat bir marta yozdi — u qayta yaratilmaydi, bo'sh [] li effekt ham qayta ishlamaydi. Template varianti har o'tishda yozdi. Eslatma: haqiqiy loyihada analitikani layout'da useEffect(…, [pathname]) bilan ham qilsa bo'ladi — bu ham to'g'ri yo'l. Mashqning maqsadi — ikki komponentning hayot sikli farqini ko'rish. eslint-disable qatori ataylab: ESLint pathname ni bog'liqlikka qo'shishni so'raydi, biz esa "faqat yaratilganda" xulqini ko'rsatyapmiz.

4-mashq: Portfolio qadami

O'z portfolio ingizga root layout (skip link, header, footer), template.tsx + kirish animatsiyasi (prefers-reduced-motion ichida) va icon.svg ni qo'shing. Menyu — massivdan. next dev da sahifalar orasida o'tib ko'ring: animatsiya har sahifada qayta o'ynashi kerak. Keyin npm run lint, npm run tip, npm run build — toza bo'lsa, commit.

Yechim

Namuna — «Portfolio qadami» bo'limidagi fayllar jadvali va root layout kodi. Tekshirish uchun: DevTools'da (Rendering paneli) "Emulate CSS prefers-reduced-motion: reduce" ni yoqing — animatsiya yo'qolishi kerak. Commit xabarini o'zingiz yozing yoki kanondagini oling. Animatsiyaning LCP narxini hozir o'lchamang — Performance darsida Lighthouse bilan birga qilamiz.

11. Real ishda

  • Har Next.js loyihasida root layout bor: shrift, analitika skripti, tema provayderi (Provider'lar), umumiy header — hammasi shu yerga.
  • Admin panellar — klassik ichma-ich layout: app/admin/layout.tsx da yon menyu, ichidagi sahifalar almashadi, menyu va uning scroll holati saqlanadi.
  • Template amalda kam uchraydi: animatsiya kutubxonalari (Motion) bilan sahifa o'tishlari, sahifa ko'rishlarini yozish. Intervyuda "layout va template farqi nima?" — tez-tez beriladigan savol: javob — qayta yaratilish va key.
  • "Layout'da searchParams yo'q" — tajribasiz dasturchilarning eng ko'p savollaridan biri. Endi sababini bilasiz.

Xulosa

  • layout.tsx — papkadagi hamma sahifaga umumiy qobiq; sahifa {children} o'rniga tushadi.
  • Root layout majburiy va <html>, <body> ni o'zi yozadi; bo'lmasa dev xato beradi, build esa jim quirks mode sahifa chiqaradi.
  • Ichma-ich layout'lar sahifadan ildizgacha ustma-ust o'raladi; o'tishda umumiy layout saqlanadi, state tirik qoladi.
  • template.tsx har o'tishda key bilan yangidan yaratiladi — animatsiya va "har safar" effektlari uchun.
  • Layout'ga searchParams va joriy manzil kelmaydi; ularni kichik client komponent (usePathname) o'qiydi.
  • Kirish animatsiyasining narxi o'lchandi: LCP render kechikishi 345 → 132 ms; kanon holat 38 da uni olib tashladi.

Keyingi dars: Maxsus fayllar: loading, error, not-found — /loyihalar/yoq ning 200 statusini tuzatamiz va xato, yuklanish sahifalarini qo'shamiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Next.js layout va template: umumiy qobiq, ichma-ich layout va template.tsx — IlmHamroh