فارسی

بر فرآیند تطبیق React مسلط شوید. بیاموزید که چگونه استفاده صحیح از پراپ 'key'، رندر لیست‌ها را بهینه کرده، از باگ‌ها جلوگیری و عملکرد برنامه را افزایش می‌دهد.

گشایش قفل عملکرد: بررسی عمیق کلیدهای تطبیق React برای بهینه‌سازی لیست‌ها

در دنیای توسعه وب مدرن، ایجاد رابط‌های کاربری پویا که به سرعت به تغییرات داده‌ها پاسخ می‌دهند، امری حیاتی است. React، با معماری مبتنی بر کامپوننت و ماهیت اعلانی خود، به یک استاندارد جهانی برای ساخت این رابط‌ها تبدیل شده است. در قلب کارایی React، فرآیندی به نام تطبیق (reconciliation) قرار دارد که شامل DOM مجازی می‌شود. با این حال، حتی قدرتمندترین ابزارها نیز ممکن است به صورت ناکارآمد استفاده شوند و یک حوزه مشترک که توسعه‌دهندگان، چه تازه‌کار و چه باتجربه، در آن دچار مشکل می‌شوند، رندر کردن لیست‌ها است.

احتمالاً شما بارها کدی شبیه به data.map(item => <div>{item.name}</div>) نوشته‌اید. این کد ساده و تقریباً پیش پا افتاده به نظر می‌رسد. با این حال، در پس این سادگی، یک ملاحظه عملکردی حیاتی نهفته است که اگر نادیده گرفته شود، می‌تواند به برنامه‌های کند و باگ‌های گیج‌کننده منجر شود. راه‌حل؟ یک پراپ کوچک اما قدرتمند: key.

این راهنمای جامع شما را به یک بررسی عمیق از فرآیند تطبیق React و نقش ضروری کلیدها در رندر لیست‌ها می‌برد. ما نه تنها «چه چیزی» بلکه «چرا» را بررسی خواهیم کرد—چرا کلیدها ضروری هستند، چگونه آن‌ها را به درستی انتخاب کنیم، و عواقب قابل توجه انتخاب اشتباه آن‌ها چیست. در پایان، شما دانش لازم برای نوشتن برنامه‌های React با عملکرد بهتر، پایدارتر و حرفه‌ای‌تر را کسب خواهید کرد.

فصل ۱: درک تطبیق React و DOM مجازی

قبل از اینکه بتوانیم اهمیت کلیدها را درک کنیم، ابتدا باید سازوکار بنیادی که React را سریع می‌کند را بفهمیم: تطبیق، که توسط DOM مجازی (VDOM) قدرت گرفته است.

DOM مجازی چیست؟

تعامل مستقیم با مدل شیء سند (DOM) مرورگر از نظر محاسباتی پرهزینه است. هر بار که چیزی را در DOM تغییر می‌دهید—مانند افزودن یک گره، به‌روزرسانی متن یا تغییر یک استایل—مرورگر باید حجم قابل توجهی کار انجام دهد. ممکن است نیاز به محاسبه مجدد استایل‌ها و طرح‌بندی برای کل صفحه داشته باشد، فرآیندی که به عنوان reflow و repaint شناخته می‌شود. در یک برنامه پیچیده و داده‌محور، دستکاری‌های مکرر و مستقیم DOM می‌تواند به سرعت عملکرد را به شدت کاهش دهد.

React برای حل این مشکل یک لایه انتزاعی معرفی می‌کند: DOM مجازی. VDOM یک نمایش سبک و درون‌حافظه‌ای از DOM واقعی است. آن را به عنوان یک طرح اولیه از UI خود در نظر بگیرید. وقتی به React می‌گویید UI را به‌روزرسانی کند (برای مثال، با تغییر استیت یک کامپوننت)، React بلافاصله DOM واقعی را دستکاری نمی‌کند. در عوض، مراحل زیر را انجام می‌دهد:

  1. یک درخت VDOM جدید که نمایانگر استیت به‌روز شده است، ایجاد می‌شود.
  2. این درخت VDOM جدید با درخت VDOM قبلی مقایسه می‌شود. این فرآیند مقایسه "diffing" نامیده می‌شود.
  3. React حداقل مجموعه تغییرات لازم برای تبدیل VDOM قدیمی به VDOM جدید را پیدا می‌کند.
  4. این تغییرات حداقلی سپس با هم دسته‌بندی شده و در یک عملیات واحد و کارآمد به DOM واقعی اعمال می‌شوند.

این فرآیند، که به عنوان تطبیق شناخته می‌شود، همان چیزی است که React را اینقدر کارآمد می‌کند. به جای بازسازی کل خانه، React مانند یک پیمانکار خبره عمل می‌کند که دقیقاً مشخص می‌کند کدام آجرها نیاز به تعویض دارند و کار و اختلال را به حداقل می‌رساند.

فصل ۲: مشکل رندر کردن لیست‌ها بدون کلید

حالا، بیایید ببینیم این سیستم زیبا کجا می‌تواند با مشکل مواجه شود. یک کامپوننت ساده را در نظر بگیرید که لیستی از کاربران را رندر می‌کند:


function UserList({ users }) {
  return (
    <ul>
      {users.map(user => (
        <li>{user.name}</li>
      ))}
    </ul>
  );
}

وقتی این کامپوننت برای اولین بار رندر می‌شود، React یک درخت VDOM می‌سازد. اگر یک کاربر جدید به *انتهای* آرایه `users` اضافه کنیم، الگوریتم مقایسه React آن را به خوبی مدیریت می‌کند. لیست‌های قدیمی و جدید را مقایسه می‌کند، یک آیتم جدید در انتها می‌بیند و به سادگی یک `<li>` جدید به DOM واقعی اضافه می‌کند. کارآمد و ساده.

اما اگر یک کاربر جدید به ابتدای لیست اضافه کنیم یا ترتیب آیتم‌ها را تغییر دهیم چه اتفاقی می‌افتد؟

فرض کنید لیست اولیه ما این است:

و پس از یک به‌روزرسانی، به این شکل در می‌آید:

بدون هیچ شناسه منحصر به فردی، React دو لیست را بر اساس ترتیبشان (اندیس) مقایسه می‌کند. این چیزی است که React می‌بیند:

این فوق‌العاده ناکارآمد است. به جای اینکه فقط یک عنصر جدید برای "Charlie" در ابتدا درج کند، React دو تغییر (mutation) و یک درج انجام داد. برای یک لیست بزرگ، یا برای آیتم‌های لیستی که کامپوننت‌های پیچیده‌ای با استیت داخلی خود هستند، این کار غیرضروری منجر به کاهش قابل توجه عملکرد و مهم‌تر از آن، باگ‌های احتمالی در استیت کامپوننت می‌شود.

به همین دلیل است که اگر کد بالا را اجرا کنید، کنسول توسعه‌دهنده مرورگر شما یک هشدار نمایش می‌دهد: "Warning: Each child in a list should have a unique 'key' prop." React به صراحت به شما می‌گوید که برای انجام کارآمد وظیفه‌اش به کمک نیاز دارد.

فصل ۳: پراپ `key` به کمک می‌آید

پراپ `key` همان راهنمایی است که React به آن نیاز دارد. این یک ویژگی رشته‌ای خاص است که شما هنگام ایجاد لیست‌هایی از عناصر ارائه می‌دهید. کلیدها به هر عنصر یک هویت پایدار و منحصر به فرد در طول رندرهای مجدد می‌دهند.

بیایید کامپوننت `UserList` خود را با کلیدها بازنویسی کنیم:


function UserList({ users }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

در اینجا، ما فرض می‌کنیم که هر شیء `user` یک ویژگی `id` منحصر به فرد دارد (مثلاً از یک پایگاه داده). حال، بیایید به سناریوی خود برگردیم.

داده‌های اولیه:


[{ id: 'u1', name: 'Alice' }, { id: 'u2', name: 'Bob' }]

داده‌های به‌روز شده:


[{ id: 'u3', name: 'Charlie' }, { id: 'u1', name: 'Alice' }, { id: 'u2', name: 'Bob' }]

با وجود کلیدها، فرآیند مقایسه React بسیار هوشمندانه‌تر است:

  1. React به فرزندان `<ul>` در VDOM جدید نگاه می‌کند و کلیدهای آن‌ها را بررسی می‌کند. کلیدهای `u3`، `u1` و `u2` را می‌بیند.
  2. سپس فرزندان VDOM قبلی و کلیدهایشان را بررسی می‌کند. کلیدهای `u1` و `u2` را می‌بیند.
  3. React می‌داند که کامپوننت‌هایی با کلیدهای `u1` و `u2` از قبل وجود دارند. نیازی به تغییر آن‌ها نیست؛ فقط باید گره‌های DOM مربوط به آن‌ها را به موقعیت‌های جدیدشان منتقل کند.
  4. React می‌بیند که کلید `u3` جدید است. یک کامپوننت و گره DOM جدید برای "Charlie" ایجاد کرده و آن را در ابتدا درج می‌کند.

نتیجه یک درج DOM و مقداری جابجایی است که بسیار کارآمدتر از چندین تغییر و یک درج است که قبلاً دیدیم. کلیدها یک هویت پایدار فراهم می‌کنند و به React اجازه می‌دهند تا عناصر را در طول رندرها ردیابی کند، صرف نظر از موقعیتشان در آرایه.

فصل ۴: انتخاب کلید مناسب - قوانین طلایی

اثربخشی پراپ `key` کاملاً به انتخاب مقدار مناسب بستگی دارد. بهترین شیوه‌های مشخص و ضد الگوهای خطرناکی وجود دارد که باید از آنها آگاه باشید.

بهترین کلید: شناسه‌های منحصر به فرد و پایدار

کلید ایده‌آل مقداری است که یک آیتم را در یک لیست به طور منحصر به فرد و دائمی شناسایی می‌کند. این تقریباً همیشه یک شناسه منحصر به فرد از منبع داده شما است.

منابع عالی برای کلیدها عبارتند از:


// خوب: استفاده از یک شناسه پایدار و منحصر به فرد از داده‌ها.
<div>
  {products.map(product => (
    <ProductItem key={product.sku} product={product} />
  ))}
</div>

ضد الگو: استفاده از اندیس آرایه به عنوان کلید

یک اشتباه رایج استفاده از اندیس آرایه به عنوان کلید است:


// بد: استفاده از اندیس آرایه به عنوان کلید.
<div>
  {items.map((item, index) => (
    <ListItem key={index} item={item} />
  ))}
</div>

در حالی که این کار هشدار React را خاموش می‌کند، می‌تواند به مشکلات جدی منجر شود و به طور کلی یک ضد الگو محسوب می‌شود. استفاده از اندیس به عنوان کلید به React می‌گوید که هویت یک آیتم به موقعیت آن در لیست گره خورده است. این اساساً همان مشکلی است که در صورت عدم وجود کلید هنگام مرتب‌سازی مجدد، فیلتر کردن یا افزودن/حذف آیتم‌ها از ابتدا یا وسط لیست رخ می‌دهد.

باگ مدیریت استیت:

خطرناک‌ترین اثر جانبی استفاده از کلیدهای اندیسی زمانی ظاهر می‌شود که آیتم‌های لیست شما استیت داخلی خود را مدیریت می‌کنند. لیستی از فیلدهای ورودی را تصور کنید:


function UnstableList() {
  const [items, setItems] = React.useState([{ id: 1, text: 'First' }, { id: 2, text: 'Second' }]);

  const handleAddItemToTop = () => {
    setItems([{ id: 3, text: 'New Top' }, ...items]);
  };

  return (
    <div>
      <button onClick={handleAddItemToTop}>Add to Top</button>
      {items.map((item, index) => (
        <div key={index}>
          <label>{item.text}: </label>
          <input type="text" />
        </div>
      ))}
    </div>
  );
}

این تمرین ذهنی را امتحان کنید:

  1. لیست با "First" و "Second" رندر می‌شود.
  2. شما "Hello" را در اولین فیلد ورودی (مربوط به "First") تایپ می‌کنید.
  3. روی دکمه "Add to Top" کلیک می‌کنید.

انتظار دارید چه اتفاقی بیفتد؟ انتظار دارید یک ورودی جدید و خالی برای "New Top" ظاهر شود و ورودی مربوط به "First" (که هنوز حاوی "Hello" است) به پایین منتقل شود. اما در واقع چه اتفاقی می‌افتد؟ فیلد ورودی در موقعیت اول (اندیس ۰)، که هنوز حاوی "Hello" است، باقی می‌ماند. اما اکنون با آیتم داده جدید، یعنی "New Top"، مرتبط شده است. استیت کامپوننت ورودی (مقدار داخلی آن) به موقعیت آن (key=0) گره خورده است، نه به داده‌ای که قرار است نمایندگی کند. این یک باگ کلاسیک و گیج‌کننده است که توسط کلیدهای اندیسی ایجاد می‌شود.

اگر به سادگی `key={index}` را به `key={item.id}` تغییر دهید، مشکل حل می‌شود. React اکنون به درستی استیت کامپوننت را با شناسه پایدار داده مرتبط می‌کند.

چه زمانی استفاده از کلید اندیسی قابل قبول است؟

موقعیت‌های نادری وجود دارد که استفاده از اندیس بی‌خطر است، اما باید تمام این شرایط را برآورده کنید:

  1. لیست ثابت است: هرگز ترتیب آن تغییر نخواهد کرد، فیلتر نخواهد شد، یا آیتم‌ها از جایی به جز انتها اضافه/حذف نخواهند شد.
  2. آیتم‌های لیست هیچ شناسه پایداری ندارند.
  3. کامپوننت‌های رندر شده برای هر آیتم ساده هستند و هیچ استیت داخلی ندارند.

حتی در این صورت، اغلب بهتر است در صورت امکان یک شناسه موقت اما پایدار تولید کنید. استفاده از اندیس همیشه باید یک انتخاب آگاهانه باشد، نه یک پیش‌فرض.

بدترین متخلف: `Math.random()`

هرگز، هرگز از `Math.random()` یا هر مقدار غیرقطعی دیگری برای کلید استفاده نکنید:


// وحشتناک: این کار را نکنید!
<div>
  {items.map(item => (
    <ListItem key={Math.random()} item={item} />
  ))}
</div>

کلیدی که توسط `Math.random()` تولید می‌شود، تضمین شده است که در هر رندر متفاوت باشد. این به React می‌گوید که کل لیست کامپوننت‌های رندر قبلی از بین رفته و یک لیست کاملاً جدید از کامپوننت‌های کاملاً متفاوت ایجاد شده است. این کار React را مجبور می‌کند تا تمام کامپوننت‌های قدیمی را unmount کند (و استیت آن‌ها را از بین ببرد) و تمام کامپوننت‌های جدید را mount کند. این به طور کامل هدف تطبیق را از بین می‌برد و بدترین گزینه ممکن برای عملکرد است.

فصل ۵: مفاهیم پیشرفته و سوالات متداول

کلیدها و `React.Fragment`

گاهی اوقات شما نیاز دارید چندین عنصر را از یک بازخوانی `map` برگردانید. روش استاندارد برای این کار استفاده از `React.Fragment` است. وقتی این کار را انجام می‌دهید، `key` باید روی خود کامپوننت `Fragment` قرار گیرد.


function Glossary({ terms }) {
  return (
    <dl>
      {terms.map(term => (
        // کلید روی Fragment قرار می‌گیرد، نه روی فرزندان.
        <React.Fragment key={term.id}>
          <dt>{term.name}</dt>
          <dd>{term.definition}</dd>
        </React.Fragment>
      ))}
    </dl>
  );
}

مهم: سینتکس کوتاه `<>...</>` از کلیدها پشتیبانی نمی‌کند. اگر لیست شما به فرگمنت‌ها نیاز دارد، باید از سینتکس صریح `<React.Fragment>` استفاده کنید.

کلیدها فقط باید در میان خواهر و برادرها منحصر به فرد باشند

یک تصور غلط رایج این است که کلیدها باید در کل برنامه شما منحصر به فرد باشند. این درست نیست. یک کلید فقط باید در لیست فوری خواهر و برادرهای خود منحصر به فرد باشد.


function CourseRoster({ courses }) {
  return (
    <div>
      {courses.map(course => (
        <div key={course.id}>  {/* کلید برای دوره */} 
          <h3>{course.title}</h3>
          <ul>
            {course.students.map(student => (
              // این کلید دانشجو فقط باید در لیست دانشجویان همین دوره خاص منحصر به فرد باشد.
              <li key={student.id}>{student.name}</li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}

در مثال بالا، دو دوره مختلف می‌توانند دانشجویی با `id: 's1'` داشته باشند. این کاملاً خوب است زیرا کلیدها در عناصر والد `<ul>` متفاوتی ارزیابی می‌شوند.

استفاده از کلیدها برای بازنشانی عمدی استیت کامپوننت

در حالی که کلیدها عمدتاً برای بهینه‌سازی لیست‌ها هستند، هدف عمیق‌تری را نیز دنبال می‌کنند: آنها هویت یک کامپوننت را تعریف می‌کنند. اگر کلید یک کامپوننت تغییر کند، React سعی نمی‌کند کامپوننت موجود را به‌روز کند. در عوض، کامپوننت قدیمی (و تمام فرزندانش) را از بین می‌برد و یک کامپوننت کاملاً جدید از ابتدا ایجاد می‌کند. این کار نمونه قدیمی را unmount کرده و نمونه جدیدی را mount می‌کند، که به طور موثر استیت آن را بازنشانی می‌کند.

این می‌تواند یک روش قدرتمند و اعلانی برای بازنشانی یک کامپوننت باشد. به عنوان مثال، یک کامپوننت `UserProfile` را تصور کنید که داده‌ها را بر اساس یک `userId` واکشی می‌کند.


function App() {
  const [userId, setUserId] = React.useState('user-1');

  return (
    <div>
      <button onClick={() => setUserId('user-1')}>View User 1</button>
      <button onClick={() => setUserId('user-2')}>View User 2</button>
      
      <UserProfile key={userId} id={userId} />
    </div>
  );
}

با قرار دادن `key={userId}` روی کامپوننت `UserProfile`، ما تضمین می‌کنیم که هر زمان `userId` تغییر کند، کل کامپوننت `UserProfile` دور انداخته شده و یک کامپوننت جدید ایجاد می‌شود. این از باگ‌های احتمالی جلوگیری می‌کند که در آن استیت پروفایل کاربر قبلی (مانند داده‌های فرم یا محتوای واکشی شده) ممکن است باقی بماند. این یک روش تمیز و صریح برای مدیریت هویت و چرخه حیات کامپوننت است.

نتیجه‌گیری: نوشتن کد React بهتر

پراپ `key` بسیار بیشتر از راهی برای خاموش کردن یک هشدار در کنسول است. این یک دستورالعمل اساسی برای React است که اطلاعات حیاتی مورد نیاز برای الگوریتم تطبیق آن را برای کارکرد کارآمد و صحیح فراهم می‌کند. تسلط بر استفاده از کلیدها نشانه یک توسعه‌دهنده حرفه‌ای React است.

بیایید نکات کلیدی را خلاصه کنیم:

با درونی کردن این اصول، شما نه تنها برنامه‌های React سریع‌تر و قابل اعتمادتری خواهید نوشت، بلکه درک عمیق‌تری از مکانیک‌های اصلی این کتابخانه به دست خواهید آورد. دفعه بعد که روی یک آرایه `map` می‌کنید تا یک لیست را رندر کنید، به پراپ `key` توجهی را که شایسته آن است، بدهید. عملکرد برنامه شما—و خود آینده‌تان—از شما سپاسگزار خواهد بود.