IlmHamroh
JavaScript Full-stack/20-qism. React: UI kutubxonalar va animatsiya25/30-dars26 daqiqa
Mundarija (36)

Motion'da chiqish va layout animatsiyalari: AnimatePresence, exit, layout va layoutId

Qisqacha: React o'chirilgan komponentning elementini DOM'dan darhol olib tashlaydi. Shuning uchun exit animatsiyasiga vaqt qolmaydi. AnimatePresence elementni chiqish animatsiyasi tugaguncha ushlab turadi. mode="wait" da avval eski element chiqadi, keyin yangisi kiradi. layout prop joylashuv o'zgarishini transform bilan silliq qiladi, layoutId esa elementni boshqa joyga "uchiradi". Ehtiyot bo'ling: chiqayotgan element state'da yo'q, DOM'da bor. vazifalar da u filtr va testlarni buzdi. Shuning uchun kanonda chiqish animatsiyasi o'chirishdan oldin bajariladi.

Bu darsda

  • exit nega AnimatePresence siz ishlamasligini va u qanday "kechiktirib o'chirish"ini konsol bilan ko'rasiz.
  • key almashganda eski va yangi element navbatini mode (sync, wait, popLayout) bilan boshqarasiz.
  • usePresence bilan chiqishni o'zingiz boshqarasiz — Motion animatsiyasisiz ham.
  • layout, layoutId va LayoutGroup bilan joylashuv o'zgarishini silliq qilasiz va FLIP g'oyasini tushuntira olasiz.
  • Kanon o'lchovi bilan ro'yxatdagi AnimatePresence ning xavfini ko'rasiz va vazifalar ga o'chirishdagi chiqish animatsiyasini qo'shasiz (holat 25 davomi).

Oldin bilishingiz kerak: Motion II: variants, stagger va gestures, Motion I: motion komponentlar, CSS animatsiyalari React'da, Web Animations API, Ro'yxatlarni render qilish va key.

1. Nega bu kerak?

Oldingi ikki darsda elementlarni ekranga kiritdik: initial → animate, variantlar, ketma-ketlik. Endi teskari savol. Jasur aka vazifalar ni sinab ko'rdi: yangi vazifa yumshoq paydo bo'ladi, o'chirilgani esa "pat" etib yo'qoladi. "Kirishi chiroyli-yu, chiqishi kesib tashlangandek", dedi u.

Sababi React'da. State o'zgardi, komponent endi chizilmaydi — React uning DOM elementini shu commit'dayoq olib tashlaydi. Animatsiya qilish uchun element kerak, u esa allaqachon yo'q.

Hayotiy o'xshatish: teatr. Aktyor rolini tugatdi, rejissyor esa pardani darhol tushirib, sahnani bo'shatib qo'ydi — aktyor ta'zim qilishga ulgurmadi. AnimatePresence — sahna ortidagi yordamchi: "parda tushsin, lekin aktyor ta'zimini tugatsin, keyin u sahnadan chiqadi".

Ikkinchi muammo — joylashuv. Ro'yxatdan qator o'chsa, pastdagilar bir zumda yuqoriga "sakraydi". Karta kengaysa, qo'shnilari ham sakraydi. Ko'z bu sakrashni kuzatolmaydi va "nima bo'ldi?" deb qoladi. layout prop shu sakrashni silliq harakatga aylantiradi. Oxirida esa eng muhim savol: chiroyli chiqish ro'yxatni buzmaydimi? vazifalar kanonida buzdi — o'lchov bilan ko'ramiz.

2. exit va AnimatePresence

2.1 React elementni kutmaydi

Avval exit ni AnimatePresence siz yozib ko'ramiz. Karta useEffect bilan DOM'ga qo'shilgan va olib tashlangan paytini yozadi (useEffect I darsidagi tozalash funksiyasi):

jsx
import { useEffect, useState } from "react";
import { motion } from "motion/react";

function Card() {
  useEffect(() => {
    console.log("karta DOM'da");
    return () => console.log("karta olib tashlandi");
  }, []);
  return (
    <motion.p
      exit={{ opacity: 0 }}
      transition={{ duration: 0.3 }}
      onAnimationComplete={(d) => console.log("tugadi:", d)}
    >
      Osh — 35 000 so'm
    </motion.p>
  );
}

export default function App() {
  const [show, setShow] = useState(true);
  return (
    <div>
      <button onClick={() => setShow(false)}>Yashirish</button>
      {show && <Card />}
    </div>
  );
}

Konsolda:

text
karta DOM'da
karta olib tashlandi

Ekranda:

text
Yashirish

tugadi: qatori yo'q — chiqish animatsiyasi umuman boshlanmadi. exit prop'i yozilgan, lekin uni ishga tushiradigan hech kim yo'q: React Card ni ro'yxatdan chiqardi va DOM'dan o'chirdi. Xato ham, ogohlantirish ham chiqmaydi — exit jim e'tiborsiz qoldi.

2.2 AnimatePresence — o'chirishni kechiktiradi

Endi o'sha kartani AnimatePresence ichiga qo'yamiz. Bitta shart: bola key ga ega bo'lsin — AnimatePresence bolalarni shu kalit bo'yicha taniydi (Ro'yxatlar va key darsidagi o'sha key):

jsx
import { useEffect, useState } from "react";
import { AnimatePresence, motion } from "motion/react";

function Card() {
  useEffect(() => {
    console.log("karta DOM'da");
    return () => console.log("karta olib tashlandi");
  }, []);
  return (
    <motion.p
      exit={{ opacity: 0 }}
      transition={{ duration: 0.3 }}
      onAnimationComplete={(d) => console.log("tugadi:", d)}
    >
      Osh — 35 000 so'm
    </motion.p>
  );
}

export default function App() {
  const [show, setShow] = useState(true);
  return (
    <div>
      <button onClick={() => setShow(false)}>Yashirish</button>
      <AnimatePresence
        onExitComplete={() => console.log("onExitComplete")}
      >
        {show && <Card key="karta" />}
      </AnimatePresence>
    </div>
  );
}

Konsolda:

text
karta DOM'da
tugadi: {opacity: 0}
onExitComplete
karta olib tashlandi

Ekranda:

text
Yashirish

Tartibni o'qiymiz:

  1. «Yashirish» bosildi — show endi false, React App ni qayta chizdi va Card ni bolalar ro'yxatida topmadi.
  2. AnimatePresence buni sezdi va eski elementni (o'sha key="karta") chizishda davom etdi. Ichida exit animatsiyasini boshladi.
  3. 300 ms o'tdi — tugadi: {opacity: 0}. Animatsiya exit dagi qiymatga yetdi.
  4. onExitComplete — barcha chiquvchilar tugadi.
  5. Shundan keyingina React kartani haqiqatan olib tashladi: effekt tozalanishi oxirida ishladi.
sequenceDiagram
  participant U as Foydalanuvchi
  participant R as React
  participant P as AnimatePresence
  participant D as DOM
  U->>R: setShow(false)
  R->>P: yangi bolalar: karta yo'q
  P->>D: eski kartani qoldiradi, exit boshlanadi
  D-->>P: 300 ms — animatsiya tugadi
  P->>R: endi olib tashla
  R->>D: karta o'chdi, effekt tozalandi

Diagrammadagi asosiy narsa — o'rtadagi qadam: state allaqachon "karta yo'q" deydi, DOM'da esa karta hali bor. Bu farq butun dars davomida qaytib keladi.

2.3 Uchta qoida

AnimatePresence faqat shu shartlarda ishlaydi:

  • O'zi doim chizilgan bo'lsin. Shart uning ichida turadi: <AnimatePresence>{show && …}</AnimatePresence>. Teskarisi — {show && <AnimatePresence>…</AnimatePresence>} — AnimatePresence ning o'zini o'chiradi va u hech narsani ushlab tura olmaydi.
  • To'g'ridan-to'g'ri bolalarda key. Shartli bitta bola uchun ham. Ro'yxatda — har elementning barqaror id'si (indeks emas).
  • exit bola ichida bo'lsin. exit istalgan chuqurlikdagi motion komponentida yozilishi mumkin — AnimatePresence hammasini kutadi. Bizda u Card ichidagi motion.p da.

Tekshirib ko'ring: Sardor shunday yozdi: {open && <AnimatePresence><motion.div key="m" exit={{ opacity: 0 }} /></AnimatePresence>}. Menyu yopilganda chiqish animatsiyasi bormi?

Javob

Yo'q. open false bo'lganda React AnimatePresence ning o'zini o'chiradi — bolasini ushlab turadigan komponent qolmaydi. Konsol jim, xuddi «React elementni kutmaydi» bo'limidagidek. To'g'risi — <AnimatePresence>{open && <motion.div key="m" … />}</AnimatePresence>: AnimatePresence doim joyida, faqat bolasi kelib-ketadi.

3. Bitta joyda almashinuv: key va mode

3.1 key o'zgarsa — eski chiqadi, yangisi kiradi

«Bahor» kassasidagi "Kun taomi" kartasi: bugun osh, «Keyingi» bosilsa — lag'mon. Komponent bitta, faqat key o'zgaradi. State'ning saqlanishi va key darsidan bilasiz: yangi key — React uchun boshqa komponent. Demak eski karta "o'chdi", yangisi "qo'shildi" — AnimatePresence uchun bu chiqish + kirish.

Har karta animatsiya boshlanishi va tugashini yozadi. initial={false} — sahifa ochilgandagi birinchi karta kirish animatsiyasisiz chiqsin:

jsx
import { useState } from "react";
import { AnimatePresence, motion } from "motion/react";

const DISHES = ["Osh", "Lag'mon"];

function Dish({ name }) {
  const what = (d) => (d.opacity === 0 ? "chiqish" : "kirish");
  return (
    <motion.p
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
      transition={{ duration: 0.2 }}
      onAnimationStart={(d) => console.log(name, what(d), "...")}
      onAnimationComplete={(d) => console.log(name, what(d), "✓")}
    >
      {name}
    </motion.p>
  );
}

export default function App() {
  const [i, setI] = useState(0);
  return (
    <div>
      <button onClick={() => setI(1)}>Keyingi</button>
      <AnimatePresence initial={false}>
        <Dish key={DISHES[i]} name={DISHES[i]} />
      </AnimatePresence>
    </div>
  );
}

Konsolda:

text
Osh chiqish ...
Lag'mon kirish ...
Osh chiqish ✓
Lag'mon kirish ✓

Ekranda:

text
Keyingi
Lag'mon

Standart rejim — sync: chiqish va kirish bir vaqtda. 200 ms davomida ekranda ikkala karta bor: osh so'nmoqda, lag'mon paydo bo'lmoqda. Ular ustma-ust emas, ketma-ket turadi — lag'mon oshning ostida paydo bo'ladi va osh ketgach yuqoriga sakraydi.

3.2 mode="wait" — navbat bilan

Sakrashni yo'qotishning birinchi yo'li — kutish: avval eski chiqib bo'lsin, keyin yangisi kirsin. O'sha komponent, faqat mode:

jsx
export default function App() {
  const [i, setI] = useState(0);
  return (
    <div>
      <button onClick={() => setI(1)}>Keyingi</button>
      <AnimatePresence initial={false} mode="wait">
        <Dish key={DISHES[i]} name={DISHES[i]} />
      </AnimatePresence>
    </div>
  );
}

Konsolda:

text
Osh chiqish ...
Osh chiqish ✓
Lag'mon kirish ...
Lag'mon kirish ✓

Ekranda:

text
Keyingi
Lag'mon

Endi lag'mon osh chiqib bo'lgandan keyin boshlandi. Ekranda bir paytda faqat bitta karta — sakrash yo'q. Narxi — vaqt: almashinuv 200 emas, 400 ms davom etdi. Slayder, bosqichli forma ("1-qadam → 2-qadam"), sahifa sarlavhasi — wait uchun tabiiy joylar.

Diqqat: mode="wait" bir paytda faqat bitta bola bilan ishlaydi. Unga ro'yxat bersangiz, Motion konsolga ogohlantirish yozadi va xulq kutilmagan bo'ladi. Ro'yxat — sync yoki popLayout.

3.3 mode="popLayout" — chiquvchini oqimdan "sug'urib olish"

Uchinchi rejim boshqacha yo'l tanlaydi: chiqayotgan elementni position: absolute qilib, sahifa oqimidan chiqarib oladi. U o'z joyida so'nib ketadi, qo'shnilar esa darhol yangi joyiga o'tadi. layout bilan birga (keyingi bo'limda) qo'shnilar silliq siljiydi.

jsx
<AnimatePresence mode="popLayout">
  {items.map((item) => (
    <motion.li key={item.id} layout exit={{ opacity: 0 }}>
      {item.text}
    </motion.li>
  ))}
</AnimatePresence>

Ikki shart bor. Birinchisi: bola o'z komponentingiz bo'lsa, u ref ni DOM elementga o'tkazishi kerak — Motion elementni o'lchaydi (useRef II). Ikkinchisi — position: absolute ni Motion qanday qo'yadi? Motion 14.0.0 manba kodida (PopChild): <style> tegini yaratib, <head> ga qo'shadi.

Bu tanish tuzoq. Radix Primitives chuqur darsida modal menyu ham <style> qo'shgan va vazifalar CSP'si (style-src 'self') uni to'sgan edi. Biz popLayout ni ham o'sha CSP ostida Chrome 155 da sinab ko'rdik (alohida tajriba sahifasi, uchta qator, birinchisi o'chiriladi):

text
pop sync (200 ms): ["Osh:static:16","Lag'mon:static:34","Manti:static:52"] style teglari: 0
pop popLayout (200 ms): ["Osh:static:16","Lag'mon:static:34","Manti:static:52"] style teglari: 1
{"csp":["style-src-elem inline"]}

Har qator — matn:position:tepadan_px. popLayout da <style> qo'shildi, CSP uni to'sdi — chiqayotgan «Osh» static qoldi, qo'shnilar joyidan qimirlamadi. Ya'ni qattiq CSP ostida popLayout jim sync ga aylanadi, konsolda esa Applying inline style violates … 'style-src 'self'' xatosi. Saytdagi natija oynasi inline <style> ga ruxsat beradi — u yerda ishlaydi. Motion'da MotionConfig ning nonce sozlamasi bor, lekin u faqat CSP'ingiz nonce bilan qurilgan bo'lsa yordam beradi.

Rejim Eski va yangi Qachon
sync (standart) bir vaqtda, ikkalasi oqimda ro'yxatlar, mustaqil elementlar
wait avval chiqish, keyin kirish bitta element almashinuvi: slayd, qadam
popLayout chiquvchi oqimdan chiqadi layout li ro'yxat; CSP <style> ga ruxsat bersa

Tekshirib ko'ring: Bosqichli bron formasida «Keyingi» bosilganda 1-qadam chapga chiqib, 2-qadam o'ngdan kirishi kerak. Ikki qadam bir paytda ko'rinmasin. Qaysi rejim?

Javob

mode="wait". Bola bitta — joriy qadam, uning key i — qadam raqami. wait avval 1-qadamning exit ini tugatadi. Keyin 2-qadamning kirishini boshlaydi — ikkalasi bir paytda ekranda bo'lmaydi. sync da esa 200 ms davomida ikkala forma birga turadi va sahifa balandligi sakraydi.

4. Chiqishni o'zingiz boshqarish: usePresence

exit prop'i Motion animatsiyasi uchun. Lekin chiqish paytida boshqa ish kerak bo'lsa-chi — CSS animatsiyasi, Web Animations API, "saqlanmoqda" xabari? usePresence hooki komponentga ikki narsa beradi:

  • isPresent — false bo'lsa, siz allaqachon "o'chirilgansiz", lekin AnimatePresence sizni ushlab turibdi.
  • safeToRemove — "ishim tugadi, endi olib tashlang" signali.
jsx
import { useEffect, useState } from "react";
import { AnimatePresence, usePresence } from "motion/react";

function Card() {
  const [isPresent, safeToRemove] = usePresence();
  useEffect(() => {
    if (isPresent) return;
    console.log("chiqish boshlandi");
    const timer = setTimeout(() => {
      console.log("endi olib tashlash mumkin");
      safeToRemove();
    }, 200);
    return () => clearTimeout(timer);
  }, [isPresent, safeToRemove]);
  return <p>{isPresent ? "Karta" : "Karta (chiqmoqda)"}</p>;
}

export default function App() {
  const [show, setShow] = useState(true);
  return (
    <div>
      <button onClick={() => setShow(false)}>Yashirish</button>
      <AnimatePresence>{show && <Card key="k" />}</AnimatePresence>
    </div>
  );
}

Konsolda:

text
chiqish boshlandi
endi olib tashlash mumkin

Ekranda:

text
Yashirish

ekran("chiqmoqda") oraliq holatni tekshiradi: bosishdan keyin karta hali DOM'da va o'z matnini o'zgartirgan. 200 ms dan keyin safeToRemove() chaqirildi — karta ketdi.

Faqat "chiqyapmanmi?" ni bilish kerak bo'lsa (masalan, chiqayotgan formadagi tugmani o'chirib qo'yish) — useIsPresent() yetadi: u faqat isPresent ni qaytaradi.

Diqqat: safeToRemove ni chaqirishni unutsangiz, element DOM'da abadiy qoladi. Shuning uchun taymer yoki Promise.race bilan "ko'pi bilan N ms" chegarasini qo'ying — «Vazifalar qadami» da kanon aynan shunday qiladi.

5. Layout animatsiyasi: layout prop

5.1 FLIP: o'lcha, siljit, qaytar

Layout animatsiyasining muammosi: width, height, top ni animatsiya qilish — har kadrda brauzer layout ni qayta hisoblaydi. UI animatsiya tamoyillari va Rendering pipeline darslaridan bilasiz: bu qimmat, telefonda kadrlar tushib qoladi. Arzoni — transform va opacity.

Motion layout prop bilan FLIP usulini qo'llaydi. Nomi to'rt qadamning bosh harflari:

flowchart LR
  F["First: eski o'rin va o'lcham"] --> L["Last: React yangi holatni chizdi"]
  L --> I["Invert: transform bilan eski joyga qaytar"]
  I --> P["Play: transform → none"]
  1. First — o'zgarishdan oldin element o'lchanadi (getBoundingClientRect).
  2. Last — React yangi holatni chizadi; element darhol yangi joyda. Yana o'lchanadi.
  3. Invert — farq hisoblanadi va transform: translate(…) scale(…) bilan element ko'zga eski joyida ko'rinadi.
  4. Play — transform asta-sekin none ga qaytadi. Ko'z silliq siljishni ko'radi.

Hiyla shundaki, layout bir marta hisoblanadi (Last), animatsiyaning o'zi esa faqat transform — kompozitorda.

5.2 Birinchi layout

«Bahor» menyusidagi karta bosilsa kengayib, tarkibini ko'rsatadi:

jsx
import { useState } from "react";
import { motion } from "motion/react";

const box = {
  border: "2px solid #1d4ed8",
  borderRadius: 12,
  padding: 12,
  width: 140,
  background: "#fff",
  color: "#111",
};

export default function App() {
  const [open, setOpen] = useState(false);
  return (
    <motion.button
      layout
      onClick={() => setOpen(!open)}
      style={{ ...box, width: open ? 260 : 140 }}
      transition={{ duration: 0.3 }}
    >
      <motion.b layout="position">Osh</motion.b>
      {open && <p>Guruch, sabzi, go'sht — 35 000 so'm</p>}
    </motion.button>
  );
}

Ekranda:

text
Osh
Guruch, sabzi, go'sht — 35 000 so'm

«Ishga tushir» dan keyin kartani bir necha marta bosing: u siljib kengayadi va torayadi. layout bo'lmasa — bir zumda sakraydi. Tekshiruvchi bu yerda faqat yakuniy matnni solishtiradi: jsdom'da layout o'lchab bo'lmaydi (hamma o'lcham 0), animatsiya faqat haqiqiy brauzerda ko'rinadi.

5.3 Cho'zilish va layout="position"

scale bilan o'lcham o'zgartirilganda bolalar ham cho'ziladi: matn bir lahza "ezilgan" ko'rinadi. Motion buni ikki yo'l bilan tuzatadi:

  • border-radius va box-shadow ni avtomatik to'g'rilaydi — burchaklar cho'zilmaydi (shuning uchun ular style da bo'lsin, sinfda emas).
  • Bola ham layout olsa, Motion uning teskari masshtabini hisoblaydi. Yuqoridagi <motion.b layout="position"> — shu: sarlavha faqat o'rnini animatsiya qiladi, o'lchami cho'zilmaydi.

layout ning qiymatlari: true (o'rin va o'lcham), "position" (faqat o'rin), "size" (faqat o'lcham).

5.4 layoutId: bir element boshqa joyga "uchadi"

Ikki turli element bir xil layoutId olsa, Motion ularni bitta deb biladi: biri yo'qolib, ikkinchisi paydo bo'lganda, o'tish eski joydan yangi joyga uchish bo'ladi. Klassik misol — tablar ostidagi chiziq:

jsx
import { Fragment, useState } from "react";
import { motion } from "motion/react";

const TABS = ["Osh", "Lag'mon", "Manti"];
const line = {
  display: "block",
  height: 3,
  background: "#1d4ed8",
};

export default function App() {
  const [tab, setTab] = useState("Osh");
  return (
    <div>
      {TABS.map((t) => (
        <Fragment key={t}>
          <button
            aria-pressed={tab === t}
            onClick={() => setTab(t)}
            style={{ padding: 8 }}
          >
            {t}
            {tab === t && (
              <motion.span layoutId="tab-chiziq" style={line} />
            )}
          </button>{" "}
        </Fragment>
      ))}
      <p>Tanlangan: {tab}</p>
    </div>
  );
}

Ekranda:

text
Osh Lag'mon Manti
Tanlangan: Manti

Har tugmada chiziq o'zining span i: «Manti» bosilganda oshdagi span o'chadi, mantidagi chiziladi. layoutId ularni bog'laydi — chiziq osh ostidan manti ostiga suriladi. Kartadan to'liq sahifaga o'tuvchi rasm (galereya), savatchaga "uchadigan" mahsulot — hammasi shu g'oya.

aria-pressed ni unutmang: chiziq — faqat ko'rinish. Malika (ekran o'quvchi) qaysi tab tanlanganini atributdan biladi, chiziqdan emas.

5.5 LayoutGroup: qo'shni komponentlar bir-birini bilsin

layout faqat o'zi qayta chizilgan komponentni o'lchaydi. Akkordeonda bitta band ochilsa, boshqa komponentdagi pastki band siljiydi — lekin u qayta chizilmadi, o'zgarishni bilmaydi va sakraydi. Ularni LayoutGroup bilan o'rang: guruhdagi birortasi o'zgarsa, hammasi o'lchanadi.

jsx
import { useState } from "react";
import { LayoutGroup, motion } from "motion/react";

function Item({ title, text }) {
  const [open, setOpen] = useState(false);
  return (
    <motion.div layout style={{ borderBottom: "1px solid #999" }}>
      <motion.button layout="position" onClick={() => setOpen(!open)}>
        {title}
      </motion.button>
      {open && <p>{text}</p>}
    </motion.div>
  );
}

export default function App() {
  return (
    <LayoutGroup>
      <Item title="Osh" text="35 000 so'm, har kuni" />
      <Item title="Manti" text="30 000 so'm, juma kuni" />
    </LayoutGroup>
  );
}

Ekranda:

text
Osh
35 000 so'm, har kuni
Manti

LayoutGroup ning ikkinchi vazifasi — nomlar fazosi. Sahifada ikkita tab paneli bo'lsa va ikkalasida layoutId="tab-chiziq" bo'lsa, chiziq bir paneldan boshqasiga uchib ketadi. <LayoutGroup id="menyu"> va <LayoutGroup id="buyurtma"> — ichidagi layoutId lar endi alohida.

5.6 Narxi: layout — og'ir imkoniyat

layout uchun Motion o'lchash va "proyeksiya" kodini yuklaydi. Keyingi darsda ko'radigan LazyMotion da bu farq aniq: kichik domAnimation to'plamida layout yo'q, u faqat katta domMax da. Kanon tajribasida (tajriba/kutubxonalar.txt, gzip): to'liq motion (layout bilan) — +40.4 KB, LazyMotion + domAnimation — +27.1 KB. vazifalar da layout ishlatilmadi: qo'shimcha sababi — «Tartiblash» rejimidagi dnd-kit ham transform ni boshqaradi, ikki kutubxona bitta xususiyatda to'qnashadi (TEXNIK-QARZ 57).

Harakatni kamaytirish: <MotionConfig reducedMotion="user"> (Motion I) layout va layoutId siljishlarini ham o'chiradi — element darhol yangi joyida bo'ladi. exit dagi opacity esa qoladi: chiqayotgan narsa joyida so'nadi.

Tekshirib ko'ring: Karta kengayganda uning ichidagi matn bir lahza cho'zilib, keyin to'g'rilanyapti. Nima qilasiz?

Javob

Matnni o'rab turgan elementga ham layout (ko'pincha layout="position") bering. Ota scale bilan cho'zilganda, Motion layout li bolaga teskari masshtab qo'llaydi va matn o'z o'lchamida qoladi. Burchak yumaloqligi cho'zilsa — borderRadius ni sinf orqali emas, style orqali bering: Motion uni faqat shunda to'g'rilay oladi.

6. Ro'yxatda AnimatePresence: kanon nega rad etdi

6.1 Birinchi variant va o'lchov

Eng tabiiy yechim — ro'yxatni AnimatePresence ga o'rash: har li ga exit, o'chirilgani so'nib ketadi. Kanon holat 25 ni avval aynan shunday yozdi (exit 150 ms) va brauzer testlarini ishga tushirdi. Natija (natijalar/25-animatepresence.txt, Chrome 155, qisqartirilgan):

text
## e2e.mjs (159): ❌ 7 ta (hammasi — exit paytida DOM'da qolgan qatorlar: filtr, qidiruv)
❌ Orqaga → faol: ["3:Onamga qo'ng'iroq:-","4:Sabzi olish #bozor:-","1:Non olish:x"] (kutilgan ["3:Onamga qo'ng'iroq:-","4:Sabzi olish #bozor:-"])
❌ qidiruv: topilmadi — bosh xabar: [9,false,"«zzz» bo'yicha vazifa topilmadi.","0 ta topildi"] (kutilgan [0,false,"«zzz» bo'yicha vazifa topilmadi.","0 ta topildi"])
## e2e-18.mjs: forma rad etildi — exit
❌ forma: rad etildi — vazifa yo'q, matn maydonda qoldi (useActionState): [true,"Yo'qolmaydigan matn"] (kutilgan [false,"Yo'qolmaydigan matn"])
## e2e-19.mjs:508 — p fon tabda (p2 ochilgan): 'so'ramasdan o'chirish' — li[data-id=1] 10 s da DOM'dan ketmadi (exit animatsiyasi rAF kutadi)

Qatorlarni o'qiymiz:

  • «Orqaga → faol» — "faol" filtriga qaytildi. Bajarilgan «Non olish» (x) ro'yxatdan chiqishi kerak edi. U chiqib ketyapti — DOM'da hali bor. Test ro'yxatni shu paytda o'qidi va uchta qator ko'rdi.
  • «qidiruv: topilmadi» — «zzz» bo'yicha hech narsa topilmadi, xabar to'g'ri ("0 ta topildi"). Lekin DOM'da 9 ta li — hammasi chiqib ketyapti. Ekranda bir lahza "topilmadi" xabari va 9 ta so'nayotgan qator birga turadi.
  • «forma: rad etildi» — server qo'shishni rad etdi, optimistik qator qaytarib olindi. U ham chiqish animatsiyasi bilan "ketdi" — test uni hali bor deb ko'rdi.
  • Fon tab — eng og'iri. Ikkinchi tabda vazifa o'chirildi, birinchi tab fonda. Motion animatsiyasi kadrlar bilan yuradi (requestAnimationFrame), fon tabda esa brauzer kadrlarni to'xtatadi. O'chirilgan qator 10 soniyada ham DOM'dan ketmadi.

6.2 Muammoning ildizi

«AnimatePresence — o'chirishni kechiktiradi» bo'limidagi diagrammani eslang: state "yo'q" deydi, DOM esa "bor". Bitta kartada bu zararsiz. Ro'yxatda esa bu farqni hamma ko'radi:

  • Testlar — DOM'ni o'qiydi va eski qatorni sanaydi.
  • Ekran o'quvchi — Malika "o'chirilgan" vazifani hali o'qishi mumkin; chiqayotgan qatordagi tugma fokus ham olishi mumkin.
  • Foydalanuvchi — filtr yoki qidiruvda har harfdan keyin o'nlab qatorlar so'nib ketadi. Bu "tez ishlaydigan" filtrni sekin ko'rsatadi.
  • Fon tab — animatsiya tugamasa, element hech qachon ketmaydi.

Testlarni "animatsiyani kutadigan" qilib o'zgartirish mumkin edi. Lekin testlar shu yerda haqiqiy muammoni ko'rsatdi: ilova holati va ekran bir-biriga mos emas. Qoida: animatsiya — bezak, DOM holati darhol to'g'ri bo'lsin.

6.3 Kanon yechimi: chiqish — faqat o'chirishda, o'chirishdan oldin

Ro'yxatdan qator uch xil sabab bilan ketadi: foydalanuvchi o'chirdi, filtr/qidiruv uni yashirdi, server rad etdi. Chiqish animatsiyasi faqat birinchisiga kerak — odam o'zi bosgan narsaning natijasini ko'rishi kerak. Qolganlari darhol yo'qolsin.

Shuning uchun tartib teskari: avval animatsiya, keyin state.

flowchart LR
  B["O'chirish bosildi"] --> A["li.animate: 150 ms"]
  A --> R{"tugadi yoki 300 ms"}
  R --> S["state'dan o'chirish"]
  S --> D["React qatorni olib tashlaydi"]

Animatsiya paytida qator haqiqatan hali ro'yxatda — state ham, DOM ham "bor" deydi, ziddiyat yo'q. Animatsiya tugagach state o'zgaradi va React qatorni odatdagidek darhol olib tashlaydi. Fon tab uchun — Promise.race: animatsiya 300 ms da tugamasa ham, o'chirish baribir bajariladi.

Animatsiya uchun Motion emas, brauzerning o'zidagi Web Animations API (element.animate). Sababi hajm: Motion'ning useAnimate i animatsiya yadrosini birinchi yuklashga qaytarardi. Kanon esa uni alohida, kechroq yuklaydi — bu keyingi darsning mavzusi.

7. Vazifalar qadami: o'chirishda chiqish (holat 25 davomi)

7.1 Nima qo'shiladi

Motion I va Motion II darslarida feat/motion branch'ida yangi qatorning kirishi tayyor bo'ldi: useQatorHarakati, m.li, HarakatSozlamasi, "yangi" qatorlar to'plami. Bugun ikkinchi yarmi — chiqish: foydalanuvchi o'chirgan qator o'ngga siljib so'nadi (150 ms, WAAPI), keyin ro'yxatdan o'chadi. Filtr, qidiruv va server rad etishida esa qator darhol yo'qoladi — «Kanon yechimi» bo'limidagi qoida.

O'zgaradigan fayllar ikkita: src/ui/qator-harakati.ts (yangi funksiya) va App.tsx (o'chirish tartibi).

7.2 chiqishAnimatsiyasi

src/ui/qator-harakati.ts ning kirish qismi Motion I da yozilgan. Endi faylga ikkinchi funksiya qo'shiladi:

ts
/**
 * O'chirilayotgan qator — avval ko'rinishdan chiqadi, keyin
 * ro'yxatdan. Brauzerning o'z Web Animations API'si (Motion'ning
 * useAnimate'i butun animatsiya yadrosini birinchi yuklashga
 * qo'shardi). Ko'pi bilan 300 ms kutadi: fon tabda animatsiya
 * tugashi kechikishi mumkin
 */
export async function chiqishAnimatsiyasi(
  li: Element | null,
): Promise<void> {
  if (
    li === null ||
    typeof li.animate !== "function" || // jsdom (Vitest)
    matchMedia("(prefers-reduced-motion: reduce)").matches
  ) {
    return;
  }
  const animatsiya = li.animate(
    [
      { opacity: 1, transform: "none" },
      { opacity: 0, transform: "translateX(1rem)" },
    ],
    { duration: 150, easing: "ease-in", fill: "forwards" },
  );
  await Promise.race([
    animatsiya.finished.then(
      () => undefined,
      () => undefined,
    ),
    new Promise<void>((tugadi) => setTimeout(tugadi, 300)),
  ]);
}

Uchta himoya qatori:

  • Uchta erta return — qator topilmadi, animate yo'q (Vitest'dagi jsdom) yoki harakat kamaytirilgan: animatsiyasiz, darhol davom.
  • fill: "forwards" — animatsiya tugagach qator opacity: 0 holatida qoladi. Bu bo'lmasa, state o'zgarguncha bir lahza qayta ko'rinib "miltillardi".
  • Promise.race — ikkita va'daning qaysi biri oldin bajarilsa, shu. Animatsiya tugadi yoki 300 ms o'tdi. finished rad etilsa ham (animatsiya bekor qilinsa) — undefined, xato tashlanmaydi.

7.3 App.tsx — avval chiqish, keyin o'chirish

App.tsx dagi ochir — chiqish, keyin haqiqiy o'chirish:

diff
+import { chiqishAnimatsiyasi } from "./ui/qator-harakati.ts";
...
+    // 20/#25: avval qator ko'rinishdan chiqadi (150 ms),
+    // keyin o'chadi
+    const li =
+      tugmalar.current.get(`${id}:ochir`)?.closest("li") ?? null;
+    void chiqishAnimatsiyasi(li).then(() =>
+      ochirishniBajar({ id, matn, zaxira }),
+    );
+  }
+
+  function ochirishniBajar({ id, matn, zaxira }: OchirishSorovi) {
     const amal = { tur: "ochirildi", id } as const;
     elonQil(`«${matn}» o'chirildi.`);

Qatorning li si «O'chirish» tugmasi ref'idan topiladi (Fokus boshqaruvi uchun tugmalar ro'yxati allaqachon bor). void — va'dani ataylab kutmaymiz, lint'ga "unutmadim" deymiz.

Ilgari ochir funksiyasi bitta edi. Endi u ikkiga bo'lindi: ochir — animatsiya, ochirishniBajar — e'lon, fokus va serverga so'rov (eski tana o'zgarmagan). Animatsiya paytida qator state'da ham, DOM'da ham bor — ziddiyat yo'q.

7.4 Kanon tekshiruvlari (holat 25)

20-qism brauzer ssenariysidagi (e2e-20.mjs, Chrome 155, CSP ostida) chiqishga tegishli qatorlar (natija-20-25.txt):

text
✅ o'chirish: avval chiqish (opacity+transform, 150 ms), keyin qator yo'q: [true,"opacity+transform:150",false]
✅ o'chirish: e'lon va fokus qo'shniga: ["«Animatsiya sinovi» o'chirildi.","ochir"]
✅ qo'shish/o'chirish: CLS 0 (hadRecentInput hisobga olinmaydi): 0
✅ reduced-motion: qatorda animatsiya yo'q, o'chirilgan qator darhol yo'q: [0,false]

Ssenariy Element.prototype.animate ni "kuzatadi". Birinchi qatordagi massiv: tasdiqdan 40 ms keyin qator hali bor (true), oxirgi animatsiya — opacity+transform, 150 ms; keyin qator yo'q (false). Ikkinchi qator: e'lon chiqdi, fokus qo'shni qatorning «O'chirish» tugmasida — 18-qismdagi fokus qoidasi animatsiya bilan ham buzilmadi. CLS 0 — chiqish transform bilan, qo'shnilar sakramaydi. Oxirgisi: harakat kamaytirilganda qator animatsiyasiz, darhol o'chadi.

Commit hali yo'q: holat 25 butun Motion qadamini (#23–#26) qamraydi. Bitta commit va vazifalar ning v10 yakuni — Motion IV darsida, LazyMotion o'lchovidan keyin.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
exit ishlamaydi, konsol jim AnimatePresence yo'q yoki u ham shart bilan o'chadi <AnimatePresence>{shart && …}</AnimatePresence>
chiqish yo'q, kirish bor bolada key yo'q yoki indeks barqaror key
almashinuvda ikki element, sahifa sakraydi sync rejim mode="wait" yoki popLayout + layout
mode="wait" da ro'yxat g'alati wait — bitta bola uchun sync / popLayout
popLayout ishlamaydi, style-src xatosi Motion <style> qo'shadi, CSP to'sadi sync + layout yoki nonce'li CSP
element DOM'da qolib ketdi safeToRemove chaqirilmadi / fon tab taymer yoki Promise.race
layout da matn cho'ziladi bola layout siz bolaga layout="position"
layoutId chiziq boshqa panelga uchadi ikkita bir xil layoutId LayoutGroup id="…"
filtrdan keyin eski qatorlar ko'rinadi ro'yxat AnimatePresence da chiqish — faqat o'chirishda, state'dan oldin

9. Mashqlar

Mashqlar kurs/mashqlar/20/25-chiqish-layout/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): qaysi rejim?

Har vaziyat uchun AnimatePresence ning mode qiymatini yozing (qo'shtirnoqsiz):

  • Rasm galereyasi: bitta katta rasm, «Keyingi» bilan almashadi, ikki rasm bir paytda ko'rinmasin →
  • Bildirishnomalar ro'yxati: har biri o'z vaqtida yopiladi, boshqalari joyida →
  • layout li ro'yxat: o'chirilgan qator so'nayotganda pastdagilar darhol yuqoriga sirpanib chiqsin (CSP <style> ga ruxsat beradi) →
Yechim
  • wait — bitta bola, ketma-ket almashinuv. Ikki rasm bir paytda bo'lmaydi.
  • sync — standart. Bildirishnomalar mustaqil: biri chiqayotganda boshqasi kirsa, kutish shart emas.
  • popLayout — chiquvchi oqimdan chiqadi, layout li qo'shnilar darhol siljiydi. Faqat CSP inline <style> ga ruxsat bersa: vazifalar dagidek style-src 'self' da u jim sync ga aylanadi.

2-mashq (o'rta): "Saqlandi" xabari chiqib ketmayapti

«Bahor» buyurtma formasida «Saqlash» bosilsa, "Saqlandi" xabari chiqadi va 300 ms dan keyin yo'qolishi kerak. Yo'qolishi — 200 ms so'nish bilan. Hozir xabar so'nmasdan "pat" etib yo'qoladi. Kodni tuzating: xabar exit bilan chiqsin, chiqish tugaganda konsolga xabar ketdi yozilsin.

Ishora: «Uchta qoida» bo'limi — AnimatePresence qayerda turishi kerak? Tugaganini bilish — onExitComplete.

jsx
import { useState } from "react";
import { AnimatePresence, motion } from "motion/react";

export default function App() {
  const [saved, setSaved] = useState(false);
  function save() {
    setSaved(true);
    setTimeout(() => setSaved(false), 300);
  }
  return (
    <div>
      <button onClick={save}>Saqlash</button>
      {saved && (
        <AnimatePresence>
          <motion.p
            key="xabar"
            exit={{ opacity: 0 }}
            transition={{ duration: 0.2 }}
          >
            Saqlandi
          </motion.p>
        </AnimatePresence>
      )}
    </div>
  );
}

Ekranda:

text
Saqlash
Yechim
jsx
import { useState } from "react";
import { AnimatePresence, motion } from "motion/react";

export default function App() {
  const [saved, setSaved] = useState(false);
  function save() {
    setSaved(true);
    setTimeout(() => setSaved(false), 300);
  }
  return (
    <div>
      <button onClick={save}>Saqlash</button>
      <AnimatePresence
        onExitComplete={() => console.log("xabar ketdi")}
      >
        {saved && (
          <motion.p
            key="xabar"
            role="status"
            exit={{ opacity: 0 }}
            transition={{ duration: 0.2 }}
          >
            Saqlandi
          </motion.p>
        )}
      </AnimatePresence>
    </div>
  );
}

Konsolda:

text
xabar ketdi

Ekranda:

text
Saqlash

Shart AnimatePresence ning ichiga ko'chdi. Endi u doim chizilgan: saved false bo'lganda bolasini 200 ms ushlab turadi. ekran("Saqlandi") bosishdan keyin xabar chiqqanini tekshiradi. role="status" — Malika uchun: xabar paydo bo'lganda ekran o'quvchi uni o'qiydi (jonli hudud).

3-mashq (qiyin): o'chirishdan oldin chiqish — Motion bilan

Kanon yechimini Motion'ning useAnimate hooki bilan takrorlang. Bu hook hali o'tilmagan — to'liq Motion IV da. Mashq uchun shu yetadi: useAnimate() ikki narsa qaytaradi. scope — ro'yxatga beriladigan ref. animate(element, kadrlar, sozlama) — elementni animatsiya qiladi va va'da (Promise) qaytaradi, uni await qilish mumkin. Talablar:

  1. Ro'yxat: Osh, Lag'mon, Manti. Har qatorda «O'chirish» tugmasi (aria-label="Osh: o'chirish" kabi).
  2. Bosilganda: avval qator opacity: 0 va x: 16 ga 150 ms da animatsiya qilinadi, konsolga chiqdi: Osh. Keyin state'dan o'chiriladi.
  3. Animatsiya 300 ms da tugamasa ham o'chirish bajarilsin (Promise.race).
  4. AnimatePresence ishlatilmasin.

Ishora: const [scope, animate] = useAnimate(), animate(element, kadrlar, sozlama) va'da qaytaradi. Element — e.currentTarget.closest("li") (DOM bo'ylab harakatlanish).

Yechim
jsx
import { useState } from "react";
import { useAnimate } from "motion/react";

const wait = (ms) => new Promise((r) => setTimeout(r, ms));

export default function App() {
  const [items, setItems] = useState(["Osh", "Lag'mon", "Manti"]);
  const [scope, animate] = useAnimate();

  async function remove(e, name) {
    const li = e.currentTarget.closest("li");
    const exit = animate(
      li,
      { opacity: 0, x: 16 },
      { duration: 0.15, ease: "easeIn" },
    );
    await Promise.race([exit, wait(300)]);
    console.log("chiqdi:", name);
    setItems((list) => list.filter((x) => x !== name));
  }

  return (
    <ul ref={scope}>
      {items.map((name) => (
        <li key={name}>
          {name}{" "}
          <button
            aria-label={`${name}: o'chirish`}
            onClick={(e) => remove(e, name)}
          >
            ✕
          </button>
        </li>
      ))}
    </ul>
  );
}

Konsolda:

text
chiqdi: Osh

Ekranda:

text
Lag'mon ✕
Manti ✕

ekran("Osh") — bosishdan keyin qator hali bor: animatsiya ketyapti, state o'zgarmagan. 150 ms dan keyin chiqdi: Osh va state'dan o'chirish. Ro'yxat holati va DOM hech qachon bir-biriga zid emas. e.currentTarget ni await dan oldin o'qidik: React hodisasi tugagach currentTarget null bo'ladi. Kanon nega useAnimate emas, WAAPI tanladi? Hajm: useAnimate Motion'ning animatsiya yadrosini birinchi yuklashga olib kiradi — keyingi darsda o'lchaymiz.

4-mashq: Vazifalar qadami

feat/motion branch'ida davom eting: src/ui/qator-harakati.ts ga chiqishAnimatsiyasi ni qo'shing, App.tsx da ochir ni ikkiga bo'ling — chiqishAnimatsiyasi → ochirishniBajar. Commit qilmang: holat 25 commit'i Motion IV darsida.

Yechim

Tekshiruv ro'yxati:

  • Vazifani o'chiring — o'ngga siljib so'nadi, keyin yo'qoladi; fokus qo'shni «O'chirish» da, e'lon «…» o'chirildi.
  • Filtrni almashtiring va qidiring — qatorlar darhol yo'qoladi (chiqish animatsiyasi yo'q).
  • Ikkinchi tab ochiq, birinchisi fonda: ikkinchisida o'chiring — fondagi tabda qator 300 ms dan kechiktirmay yo'qoladi (Promise.race).
  • DevTools → Rendering → prefers-reduced-motion: reduce — o'chirilgan qator animatsiyasiz, darhol yo'qoladi.
  • npm test — Vitest jsdom'da animate yo'q, funksiya darhol qaytadi: mavjud testlar o'zgarishsiz o'tadi.

10. Real ishda

  • Modal, toast, menyu — AnimatePresence ning eng ko'p uchraydigan joylari: bitta element, chiqishi kutiladi. Radix va shadcn komponentlari esa chiqishni CSS bilan qiladi (data-[state=closed], CSS animatsiyalari React'da) — g'oya o'sha: yopilish animatsiyasi tugaguncha element DOM'da qoladi.
  • Mahsulot galereyasi, onlayn do'konlar — layoutId bilan kartadan to'liq sahifaga "uchuvchi" rasm, tab ostidagi chiziq, savatchaga uchadigan mahsulot.
  • Admin panellar — jadval va ro'yxatlarda chiqish animatsiyasini ehtiyot bilan: testlar (Playwright, Cypress) DOM'ni o'qiydi. Ko'p jamoalar test muhitida animatsiyani butunlay o'chiradi (MotionGlobalConfig.skipAnimations) — lekin bu haqiqiy foydalanuvchi ko'radigan nomuvofiqlikni yashiradi.
  • Intervyu savollari: "React'da chiqish animatsiyasi nega qiyin?", "AnimatePresence qanday ishlaydi va nega key kerak?", "FLIP nima va nega width emas, transform?", "chiqayotgan element ekran o'quvchi va testlarga qanday ta'sir qiladi?".

Xulosa

  • React o'chirilgan elementni darhol olib tashlaydi; exit faqat AnimatePresence ichida ishlaydi — u doim chizilgan, bolada key.
  • mode: sync — birga, wait — navbat bilan (bitta bola), popLayout — chiquvchi oqimdan chiqadi, lekin <style> qo'shadi va qattiq CSP'da ishlamaydi.
  • usePresence — chiqishni o'zingiz boshqarasiz; safeToRemove ni albatta chaqiring.
  • layout — FLIP: layout bir marta, animatsiya transform bilan; layoutId — element "uchadi"; LayoutGroup — qo'shnilar va nomlar fazosi.
  • Chiqayotgan element — state'da yo'q, DOM'da bor: ro'yxatda u filtr, qidiruv, testlar va fon tabni buzdi. Kanon: chiqish faqat o'chirishda, state'dan oldin, WAAPI va 300 ms chegarasi bilan.

Keyingi dars: Motion IV: scroll animatsiya, motion values va optimallashtirish. U yerda React'ni qayta chizmaydigan motion values, scroll effektlari va LazyMotion bilan hajmni kamaytirishni o'rganamiz.

Manbalar

  • Motion: AnimatePresence, Layout animations, LayoutGroup, usePresence — motion.dev/docs/react-animate-presence, motion.dev/docs/react-layout-animations (motion 14.0.0, 2026-10)
  • Motion manba kodi: framer-motion/dist/es/components/AnimatePresence/PopChild.mjs (14.0.0) — popLayout uchun <style>
  • MDN: Element.animate(), Animation.finished — developer.mozilla.org/docs/Web/API/Element/animate
  • Paul Lewis: FLIP Your Animations — aerotwist.com/blog/flip-your-animations
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Motion'da chiqish va layout animatsiyalari: AnimatePresence, exit, layout va layoutId — IlmHamroh