Keeping Components Pure

تعدادی از توابع جاوااسکریپت pure هستند. Pure functionها تنها یک محاسبه را انجام می‌دهند. با کاملاً نوشتن اجزای کد خود به عنوان Pure functionها، می‌توانید از اشکال مختلف باگ های گیج‌کننده و رفتارهای غیرقابل پیش‌بینی در کدتان جلوگیری کنید.با این حال برای بهره‌مندی از این مزایا قوانینی وجود دارد که باید پیروی شود.

You will learn

  • purity چیست و چگونه به شما کمک می کند تا از باگ ها جلوگیری کنید
  • چگونه کامپوننت ها را با دور نگه داشتن از تغییرات خارج از فاز رندر pure نگه داریم
  • چگونه از حالت Strict برای یافتن اشتباهات در کامپوننت ها استفاده کنیم

Purity: کامپوننت ها به عنوان فرمول ها

در علوم کامپیوتر (و به ویژه دنیای برنامه نویسی تابعی)، یک تابعpure تابعی با ویژگی های زیر است:

  • سرش تو کار خودشه. هیچ شی یا متغیری را که قبل از فراخوانی وجود داشته است تغییر نمی دهد.
  • ورودی های یکسان، خروجی یکسان. با توجه به ورودی های یکسان، یک تابع pure همیشه باید همان نتیجه را برگرداند.

شاید قبلاً با یک مثال از توابع pure آشنا شده باشید: فرمول‌ها در ریاضی

این فرمول ریاضی را در نظر بگیرید: y = 2x.

اگر x = 2 آنگاه y = 4. همیشه.

اگر x = 3 آنگاه y = 6. همیشه.

اگر x = 3آنگاه y گاهی 9 یا –1 یا 2.5 بسته به زمان روز یا وضعیت بازار سهام نخواهد بود.

اگر y = 2x و x = 3آنگاه y همیشه 6خواهد بود.

اگر این را به یک تابع جاوا اسکریپت تبدیل کنیم، به این شکل خواهد بود:

function double(number) {
return 2 * number;
}

در مثال بالا، double یک تابع pure است. اگر به آن مقدار 3 را بدهید ، 6 برمی گرداند. همیشه.

ریکت حول این مفهوم طراحی شده است. ریکت فرض می کند که هر کامپوننتی که می نویسید یک تابع pure است. این بدان معنی است که کامپوننت های ریکتی که می نویسید باید همیشه همان JSX را با توجه به ورودی های یکسان برگردانند:

function Recipe({ drinkers }) {
  return (
    <ol>    
      <li>Boil {drinkers} cups of water.</li>
      <li>Add {drinkers} spoons of tea and {0.5 * drinkers} spoons of spice.</li>
      <li>Add {0.5 * drinkers} cups of milk to boil and sugar to taste.</li>
    </ol>
  );
}

export default function App() {
  return (
    <section>
      <h1>Spiced Chai Recipe</h1>
      <h2>For two</h2>
      <Recipe drinkers={2} />
      <h2>For a gathering</h2>
      <Recipe drinkers={4} />
    </section>
  );
}

وقتی drinkers={2} را به Recipe می‌دهید، JSX حاوی 2 cups of water را برمی‌گرداند. همیشه.

اگر drinkers={4} را بدهید، JSX حاوی 4 cups of water را برمی‌گرداند. همیشه.

درست مثل یک فرمول ریاضی.

تصور کنید کامپوننت‌هایتان مثل دستور غذا هستند: اگر دستور را دقیقا دنبال کنید و مواد جدیدی به آن اضافه نکنید، هر بار همان غذای خوشمزه را خواهید داشت. در ریکت این “غذای خوشمزه” همان JSX است که کامپوننت برای رندرrender شدن به ریکت تحویل می‌دهد.

A tea recipe for x people: take x cups of water, add x spoons of tea and 0.5x spoons of spices, and 0.5x cups of milk

Illustrated by Rachel Lee Nabors

عوارض جانبی: پیامدهای (غیر) عمدی

فرایند رندر ریکت همواره باید خالص (pure) باشد. یعنی کامپوننت‌ها فقط باید JSX خود را برگردانند و هیچ‌گونه تغییری در اشیاء یا متغیرهایی که قبل از رندر وجود داشته‌اند، ایجاد نکنند. این کار باعث ایجاد ناخالصی می‌شود!

در اینجا یک کامپوننت است که این قانون را زیر پا می گذارد:

let guest = 0;

function Cup() {
  // Bad: changing a preexisting variable!
  guest = guest + 1;
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup />
      <Cup />
      <Cup />
    </>
  );
}

این کامپوننت یک متغیر به نام guest را می‌خواند و می‌نویسد که بیرون از خود کامپوننت تعریف شده است. این بدان معناست که چند بارصدا زدن این کامپوننت ، JSX های مختلفی ایجاد خواهد کرد واگر کامپوننت‌های دیگر هم متغیر guest را بخوانند، JSX آن‌ها نیز بر اساس زمان رندر آن‌ها تغییر خواهد کرد! که قابل پیش‌بینی نیست.

به فرمول خود برمی گردیم y = 2x الان با اینکه x = 2نمی توانیم اعتماد کنیم که y = 4. تست های ما ممکن است شکست بخورند، کاربران ما گیج شوند، هواپیماها از آسمان سقوط کنند - می‌توانید ببینید که چگونه این امر منجر به باگ‌های گیج‌کننده می‌شود!

می‌توانید این کامپوننت را با پاس دادن guest به عنوان prop برطرف کنید:

function Cup({ guest }) {
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup guest={1} />
      <Cup guest={2} />
      <Cup guest={3} />
    </>
  );
}

اکنون کامپوننت شما خالص (pure) است، زیرا JSXای که برمی‌گرداند فقط به prop guest بستگی دارد.

به طور کلی، انتظار نداشته باشید که کامپوننت‌های شما به ترتیب خاصی رندر شوند. فرقی نمی‌کند که y = 2x را قبل یا بعد از y = 5xفراخوانی کنید: هر دو فرمول بدون توجه به دیگری حل می‌شوند. به همین ترتیب، هر کامپوننت فقط باید “به خود فکر کند” و در طول رندر، سعی نکند با دیگران هماهنگ شود یا به آن‌ها وابسته باشد. رندر مثل یک امتحان مدرسه است: هر کامپوننت باید JSX خودش را به تنهایی محاسبه کند.

Deep Dive

تشخیص محاسبات ناخالص با StrictMode

در ریکت، سه نوع ورودی وجود دارد که می‌توانید هنگام رندر آن‌ها را بخوانید، حتی اگر هنوز از همه آن‌ها استفاده نکرده باشید: props, state و context. همیشه باید با این ورودی‌ها به صورت read-only رفتار کنید.

وقتی می‌خواهید در پاسخ به ورودی کاربر چیزی را تغییر دهید، به جای نوشتن در یک متغیر، باید از set state استفاده کنید. هرگز نباید متغیرها یا اشیاء از پیش موجود را در حین رندر کامپوننت تغییر دهید.

ریکت در حالت “Strict Mode” در طول توسعه، تابع هر کامپوننت را دو بار فراخوانی می‌کند. این کار به شناسایی کامپوننت‌هایی که این قوانین را نقض می‌کنند کمک می‌کند.

توجه کنید که چگونه مثال اصلی به جای “مهمان #1”، “مهمان #2” و “مهمان #3”، “مهمان #2”، “مهمان #4” و “مهمان #6” را نمایش می‌داد. تابع اصلی نا خالص بود، بنابراین فراخوانی دو بار آن باعث خرابی می‌شد. اما نسخه خالص و اصلاح‌شده حتی اگر تابع هر بار دو بار فراخوانی شود، کار می‌کند. توابع خالص فقط محاسبه می‌کنند، بنابراین فراخوانی دو بار آن‌ها هیچ چیزی را تغییر نمی‌دهد—مانند فراخوانی دوبار double(2) یا حل دوبار معادله y = 2x مقدار y تغییر نمی‌کند. ورودی‌های یکسان، خروجی‌های یکسان. همیشه.

Strict Mode در حالت production هیچ تاثیری ندارد، بنابراین سرعت برنامه را برای کاربران شما کاهش نمی‌دهد. برای استفاده از Strict Mode، می‌توانید کامپوننت اصلی خود را در<React.StrictMode> بپیچید. برخی از فریمورک‌ها این کار را به صورت پیش فرض انجام می‌دهند.

Local mutation: راز کوچک کامپوننت شما

در مثال بالا، مشکل این بود که کامپوننت یک متغیر موجود را در حین رندر تغییر می‌داد. این کار اغلب برای ترسناک‌تر شدن، “mutation” (جهش) نامیده می‌شود. توابع خالص Pure functions متغیرهایی خارج از محدوده خود یا اشیایی که قبل از فراخوانی ایجاد شده‌اند را تغییر نمی‌دهند – این کار آن‌ها را ناخالص می‌کند!!

با این حال، کاملاً مجاز هستید متغیرها و اشیایی را که به تازگی در حین رندر ایجاد کرده‌اید، تغییر دهید. در این مثال، شما یک آرایه خالی [] ایجاد می‌کنید، آن را به یک متغیر cups اختصاص می‌دهید و سپس دوازده فنجان را به آن push اضافه می‌کنید:

function Cup({ guest }) {
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaGathering() {
  let cups = [];
  for (let i = 1; i <= 12; i++) {
    cups.push(<Cup key={i} guest={i} />);
  }
  return cups;
}

اگر متغیر cups یا آرایه []خارج از تابع TeaGathering ایجاد شده بودند، این یک مشکل بزرگ می‌بود! شما با اضافه کردن آیتم‌ها به آن آرایه، یک شیء موجود را تغییر می‌دادید.

با این حال، مشکلی نیست چون آنها را در همان رندر، داخلTeaGathering ایجاد کرده‌اید. هیچ کدی خارج از TeaGathering هرگز متوجه این اتفاق نمی‌شود. این را “local mutation” (جهش محلی) می‌نامند – این مانند راز کوچک کامپوننت شماست.

کجا می توانید side effects ایجاد کنید

در حالی که برنامه‌نویسی فانکشنال به شدت به خالصی (purity) تکیه می‌کند، در نهایت چیزی باید تغییر کند. این تقریباً هدف اصلی برنامه‌نویسی است! این تغییرات، مانند به‌روزرسانی صفحه، شروع انیمیشن، تغییر داده، عوارض جانبی side effects نامیده می‌شوند. آن‌ها اتفاقاتی هستند که در حاشیه رخ می‌دهند، نه در طول رندر.

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

اگر تمام گزینه‌های دیگر را بررسی کرده‌اید و نمی‌توانید event handler مناسب برای side effect خود پیدا کنید، همچنان می‌توانید آن را با فراخوانی useEffect در کامپوننت خود به JSX برگردانده‌شده متصل کنید. این به ریکت می‌گوید که آن را بعداً، پس از رندر، زمانی که side effects مجاز هستند، اجرا کند.با این حال، این رویکرد باید آخرین راه حل شما باشد.

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

Deep Dive

چرا ریکت به خالصی purity اهمیت می‌دهد؟

نوشتن توابع خالص pure functions به مقداری عادت و انضباط نیاز دارد. اما همچنین فرصت‌های فوق‌العاده‌ای را به شما می‌دهد:

  • کامپوننت‌های شما می‌توانند در محیط‌های مختلف اجرا شوند، مثلاً روی سرور! از آنجایی که آنها برای ورودی‌های یکسان نتایج یکسانی را برمی‌گردانند، یک کامپوننت می‌تواند به درخواست‌های کاربران زیادی پاسخ دهد.
  • می‌توانید با اجتناب از رندرskipping rendering کامپوننت‌هایی که ورودی‌هایش تغییر نکرده‌اند عملکرد را بهبود ببخشید. این کار امن است زیرا توابع خالص همیشه نتایج یکسانی را برمی‌گردانند، بنابراین می‌توان آن‌ها را در حافظه ذخیره کرد.
  • اگر برخی از داده‌ها در وسط رندر یک درخت کامپوننت عمیق تغییر کنند، ریکت می‌تواند رندر را بدون هدر دادن وقت برای تکمیل رندر منسوخ شده، دوباره شروع کند. خالصی باعث می‌شود توقف محاسبه در هر زمانی ایمن باشد.

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

Recap

یک کامپوننت باید خالص باشد، به این معنی که:

  • کار خودش را می‌کند. نباید هیچ شی یا متغیری را که قبل از رندر وجود داشته تغییر دهد.
  • ورودی‌های یکسان، خروجی‌های یکسان با داشتن ورودی‌های یکسان، یک کامپوننت همیشه باید همان JSX را برگرداند.
  • رندر می‌تواند در هر زمانی اتفاق بیفتد بنابراین کامپوننت‌ها نباید به ترتیب رندر یکدیگر وابسته باشند.
  • نباید هیچ یک از ورودی‌هایی که کامپوننت‌های شما برای رندر استفاده می‌کنند را تغییر دهید. این شامل props state و context می‌شود. برای به‌روزرسانی صفحه، به جای تغییر اشیاء از پیش موجود، از “set” state استفاده کنید.
  • تلاش کنید تا منطق کامپوننت خود را در JSX برگشتی‌تان بیان کنید. وقتی نیاز به “تغییر چیزها” دارید، معمولاً می‌خواهید این کار را در یک event handler انجام دهید. به‌عنوان آخرین راه حل، می‌توانید ازuseEffect استفاده کنید.
  • نوشتن توابع خالص کمی تمرین می‌خواهد، اما قدرت پارادایم ریکت را آزاد می‌کند.

Challenge 1 of 3:
ساعت خراب را تعمیر کنید

این کامپوننت سعی می‌کند کلاس CSS تگ<h1> را بین نیمه‌شب تا ساعت ۶ صبح به "night" و در تمام ساعات دیگر به "day" تنظیم کند. با این حال کار نمی‌کند. می‌توانید این کامپوننت را اصلاح کنید؟

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

export default function Clock({ time }) {
  let hours = time.getHours();
  if (hours >= 0 && hours <= 6) {
    document.getElementById('time').className = 'night';
  } else {
    document.getElementById('time').className = 'day';
  }
  return (
    <h1 id="time">
      {time.toLocaleTimeString()}
    </h1>
  );
}