خالص نگهداشتن کامپوننت ها

برخی از توابع جاوا اسکریپت خالص هستند. توابع خالص فقط یک محاسبه انجام میدهند و نه بیشتر. با نوشتن کامپوننتهای خالص، میتوانید از بروز انواعی از باگهای گیجکننده و رفتارهای غیرقابل پیشبینی با رشد کدبیس جلوگیری کنید. با این حال برای دستیابی به این مزایا، باید چند قاعده را رعایت کنید.
خلوص: کامپوننتها به عنوان فرمولها
در علوم کامپیوتر (و به ویژه در دنیای برنامهنویسی تابعی)، یک تابع خالص تابعی است که دارای ویژگیهای زیر است:
- به کار خود مشغول است. هیچ شی یا متغیری که قبل از فراخوانی آن وجود داشته است را تغییر نمیدهد.
- ورودیهای یکسان، خروجی یکسان. با توجه به ورودیهای یکسان، یک تابع خالص باید همیشه همان نتیجه را برگرداند.
شاید شما قبلاً با یک مثال از توابع خالص آشنا باشید: فرمولها در ریاضی.
این فرمول ریاضی را در نظر بگیرید: y = 2x
.
اگر x = 2
، پس همیشه y = 4
می شود.
اگر x = 3، سپس y = 6. همیشه.
اگر x = 3
باشد، y
هیچ وقت نمیتواند بر اساس زمان روز یا وضعیت بازار بورس معادل 9
، -1
یا 2.5
شود. y
همشیه برابر با 6
خواهد بود.
اگر این تابع ریاضی را به یک تابع جاوا اسکریپتی تبدیل کنیم، به این شکل خواهد بود:
function double(number) {
return 2 * number;
}
در مثال بالا، double
یک تابع خالص است. اگر به آن 3
بدهید، همیشه 6
را برمیگرداند.
React بر اساس این مفهوم طراحی شده است. React فرض میکند که هر کامپوننتی که مینویسید یک تابع خالص است. این به این معناست که کامپوننتهای React ای که مینویسید باید همیشه همان 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 است که کامپوننت برای رندرکردن به React ارائه میدهد.

تاثیراتجانبی: پیامدهای (غیر) عمدی
فرآیند رندرکردن React باید همیشه خالص باشد. کامپوننتها باید فقط خروجی 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 متفاوتی خواهند داشت! این رفتار برنامه vh غیرقابل پیشبینی می کند.
اگر به فرمول y = 2x
برگردیم، حتی اگر x = 2
باشد، دیگر نمیتوانیم اعتماد کنیم که خروجی y = 2
می شود. تستهای ما ممکن است شکست بخورند، کاربران ما گیج خواهند شد، هواپیماها از آسمان سقوط خواهند کرد—شما میتوانید ببینید که این میتواند به وجود آمدن خطاهای گیجکننده منجر شود!
این کامپوننت را می توانید با انتقال اصلاح کنید:
مشاهده کد و نتیجه در CodeSandbox
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} />
</>
);
}
حالا کامپوننت شما خالص است، زیرا JSX ای که برمیگرداند فقط به ورودی guest
بستگی دارد.
به طور کلی، شما نباید انتظار داشته باشید که کامپوننتهای شما به ترتیب خاصی رندر شوند. مهم نیست که y = 2x
را قبل یا بعد از y = 5x
فراخوانی کنید: هر دو فرمول بهطور مستقل از یکدیگر حل خواهند شد. به همین ترتیب، هر کامپوننت باید فقط "برای خودش فکر کند" و در حین رندر تلاش نکند با دیگر کامپوننت ها هماهنگ شود یا به دیگران وابسته باشد. رندر مانند یک امتحان مدرسه است: هر کامپوننت باید JSX خود را به تنهایی محاسبه کند!
شناسایی محاسبات غیر خالص با حالت Strict
در React سه نوع ورودی وجود دارد که میتوانید در حین رندر آنها را بخوانید و البته ممکن است شما هنوز از همه آنها استفاده نکرده باشید: props، state، و context. شما باید همیشه این ورودیها را به عنوان فقط خواندنی در نظر بگیرید.
زمانی که میخواهید چیزی را در پاسخ به ورودی کاربر تغییر دهید، باید به جای نوشتن متغیر، از state استفاده کنید. شما هرگز نباید در حین رندر کامپوننت خود، متغیرها یا اشیاء موجود را تغییر دهید.
React همچنین "حالت Strict" را ارائه میدهد که در آن هر تابع کامپوننت را دو بار در حین توسعه فراخوانی میکند. با دو بار فراخوانی توابع کامپوننت، حالت Strict به پیدا کردن کامپوننتهایی که این قوانین را نقض میکنند کمک میکند.
توجه کنید که مثال اصلی چگونه به جای به جای "Guest #1"، "Guest #2" و "Guest #3"مقادیر "Guest #2"، "Guest #4" و "Guest #6" را نمایش داد. تابع اصلی غیر خالص بود، بنابراین فراخوانی دوباره آن کار را خراب کرد. اما نسخه خالص اصلاح شده حتی اگر تابع دو بار هم فراخوانی شود، درست کار میکند. توابع خالص تنها محاسبه میکنند، بنابراین فراخوانی دوباره آنها هیچ تغییری در خروجی ایجاد نمیکند—همانطور که دو بار فراخوانی double(2)
هیچ تغییری در آنچه برمیگردد ایجاد نمیکند. ورودیهای یکسان، خروجیهای یکسان را همیشه بهمراه دارد.
حالت Strict در محیط عملیاتی هیچ تأثیری ندارد، بنابراین باعث کاهش سرعت اپلیکیشن برای کاربران نمیشود. برای فعال کردن حالت Strict، میتوانید کامپوننت ریشه خود را در تگ <React.StrictMode>
قرار دهید. برخی از فریمورکها به طور پیشفرض این کار را انجام میدهند.
تغییر محلی: راز کوچک کامپوننت شما
در مثال بالا، مشکل این بود که کامپوننت یک متغیر موجود را در حین رندر تغییر میداد. این معمولاً به عنوان یک "تغییر" نامیده میشود تا کمی ترسناکتر به نظر برسد. توابع خالص متغیرها یا اشیایی که قبل از فراخوانی ایجاد شدهاند را تغییر نمیدهند—این آنها را غیر خالص میکند!
اما تغییر متغیرها و اشیائی که شما در این مثال، شما یک آرایه []
ایجاد میکنید، آن را به یک متغیر cups
اختصاص میدهید، و سپس دوازده فنجان به آن اضافه میکنید:
مشاهده کد و نتیجه آن در CodeSandbox
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
هرگز از اتفاقی که افتاده است خبردار نخواهد شد. به این موضوع "تغییر محلی" گفته میشود—این مانند راز کوچک کامپوننت شما است.
جایی که میتوانید تاثیرات جانبی ایجاد کنید
در حالی که برنامهنویسی تابعی به شدت بر روی خلوص تکیه میکند، در نهایت، در جایی، چیزی باید تغییر کند. این به نوعی هدف برنامهنویسی است! این تغییرات—به روز رسانی صفحه، شروع یک انیمیشن، تغییر دادهها—به عنوان تاثیرات جانبی شناخته میشوند. اینها چیزهایی هستند که در کنار رندر، و نه در حین آن، اتفاق میافتند.
در React، تاثیرات جانبی معمولاً باید در هندلر رویداد تابعی است که React هنگام انجام یک عمل خاص توسط کاربر، آنها را اجرا میکند—به عنوان مثال، زمان کلیک روی یک دکمه. حتی اگر هندلر رویداد داخل کامپوننت تعریف شده باشد، در حین رندر اجرا نمیشود! بنابراین هندلر رویداد نیازی به خالص بودن ندارد.
اگر شما تمام گزینههای دیگر را امتحان کردید و نتوانستید هندلر رویداد صحیح برای تاثیر جانبی مورد نظر پیدا کنید، میتوانید آن را با فراخوانی useEffect
در کامپوننت به JSX بازگشتی متصل کنید. این کار به React میگوید که آن را بعد از رندر، زمانی که تاثیرات جانبی مجاز هستند، اجرا کند. با این حال، این رویکرد باید آخرین راه حل شما باشد.
تا حد امکان سعی کنید منطق خود را فقط با رندر کردن بیان کنید. با دانستن اینکه چقدر این موضوع میتواند برای شما مفید باشد، شگفت زده خواهید شد!
چرا React به خلوص اهمیت میدهد؟
نوشتن توابع خالص به کمی عادت و نظم نیاز دارد. اما همچنین فرصتهای فوقالعادهای را پیش روی شما قرار می دهد:
- کامپوننتهای شما میتوانند در محیطهای متفاوت اجرا شوند—برای مثال، در سرور! از آنجا که آنها برای ورودیهای یکسان همان نتیجه را برمیگردانند، کامپوننت میتواند بسیاری از درخواستهای کاربر را پاسخ دهد.
- شما میتوانید با نادیده گرفتن رندر کردن برای کامپوننتهایی که ورودیهایشان تغییر نکرده بهبود عملکرد داشته باشید. این کار به دلیل بازگرداندن مقدار ثابت برای خروجی مشخص توسط توابع خالص، ایمن است، بنابراین آنها را میتوان کش کرد.
- اگر برخی دادهها در وسط رندر درختی عمیق از کامپوننتها تغییر کنند، React میتواند رندر را از نو آغاز کند بدون اینکه زمان را برای تکمیل رندر غیرقابل استفاده هدر دهد. خلوص اجازه میدهد که در هر زمان محاسبات را متوقف کنید.
هر ویژگی جدید React که میسازیم از خلوص بهره میبرد. از دریافت دادهها تا انیمیشنها و عملکرد، حفظ خلوص کامپوننتها قدرت پارادایم React را شکوفا می کند.
جمع بندی
- یک کامپوننت باید خالص باشد، به این معناست که:
- به کار خود مشغول است. نباید هیچ شی یا متغیری که قبل از رندر وجود داشته را تغییر دهد.
- ورودیهای یکسان، خروجی یکسان. با توجه به ورودیهای یکسان، یک کامپوننت باید همیشه یک JSX ثابت را برگرداند.
- رندر میتواند در هر زمانی اتفاق بیفتد، بنابراین کامپوننتها نباید وابسته به ترتیب رندر یکدیگر باشند.
- شما نباید هیچ کدام از ورودیهایی که کامپوننتهای شما برای رندر کردن استفاده میکنند را تغییر دهید. این شامل props، state و context میشود. برای بهروزرسانی صفحه، "از state" به جای تغییر اشیای موجود استفاده کنید.
- سعی کنید منطق کامپوننت خود را در JSX ای که بازمیگردانید بیان کنید. زمانی که نیاز به "تغییر مقادیر" دارید، معمولاً باید در هندلر رویداد این تغییرات را انجام دهید. به عنوان آخرین راهحل، میتوانید از هوک
useEffect
استفاده کنید. - نوشتن توابع خالص کمی تمرین نیاز دارد، اما قدرت پارادایم React را شکوفا می کند.
چالش ها
اصلاح ساعت خراب
این کامپوننت سعی میکند در زمانی بین نیمه شب تا شش صبح، ویژگی کلاس <h1>
را به "night"
و در سایر زمانها به "day"
تغییر دهد. با این حال، کد زیر این کار نمیکند. میتوانید این کامپوننت را تعمیر کنید؟
شما میتوانید با تغییر موقتی منطقه زمانی کامپیوتر، درستی راه حل خود را بررسی کنید. وقتی زمان کنونی بین نیمه شب و شش صبح است، ساعت باید رنگهای معکوس شدهای داشته باشد!
رندر کردن یک نوع محاسبه است، نباید سعی در "انجام کاری" کند. آیا میتوانید همان ایده را به شیوهای متفاوت بیان کنید؟
اصلاح پروفایل خراب
دو کامپوننت Profile
در کنار هم با دادههای مختلف رندر میشوند. بر روی پروفایل اول "Collapse" را بزنید و سپس آن را "Expand" کنید. متوجه میشوید که اکنون هر دو پروفایل یک شخص را نمایش میدهند. این یک باگ است.
علت خطا را پیدا کرده و آن را اصلاح کنید.
راهنمایی: کد مشکلدار در Profile.js
قرار دارد. مطمئن شوید تمام آن را از ابتدا تا انتها بخوانید!
اصلاح سینی داستان خراب
مدیر عامل شرکت از شما درخواست کرده است که "داستانها" را به اپلیکیشن ساعت آنلاین اضافه کنید و شما نمیتوانید نه بگویید. شما کامپوننت StoryTray
ای نوشتهاید که لیستی از stories
را میپذیرد و بعد از آن جایگاه "ایجاد داستان" قرار می گیرد.
جایگاه "ایجاد داستان" را با افزودن یک داستان خالی دیگر در انتهای آرایه stories
که به عنوان یک prop دریافت کردهاید، پیادهسازی کردهاید. اما به هر دلیلی، "ایجاد داستان" چندین بار نمایش داده میشود. این مشکل را اصلاح کنید.
جواب چالش ها
چالش 1
شما میتوانید این کامپوننت را با محاسبه className
و گنجاندن آن در خروجی رندر تعمیر کنید:
مشاهده راه حل چالش در CodeSandbox
export default function Clock({ time }) {
let hours = time.getHours();
let className;
if (hours >= 0 && hours <= 6) {
className = 'night';
} else {
className = 'day';
}
return (
<h1 className={className}>
{time.toLocaleTimeString()}
</h1>
);
}
در این مثال، تاثیر جانبی (تغییر DOM) اصلاً ضروری نبود. شما فقط باید JSX را برمیگرداندید.
چالش 2
مشکل این است که کامپوننت Profile
مقدار متغیر موجود به نام currentPerson
را درون خود تغییر می دهد و کامپوننتهای Header
و Avatar
از آن استفاده می کنند. این باعث میشود که هر سه آنها غیر خالص و پیشبینیناپذیر شوند.
برای رفع این خطا، متغیر currentPerson
را حذف کنید. به جای آن، تمام اطلاعات را از Profile
به Header
و Avatar
از طریق props منتقل کنید. شما باید یک prop به نام person
به هر دو کامپوننت اضافه کنید و آن را تا پایین منتقل کنید.
مشاهده راه حل چالش در CodeSandbox
import Panel from './Panel.js';
import { getImageUrl } from './utils.js';
export default function Profile({ person }) {
return (
<Panel>
<Header person={person} />
<Avatar person={person} />
</Panel>
)
}
function Header({ person }) {
return <h1>{person.name}</h1>;
}
function Avatar({ person }) {
return (
<img
className="avatar"
src={getImageUrl(person)}
alt={person.name}
width={50}
height={50}
/>
);
}
به یاد داشته باشید که React تضمین نمیکند که کامپوننت ها به ترتیبی خاص اجرا شوند، بنابراین شما نمیتوانید از طریق تنظیم متغیرها بین آنها ارتباط برقرار کنید. تمام ارتباطات باید از طریق props انجام شود.
چالش 3
توجه کنید، هر بار که ساعت بهروزرسانی میشود، "ایجاد داستان"ی جدید دو باره به لیست اضافه میشود. این نشانه ای از وجود تغییر در زمان رندر است—حالت Strict کامپوننتها را دو بار فراخوانی میکند تا این مشکلات نمایان شوند.
تابع StoryTray
خالص نیست. با فراخوانی push
بر روی آرایه stories
که به عنوان prop دریافت شده است، یک شی را که قبل از شروع رندر StoryTray
ایجاد شده است را تغییر میدهد. این باعث میشود که این تابع باگ داشته باشد و پیشبینی آن بسیار دشوار باشد.
سادهترین راهحل این است که اصلاً به آرایه دست نزنید و "ایجاد داستان" را جداگانه رندر کنید: مشاهده جواب در CodeSandbox
export default function StoryTray({ stories }) {
return (
<ul>
{stories.map(story => (
<li key={story.id}>
{story.label}
</li>
))}
<li>Create Story</li>
</ul>
);
}
بهعنوان راه حل دیگر، میتوانید یک آرایه جدید (با کپی کردن آرایه موجود) ایجاد کنید و بعد از آن آیتم جدید را به آرایه اضافه کنید: مشاهده کد راه حل در CodeSandbox
export default function StoryTray({ stories }) {
// Copy the array!
let storiesToDisplay = stories.slice();
// Does not affect the original array:
storiesToDisplay.push({
id: 'create',
label: 'Create Story'
});
return (
<ul>
{storiesToDisplay.map(story => (
<li key={story.id}>
{story.label}
</li>
))}
</ul>
);
}
این باعث محلی شدن تغییر ایجاد شده می شود و تابع رندر شما را خالص میکند. اما هنوز هم باید مراقب باشید: بهعنوان مثال، اگر بخواهید هر یک از موارد موجود در آرایه را تغییر دهید، باید آن موارد را نیز کپی کنید.
به خاطر سپردن متدهایی از آرایه که باعث ایجاد تغییرات روی خود آرایه می شود، مفید است. به عنوان مثال، push
، pop
، reverse
و sort
مقدار آرایه را تغییر می دهند، اما slice
، filter
، و map
یک آرایه جدید ایجاد میکنند.