بسم الله الرحمن الرحیم
یکی از مواردی که این روزها همه درگیر اون هستیم، رعایت اصل بروز نگه داشتن پروژه است، که بتونیم کدهای پروژمون رو بر اساس آخرین تکنولوژیها و بروزرسانیهای ارائه شده از طرف library یا framwork ای که استفاده میکنیم، بروز نگه داریم. این امر یک سری موارد رو میطلبه که در ادامه خدمتتون عرض خواهم کرد.
اول چند تا پرسش و پاسخ و با هم داشته باشیم تا یکم گرم شیم ?:
1) این آموزش برای چه کسانی مناسب است؟
برای مطالعه این آموزش دانستن مفاهیم قدیمی در ریاکت لازم است. اگر شما دارید تو پروژهای کار میکنید که کلا function component است و با Hooks نوشته شده، خب پس شاید خیلی به کارتون نیاد. البته کلی مطلب جالب درباره Hooks ها عنوان میشه که شاید اونها رو نشنیده باشید(کم شنیده باشید?). و کسانی که دارن تو پروژه هایی کار میکنن که بیش از 2 سال قدمت داره! چون اون زمان اصلا Hooks تشریف نیاورده بودن! و با استفاده از class component ها کدنویسی انجام میشد. لایفسایکل رو باید به طور کامل پیاده سازی میکردیم و جلوی رندر های اضافه رو با memo کردن کامپوننت ها میگرفتیم.
2) چه کسانی صلاحیت ریفکتور کردن تو تیم رو دارن؟
به نظر بنده ریفکتور کار مقدسیه و هر کسی نباید این کار رو بکنه. اما در کل هر کسی میتونه ریفکتور کنه که علاقه داشته باشه و ریفکتور رو کار بیهوده ندونه. عزیزانی معتقد هستن که: خوب کد که داره کار میکنه و مشتری هم که راضیه! برای چی الکی وقت بزاریم. ? . قضاوت این افراد با شما.
اما انشاالله که شما از دسته دوم هستید و آراستگی رو به شلوغی و شلختگی ترجیح میدید.
3) از کجا بفهمم روحیه من با ریفکتور سازگاره یا نه؟
به نظرم چند تا سوال از خودتون بپرسید:
1. آیا به ظاهر خودتون اهمیت میدید؟
2. آیا تو کارهاتون نظم دارید؟
3. آیا حالتون از ارائه فیچرهای جدید خوبه یا اینکه به خودتون میگید یا خدا بازم باید برم چیز جدید یاد بگیرم؟!
4. آیا چالش رو دوست دارید؟
5. آیا از قدرت تحلیل بالایی برخوردارید؟
اگر این سوالها شما رو اذیت میکنه به نظرم این کار مقدس رو به همکار محترمتون بسپارید. مگر این که اجباری تو فرآیند ریفکتور قرار گرفتید! که امیدوارم این تغییرات رو در خودتون ایجاد کنید و آراستگی رو اصل اول پایداری بدونید. ?
4) اصلا چرا باید ریفکتور کنیم؟ ما انقدر حرفه ای هستیم که از اول درست کد میزنیم.
هر چقدر هم که خوب کد بزنیم بعد از گذشت چند ماه لاجیک سیستم به گونهای تغییر میکنه که شما مجبور میشید گاها بدون فکر کردن به سایر قسمت های پروژه، بخشی رو ویرایش و یا دولوپ کنید. نه اینکه خدای نکرده شما خوب فکر نمیکنید. نه! بلکه این میتونه حاصل زحمت تیم مارکتینگ یا محصول باشه، که انقدر فیچر ها رو بر اساس نیاز مشتری تغییر میدن تا از یه MVP برسن به یک مدل فضایی! اینجا بحث زمان مطرح میشه که معمولا تو پروژه های بزرگ زمان خیلی کوتاهه! بعد از گذشت یک مدت میبینید که کدهای تکراری زیاد شده و قابل استفاده مجدد نیست، و اصطلاحا قیمهها ریخته تو ماستها! ?
5) از کجا شروع کنیم؟
بریم سراغ اصل داستان :
اولین مشکلی که همه با اون روبرو هستن: نحوه تبدیل یک class component به یک function component است. به تصویر زیر دقت کنید:
که بعد از refactor به function component این صورت نوشته میشه:
تا اینجا (تو کد بالا) همه چیز واضحه و آشکار.
بعد از ریفکتور به function component این صورت نوشته میشه :
که بعد از ریفکتور با Hooks این صورت نوشته میشه :
نکته : اگر در state از چندین property استفاده شده باشه، به صورت زیر از useState های جداگانه استفاده میکنیم.
اما یک سوال! آیا این کار همیشه درسته؟ مخصوصا تو ریفکتور کردن!
تو مثال زیر یه api صدا زده میشه و بعد از دریافت ریسپانس قراره data رو جدا کنیم و اگر ارور داشت تو یه استیت جدا ذخیره کنیم و همچنین اگر در حال دریافت اطلاعات بودیم loading رو داشته باشیم و بعد از اینکه دیتا به صورت کامل دریافت شد، loaded رو true کنیم که بفهمیم دیتا دریافت شده.
بعد از اینکه response دریافت میشه، setState یک شی با چهار property رو میگیره. این فقط یک مثاله، اما مورد عمومی در اینجا اینه که شما یک state دارید که موقع setState شدن، ویژگیهای جدید رو با ویژگی های قدیمی که از قبل داشته جایگزین میکنه و اگر هم وجود نداشته باشه که به state اضافه میکنه. اما اگر همین رو بخوایم به Hooks تبدیل کنیم، احتمالا میخوایم به ازای تک تک اون prop ها یه useState ایجاد کنیم! به شکل زیر:
و اینکه setState ها هم به صورت زیر به useState تغییر پیدا میکنه :
بله این به صورت کاملا درست کار میکنه! اما این کافی نیست! و شما نیاز دارید که کارهای خفن تر انجام بدین! من اینجا استفاده از useReducer رو پیشنهاد میدم! چقدر زیبا! به کد زیر نگاه کنید :
اما تو کد بالا چه اتفاقی میافته؟ reducer استیت قبلی رو به همراه استیت جدید میگیره و استیت های جدید رو روی استیت های قدیمی overwrite میکنه. اینجا شما فقط this رو که قبل از setState تو class component آورده شده بود، حذف میکنید و میبینید که کدتون با کمترین حجم تغییرات به درستی کار میکنه!
کد کامل رو با Hooks در پایین ببینیم :
const AppHooks = () => { const initialState = { data: null, error: null, loaded: false, fetching: false, } const reducer = (state, newState) => ({ ...state, ...newState }) const [state, setState] = useReducer(reducer, initialState); async function fetchData() { const response = await fetch(API_URL); const { data, status } = { data: await response.json(), status: response.status } // error? if (status !== 200) { return setState({ data, error: true, loaded: true, fetching: false, }) } // no error setState({ data, error: null, loaded: true, fetching: false, }) } useEffect(() => { fetchData() }, []) const { error, data } = state return error ? Sorry, and error occured! : <pre>{JSON.stringify(data, null, ' ')}</pre> }
لازمه تو این قسمت ابتدا کمی به بررسی lifecycle method های خیلی مهم در class component ها بپردازیم. البته صرفا جهت یادآوری داره.
تو function بالا، زمانی که بارگذاری component به صورت کامل به اتمام رسید ، اجرا میشه. عمده کاری که اینجا انجام میدادیم این بود که Data رو از طریق API دریافت میکردیم.
تو function بالا، هر زمانی که component بروزرسانی میشد، مواردی رو بررسی میکردیم. عمده کاری که اینجا انجام میشد این بود که prop ها یا state های قبلی component رو (قبل از update) با prop های جدیدش (بعد از update) مقایسه میکردیم تا ببینیم اگر چیزی تغییر کرده بود، بتونیم هدفی رو مدیریت کنیم.
تو function بالا،زمانی که کار یک component به اتمام میرسید، کاری رو انجام میدادیم. به عناون مثال اگر یک تایمر رو زمانی که یک component اجرا میشد، استارت میکردیم، باید بعد از اینکه اون کامپوننت از lifecycle خارج میشد، تایمرش رو هم stop میکردیم، تا روی client بار بیهوده باقی نمونه.
خوب حالا بریم سراغ ریفکتور کردنمون و ببینیم که چطوری میتونیم این function ها رو با استفاده از Hook ها پیاده سازی کنیم.
تو کد بالا همینطور که مشاهده میکنید، از useEffect استفاده کدیم. این دقیقا همون رفتار componentDidMount رو شبیح سازی میکنه.
به این نکته دقت کنید: حتما به عنوان argument دوم آرایه خالی ارسال کنید. این باعث میشه که فقط یک بار این Hook اجرا بشه. اگر این آرایه رو کلا قرار ندید، هربار که کامپوننت reRender بشه، این Hook هم اجرا میشه.
تو کد بالا هم اومدیم componentDidUpdate رو شبیح سازی کردیم. اینجا به عنوان argument دوم، id رو تو قالب یک آرایه به useEffect پاس دادیم. این باعث میشه زمانی fetchData اجرا بشه که مقدار id تغییر کرده باشه و در هربار render شدن component این اتفاق رخ نده. معادل این رفتار رو تو class component به شکل زیر داشتیم.
اینجا باید به ازای هر property یک شرط قرار میدادیم. اما تو useEffect میتونیم به ازای هر شرط یک useEffect جدا بنویسیم. به تصویر زیر نگاه کنید:
و اینکه شما میتونید چندید property رو در آرایه قرار بدید و با ویرگول از هم جدا کنید. (تصویر زیر)
همچنین میتونید state های component رو با این روش listen کنید تا زمانی که change شد، بتونید یک action رو call کنید. (جلوتر جداگونه درباره state ها توضیح میدم)
نکته دیگه ای که اینجا خیلی مهمه اینه که الان ما داریم رو یه variable که یک number و یا string و ... است، listen میکنیم! اما اگر ورودی object بود چی ؟ خوب هربار که مقدار object رو با مقدار قبلی مقایسه میکنه، با دفعه قبلش فرق میکنه و نهایتا تو loop میافته!
خیلی به این موضوع دقت کنید و مقالههایی که تو این زمینه منتشر شده رو با دقت مطالعه کنید.
استفاده از JSON.stringify باعث میشه که object شما به string تبدیل بشه. بنابر از این به بعد از نظر رشته مقایشه میشه.
این راهکار جواب میده، اما با احتیاط استفاده کنید. فقط از JSON.stringify روی object هایی با مقادیر نه چندان پیچیده و با انواع دادههایی که به راحتی قابل serialize شدن هستند استفاده کنید.
همونطوری که تو تصویر بالا مشاهده میکنید، یکم حجم کد بالا تر میره، اما راه مطمئن تریه.
مقدار person تو یه ref ریخته میشه (خط 10) و دفعات بعدی که کامپوننت reRender میشه، با مقدار قبلی مقایسه میشه (خط 5). پایین تر یه مثال از کل lifecycle میزنم خیلی خوبه! اونجا همه چیز شفاف تر میشه ?
همینطور که تو کد بالا مشاهده میکنید، شما میتونید داخل useEffect یک function هم return کنید تا زمانی که component از lifecycle خارج شد، کاری انجام بشه. مثلا ما تو کد بالا اومودیم یه timer رو زمانی که component برای اولین بار اجرا میشه start کردیم و توی return هم گفتیم که stop بشه. البته این همهی کاری نیست که انجام میشه! من فقط سعی کردم یه مثال بزنم. شما میتونید حتی API های خودتون رو تو این قسمت cancel کنید و ...
متاسفانه اینجا مجبورید ذهنتون رو یکم تغییر بدید
لطفا ساختار callback در setState رو هرگز با خودتون رو به Hook ها تو function component نیارید.
تو useState ما کلا callback نداریم! البته با اون مفهومی که تو setState داشتیم!
تو ساختار class component ها به این صورت از callback در setState استفاده میکردیم.
همینوطوری که تو مثال (تمرینی) بالا مشاهده میکنید، در خط 17، به عنوان argument دوم، یه function به setState پاس دادیم تا بعد از اینکه مقدار 2 رو تو count ریخت، increment رو صدا بزنه و مجددا یک عدد به count اضافه کنه.
حالا ببینیم همین مثال رو با Hooks چطور میشه refactor کرد. ???
به کد بالا رو دقت کنید:
یادتون هست اولش گفتم که باید کمی ذهنتون رو تغییر بدید؟ الان متوجه شدید چرا؟ یک بار دیگه مرور میکنم! ما تو Hooks بجای اینکه از callback استفاده کنیم، در واقع میایم اون state (در اینجا همون count) رو watch میکنیم، به عبارت خودمونی تر: هواسمون هست که هروقت count تغییر کرد، increment رو صدا بزنیم.
خودتون هم یک بار تست کنید ببینید چه جذابه! ?
نکته1. خیلی دقت کنید که اگر دیتایی رو دارید ایجاد میکنید که مقدار مشخصی داره و تو هر بار reRender شدنِ component مقدارش تغییر نمیکنه، حتما از useMemo استفاده کنید.
مثال زیر رو ببینید :
من اینجا یه مقدار خیلی ساده مثال زدم ( ممکنه شما دادهای داشته باشید که بر اساس یه process طولانی بدست اومده باشه ) این کار ( استفاده از useMemo ) باعث جلوگیری از مقدار دهی person در هربار reRender شدن میشه. توجه داشته باشید: فقط اولین باری که کامپوننت mount میشه، مقداردهیِ person صورت میگیره.
عاشق ادبیات خودم شدم ? (صورت میگیره ?) راستش ترجیح دادم خودمونی بنویسم، چون حس میکنم ارتباط برقرار کردن با این نوع دستخطها بهتر اتفاق میافته.
نکته2. استفاده از useEffect بدون در نظر گرفتن argument دوم (همون آرایهی خالی) خیلی خطرناکه! خیلی باید حواستون باشه که: تو هربار reRender شدنِ component، این useEffect عزیز اجرا میشه. پس هرچیزی رو اونجا قرار ندیم. ولی اگر واقعا چارهای نبود، حتما از عبارات شرطی (conditions) استفاده کنید. ? من چرا انقدر نگرانم ؟!
دو تا تصویر زیر رو با هم مقایسه کنید (اگر یک بار برای خودتون هم اجرا کنید که عالیه!)
این کد فاجعه ایجاد میکنه! کی؟ زمانی که والدش (parent) چندین بار reRender بشه! اونوقته که این component هم مجدد render میشه و useEffect دوباره صدا زده میشه.
با اضافه کردن آرایه خالی، useEffect فقط یک بار اجرا خواهد شد. به اصطلاح همون didMount تو class component ها اتفاق میافته.
نکتهای که خیلی از تازه کارها درگیرش هستن و به کرات اشتباه میکنن:
دوست عزیز، به ازایِ بروزرسانیِ هر state، یک بار کلِ component شما reRender میشه!
و هربار reRender بشه، useEffect اجرا خواهد شد. ( البته به شرط اینکه argument دوم (همون آرایه خالیه) رو بهش پاس نداده باشید ). به علاوهی اون، هر چیزی که تو root اون component هم نوشته باشید، اجرا میشه.
کد زیر رو اجرا کنید و کنسول رو هم چک کنید! ( البته اینجا آرایه خالی رو پاس دادیم که تو loop نیافته )
همینطوری که مشاهده کردید، عبارت "render" دو بار در console نمایش داده میشه. اولین بار زمانی که component برای اولین بار اجرا میشه و دومین بار زمانی که state مقداردهی یا بروزرسانی میشه!
به کد زیر دقت کنید:
import React, { useState, useCallback } from "react" const functionsCounter = new Set(); function App() { const [counter, setCounter] = useState(0); const handleClick = () => { setCounter((prevCounter) => prevCounter + 1); }; return ( <div> <div ={handleClick}>App click {counter}</div> <Test /> </div> ); } export default App; export function Test() { const [counter, setCounter] = useState(0); const handleClick = () => { setCounter((prevCounter) => prevCounter + 1); }; functionsCounter.add(handleClick); console.log("Test click : ", functionsCounter); return <div ={handleClick}>Test click {counter}</div>; }
کد این بخش خیلی ساده است. علت آوردن این مثال، شبیهسازیِ نحوهی نگه داشتنِ instance هایِ functionها، در stack است. چون باید با یک شبیه سازی، بهتون نمایش میدادم که وقتی یک متد رو مینویسید، باید به فکر نگهداشت اون تو stack هم باشید. اینجا مقدار شبیه سازی شدهی stack، همون functionsCounter است.
هربار که Test component اجرا میشه، یک instance از handleClick رو داخل functionsCounter اضافه میکنه! و این یعنی فاجعه! یعنی پرشدن ظرفیت stack بعد از گذشت کمی کار کردن با سایتتون. (بازم اشاره میکنم: تو پروژه هایی با مقیاس بزرگ)
برای جلوگیری از این فاجعه، باید functionهایی که تو reRenderهای مکرر instantiate میشن رو به useCallback مزین کنید. ( مزین ؟!!! به سبک جناب خان ?)
const handleClick = useCallback(() => { setCounter((prevCounter) => prevCounter + 1); }, [counter]);
بله دقیقا به همین سادگی! ما الان handleClick رو به useCallback مزین فرمودیم. ?
این امر موجب میشه هر بار که component Test درواقع reRender میشه، یک نمونه (instance) از handleClick تو stack نگهداری نشه و همیشه از آخرین کشی که داره استفاده کنه. این کش تا زمانی که مقدار count تغییر نکرده، تو حافظه باقی خواهد ماند. ( حتما خودتون تست کنید و کنسول رو ببینید )
انصافا تو خلوت خودتون کد زیر رو بنویسید و اجرا کنید - خط 10 تا 13 رو از comment در بیارید و 5 تا 8 رو comment کنید و مجددا console رو ببینید. به به ?
1. حتما تو componentWillUnmount مقادیری که باید از حالت اجرایی خارج کنید رو فراموش نکنید. مثلا اگر از axios برای درخواست api استفاده میکنید، حتما cancel رو توی willUnmount استفاده کنید.
2. کارهای محاسباتی رو حتما خارج از JSX انجام بدید و سعی کنید با useCallback و useMemo اون ها رو کش کنید.
3. حتما برای کارهاتون چک لیست داشته باشید