پایداری ارجاعی بهعنوان یه تایپ تو ریاکت
خلاصهٔ کاملتر
هر کی رو یه کدبیس بزرگ (P)React کار کرده باشه این بحث رو میشناسه: یکی یه useMemo اضافه میکنه، یکی دیگه یه useCallback، یه هشدار exhaustive-deps ساکت میشه و همه امیدوارن propی که به کامپوننت بعدی میره ارجاعش پایدار بمونه. نویسنده — Jovi De Croock — میگه ساعتهای زیادی رو صرف پیداکردن رندرهای اضافهای کرده که تهش یه آرایهی inline بوده که به یه کامپوننت memo شده پاس داده شده. تایپ Item[] فقط از شکل داده حرف میزنه، نه از اینکه رندر بعدی همون ارجاع قبلی رو تحویلت میده یا نه.
ایدهش اینه که همین «قصدِ پایدار بودن» بشه بخشی از تایپ. کل ماجرا روی یه برند فانتوم (phantom brand) سواره — یعنی یه نشونهی خالی که فقط تو سطح تایپ وجود داره و موقع اجرا هیچی نیست:
declare const stableBrand: unique symbol
type Stable<T> = T extends object ? T & { readonly [stableBrand]: true } : Tآبجکت و آرایه و تابع برند میخورن، ولی مقدارهای اولیه دستنخورده رد میشن چون React خودش اونها رو با مقدار مقایسه میکنه. کلید کار unique symbol ـه: اگه بهجاش یه کلید رشتهای مثل _stable: true میذاشت، هر آبجکتی که اتفاقی همون شکل رو داشت برند رو جعل میکرد. حالا یه prop میتونه Stable بخواد و آرایهای که همونجا تو JSX ساخته شده تایپش غلط میشه — یعنی مسئولیت برمیگرده گردن صداکننده، دقیقاً همونجایی که باید باشه.
اولین تلاش نویسنده module augmentation بود: پکیج رو import کنی و تایپ هوکهای React عوض بشه. ولی augmentation فقط اجازهی اضافهکردن overload رو میده، نه حذف اونهایی که @types/react داره؛ پس همین که یکی از dependencyها برند نداشت، TypeScript بیسروصدا میافتاد رو overload اصلی React و هیچ خطایی نمیداد. راهحل نهایی یه entry point جدا شد که نسخهی سختگیر همون هوکهای واقعیه:
import { useCallback, useMemo } from 'stableref/react'
const stableFilter = useCallback(filter, [query])
useMemo(() => source.filter(stableFilter), [source, stableFilter])
// Stable<Item[]>
useMemo(() => source.filter(rawFilter), [source, rawFilter])
// ^ type errorبخشی از پایداری رو خود React مجانی میده و تایپها همونا رو برند میزنن: خروجی useState و setState و useRef از قبل پایدارن. نویسنده تأکید میکنه Stable یعنی React بیدلیل هویت جدید نمیسازه، نه اینکه state عوض نمیشه. برای ثابتهای module-scope هم هلپر stable() هست که موقع اجرا فقط خود مقدار رو برمیگردونه. Context هم سریع جواب میده: createStableContext مقدار Provider رو مجبور به پایداری میکنه، پس یه value ساختهشده inline دیگه کل زیردرخت رو کار نمیندازه.
نویسنده میگه این تضمین کامل نیست — یه cast ساده هر برندی رو دور میزنه و قراره escape hatch بمونه. React Compiler هم سؤال دیگهای رو جواب میده: کامپایلر میپرسه «میتونم این هویت رو خودکار حفظ کنم؟»، ولی Stable میگه «یه کامپوننت دیگه میتونه رو این هویت بهعنوان قرارداد بهینهسازی حساب کنه». انگیزهی جالبش ایجنتهان: ایجنت رندر اضافه رو نمیبینه ولی به خطای تایپ جواب میده، برای همین خطاها بهجای never خشک یه راهنمای متنی نشون میدن.
نکات کلیدی:
- Stable با یه unique symbol بهعنوان برند فانتوم ساخته میشه و موقع اجرا هزینهای نداره
- module augmentation کار نکرد چون overload اصلی React باقی میمونه و خطا بیصدا گم میشه
- هوکهای سختگیر از stableref/react و stableref/preact میان؛ موقع اجرا همون هوکهای اصلیان
- خروجی useState، setState و useRef از قبل برندخوردهن؛ stable() برای ثابتهای module-scope
- cast میتونه برند رو دور بزنه؛ نویسنده اینو یه escape hatch عمدی میدونه




