Résumé (PDF)

اولین تجربه لایو کدینگم: وقتی مغزم سیب‌زمینی شد 🥔

اولین باری که هر کاری رو انجام می‌دیم همیشه تو ذهنمون می‌مونه. برای همین تصمیم گرفتم یه چیزی راجع به اولین تجربه لایو کدینگم بنویسم و تجربیاتم رو به اشتراک بذارم.

با یه فایل App.tsx خیلی سبک شروع کردیم—کلاً ۴۳ خط کد بود. دستورالعمل هم خیلی سرراست بود:

«بهینه‌سازیش کن.»

کدی که به من دادن این شکلی بود:

App.tsx
import { type FC, type PropsWithChildren, useEffect, useState } from "react";
export default function App() {
const [tick, setTick] = useState(0);
const [item, setItem] = useState(0);
useEffect(() => {
setInterval(() => setTick((t) => t + 1), 500);
}, []);
6 collapsed lines
const items = [
{ text: "Button 1" },
{ text: "Button 2" },
{ text: "Button 3" },
{ text: "Button 4" },
];
return (
<>
<div>tick: {tick}</div>
<div>item: {item}</div>
{items.map((item, i) => (
<Button key="{i}">{item.text}</Button>
))}
</>
);
}
6 collapsed lines
const Button: FC<PropsWithChildren<{ onClick?: void }>> = ({
onClick,
children,
}) => {
return <button onClick={onClick}>{children}</button>;
};

خوش‌خیالی‌های یک ذهن ساده

اولین چیزی که به چشمم خورد، نبودن تابع cleanup بود.

دیدم که setInterval برای وقتی که کامپوننت unmount میشه هیچ cleanupی نداره. تازه تو حالت توسعه با فعال بودن React Strict Mode، این دو بار اجرا می‌شه. برای همین، سریع دست به کار شدم و cleanup رو اضافه کردم:

App.tsx
useEffect(() => {
const id = setInterval(() => setTick((t) => t + 1), 500);
return () => clearInterval(id);
}, []);

تو دلم به خودم می‌گفتم:

«خب، حله. کجا رو برای استخدام امضا کنم؟ 😎»

حالا درسته، احتمالاً تاثیر مثبتی روی مصاحبه‌کننده داشت، ولی الان که به اون ذهنیت ساده‌ام نگاه می‌کنم یه خورده خنده‌ام می‌گیره.

بعد از اون، از اونجایی که دوست دارم چیزا رو ساده نگه دارم، متوجه شدم که کلاً می‌تونم با استخراج کردن یه کامپوننت کل مشکل رو حل کنم. یکی از استیت‌ها، یعنی tick، داشت هر ۵۰۰ میلی‌ثانیه آپدیت می‌شد. فهمیدم که بقیه بدنه تابع و JSX اصلاً نیازی به tick ندارن و دارن الکی و بدون دلیل رندر می‌شن.

کاملاً متقاعد شده بودم که باید اون عنصری که tick رو رندر می‌کنه، به همراه useState و useEffect خودش بکشم بیرون و تو یه کامپوننت جدا و ایزوله بذارم (به این میگن State Colocation). اینطوری می‌تونست هر چقدر که دلش می‌خواد رندر بشه، بدون اینکه بقیه اپلیکیشن رو با خودش درگیر کنه.

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

و دقیقاً همونجا بود که مغزم کاملاً تبدیل به یه سیب‌زمینی شد.

راهنمایی اول: React.memo و قفل شدن مغز (🥔)

اولین راهنمایی که بهم کردن این بود که از React.memo استفاده کنم. منم سریع کامپوننت Button رو باهاش رپ کردم و بوم—مشکل رندر اضافی حل شد! اما مصاحبه‌کننده گفت:

«مصاحبه هنوز تموم نشده. می‌خوام setItem رو وصل کنی تا وقتی روی یک دکمه کلیک می‌شه، استیت item ما بشه ایندکس همون دکمه خاص.»

از اونجایی که تو ذهنم هنوز درگیر همون ایده قبلی استخراج کامپوننت و State Colocation بودم، داشتم همینجوری تئوری‌های الکی می‌پروندم. وقتی نوبت به نوشتن کد رسید، ذهنم کاملاً قفل کرده بود.

رفتم پایین سراغ کامپوننت Button، یه پراپ onClick اضافه کردم و یه inline arrow function بهش پاس دادم که setItem رو با ایندکس دکمه صدا می‌زد. یه console.log هم انداختیم تو کامپوننت Button تا ببینیم چه اتفاقی میفته.

App.tsx
{items.map((item, i) => (
<Button key={i} onClick={() => setItem(i)}>{item.text}</Button>
))}
</>
);
}
const _Button: FC<PropsWithChildren<{ onClick?: () => void }>> = ({
onClick,
children,
}) => {
console.log("RENDERING BUTTON");
return <button onClick={onClick}>{children}</button>;
};
const Button = React.memo(_Button);

و سورپرایز! به خاطر اینکه اون arrow function هر بار که کامپوننت App رندر می‌شه یه رفرنس تابع کاملاً جدید می‌سازه، React.memo به اون رفرنس جدید نگاه می‌کرد، فرض می‌کرد پراپ عوض شده و در نتیجه همه دکمه‌ها رو دوباره رندر می‌کرد!

راهنمایی دوم: caching و فاز over-engineering

تو این نقطه، مغزم رسماً از کار افتاده بود و نمی‌تونستم یه راه‌حل درست و حسابی پیدا کنم. مصاحبه‌کننده یه راهنمایی در مورد caching کرد. منم پریدم وسط و یه useCallback تعریف کردم که در نهایت ما رو به این هیولا رسوند:

App.tsx
// ...
const btnCallbacks = new Map();
export default function App() {
// ...
const changeItem = useCallback((index: number) => {
const cb = btnCallbacks.get(index);
if (cb) {
return cb;
}
const newCb = () => setItem(index);
btnCallbacks.set(index, newCb);
return newCb;
}, []);
return (
<>
{/* ... */}
{items.map((item, i) => (
<Button key={i} onClick={() => changeItem(i)}>{item.text}</Button>
))}
</>
);
}
// ...

این دیگه تهِ over-engineering بود. همونطور که می‌بینید، من حتی یه Map توی اسکوپ ماژول تعریف کردم تا کال‌بک‌ها رو توش ذخیره کنم! مصاحبه‌کننده هم با تعجب (و البته کاملاً منطقی) پرسید:

«چرا Map؟ ما که قرار نیست آیتمی رو پاک کنیم، پس چرا از یه آبجکت ساده استفاده نکردی؟»

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

راه‌حل نهایی: تفکر خارج از چارچوب

در نهایت، مصاحبه‌کننده یه راهنمایی آخر بهم کرد:

«می‌تونی یه لحظه خارج از چارچوب فکر کنی؟ واقعاً هیچ راه دیگه‌ای برای حل این مسئله نیست؟»

راستش رو بخواید، حتی با اون راهنمایی هم راه‌حل رو نمی‌دیدم. تو اون حالت استرس و پنیک، ذهنم کاملاً خالی شده بود و همه چیز رو یادم رفته بود. خارج از چارچوب که هیچ، اصلاً فکر نمی‌کردم.

وقتی گیر کردن منو دید، بیشتر راهنمایی کرد:

«می‌دونی، می‌تونی تابع setItem رو که از useState برمی‌گرده مستقیماً پاس بدی…»

و همونجا بود که یهو دوزاریم افتاد! تو این سال‌هایی که با ری‌اکت کار کردم، احتمالاً یه میلیون بار این کار رو تو پروژه‌های واقعی انجام داده بودم. فقط هیچ‌وقت آگاهانه به این فکر نکرده بودم که چطور setItem یه stable reference داره و چطور پاس دادن مستقیمش باعث بهینه‌سازی رندر می‌شه.

داشتم می‌رفتم که کد رو بنویسم که نگهم داشت و گفت مصاحبه تموم شده.

با اینکه تو خود تماس نتونستم نوشتن کد رو تموم کنم، ولی اینجا اون کد بی‌نهایت ساده و سرراستی که در نهایت به دست می‌اومد رو براتون می‌ذارم. به جای این همه لقمه رو دور سر چرخوندن، فقط کافی بود setItem و index رو مستقیم به کامپوننت Button پاس بدیم و اجراش رو توی همون Button مدیریت کنیم:

App.tsx
{items.map((item, i) => (
<Button key={i} index={i} setItem={setItem}>{item.text}</Button>
))}
</>
);
}
const _Button: FC<PropsWithChildren<{ index: number; setItem: (idx: number) => void }>> = ({
index,
setItem,
children,
}) => {
console.log("RENDERING BUTTON");
return <button onClick={() => setItem(index)}>{children}</button>;
};
const Button = React.memo(_Button);

چون index یه مقدار primitive است و setItem هم که مستقیماً از طریق useState یه رفرنس ثابت بهمون میده، React.memo خیلی راحت می‌فهمه که هیچ‌چیزی تغییر نکرده.

نتیجه‌گیری…

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

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

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