وقتی اپلیکیشنهای React را توسعه میدهیم، ممکن است متوجه شده باشیم که بهروزرسانی state، مقادیر جدید را بلافاصله بعد از تغییر نشان نمیدهد. دلیل این اتفاق این است که آپدیت state به صورت asynchronous انجام میشود. به این معنی که، React منتظر میماند تا تمام کدهای درون event handler اجرا شوند و پس از آن state را آپدیت کند. به عنوان مثال:
const [count, setCount] = useState(0); useEffect(() => { setCount(1); console.log(count); }, []);
کد بالا عدد ۰
را در کنسول چاپ میکند. این اتفاق به این دلیل رخ میدهد که React قبل از آپدیت state، ابتدا کدها را اجرا میکند. بنابراین، مقدار count
در اولین رندر برابر با ۰
است.
این فرایند که به آن batching یا گروهبندی گفته میشود، توسط React برای پردازش آپدیتهای متعدد stateها، بدون فراخوانی بیش از حد re-renderها مورد استفاده قرار میگیرد. این کار به React این امکان را میدهد که چندین state را به صورت همزمان بهروزرسانی کند و در نتیجه، رندرهای غیرضروری کاهش یابد و کارایی و عملکرد کلی بهینه شود.
برای رفع مشکل آپدیت state در کد بالا، count
را به عنوان dependency به هوک useEffect
اضافه میکنیم:
useEffect(() => { setCount(1); console.log(count); }, [count]);
این بار خروجی دو بار در کنسول چاپ خواهد شد. بار اول که React از بلاک کد عبور میکند، مقدار count
برابر با ۰
است. اما چون state مربوط به count
تغییر کرده است، React صفحه را مجددا رندر کرده و مقدار count
به ۱
تغییر پیدا میکند.
ما در این مقاله قصد داریم تا دلایلی که باعث میشود React بلافاصله state را آپدیت نکند، بررسی کنیم. همچنین مثالی ارائه کرده و توضیح میدهیم که در صورت نیاز به ایجاد تغییر جدید در state، هم در کلاس کامپوننتها و هم در فانکشنال کامپوننتها چه کارهایی باید انجام دهیم. در نهایت، نحوه مدیریت تنظیم مجدد state در کامپوننتهای child را بررسی خواهیم کرد.
همانطور که بالاتر دیدیم، بهروزرسانیهای state به طور ذاتی فرآیندهای asynchronous هستند. زمانی که تابع setState
را فراخوانی میکنیم، React به جای اعمال فوری تغییرات، آپدیتها را زمانبندی میکند. هنگامی که setState
را فراخوانی میکنیم، React صفی از آپدیتهای معلق را نگه میدارد. سپس، چندین فراخوانی setState
که در همان بلاک به صورت همزمان رخ میدهند، گروهبندی میکند.
این فرآیند گروهبندی برای جلوگیری از رندرهای غیرضروری و بهینهسازی عملکرد بسیار مهم است. React حداقل تغییرات مورد نیاز برای آپدیت state کامپوننت را محاسبه کرده و آنها را در یک چرخه رندرینگ کارآمد اعمال میکند.
برای آپدیت state در کامپوننتهای React، ما از تابع this.setState
یا تابع بهروزرسانی که توسط هوک React.useState()
return میشود، به ترتیب در کلاس کامپوننتها و فانکشنال کامپوننتها استفاده میکنیم.
آپدیتهای state در React به صورت asynchronous هستند؛ زمانی که یک بهروزرسانی درخواست میشود، هیچ تضمینی وجود ندارد که آپدیتها بلافاصله انجام شوند. توابع بهروزرسانی، تغییرات state کامپوننت را در یک صف قرار میدهند؛ اما React ممکن است تغییرات را به تعویق انداخته و چندین کامپوننت را در یک چرخه آپدیت نماید. به عنوان مثال:
const [count, setCount] = useState(0); const handleClick = () => { setCount(1); setCount(1); setCount(1); };
در داخل تابع handleClick
سه تابع بهروزرسانی setCount
داریم. با این حال، زمانی که event handler handleClick
فراخوانی میشود، state count
به ۱
آپدیت میگردد.
برای درک این موضوع، یک console.log
به کد خود اضافه میکنیم تا state count
را پس از هر تابع بهروزرسانی در کنسول ثبت نماییم.
const handleClick = () => { setCount(1); console.log("log 1:", count); setCount(1); console.log("log 2:", count); setCount(1); console.log("log 3:", count); };
متوجه خواهیم شد که کنسول همچنان مقدار قدیمی count
(۰
) را ثبت میکند. این به این دلیل است که در اولین re-render، مقدار count
برابر ۰
است و state تنها پس از تکمیل فرآیند batch آپدیت میشود.
بهطور مشابه، کد زیر را در نظر میگیریم:
const handleClick = () => { setName("Amaka") setAge(20) setAddress("No 3 Rodeo drive") }
در این قطعه کد، سه فراخوانی مختلف برای آپدیت و re-render کردن کامپوننت وجود دارد. فراخوانی توابع بهروزرسانی یکی پس از دیگری و re-render کردن کامپوننتهای parent و child پس از هر فراخوانی، در بیشتر موارد ناکارآمد خواهد بود. به همین دلیل ریاکت stateها را بهصورت batch آپدیت میکند.
فرقی نمیکند که چند فراخوانی setState()
در یک event handler مانند handleClick
وجود داشته باشد؛ در نهایت، همهی آنها فقط یک re-render در پایان event ایجاد میکنند. این فرآیند برای حفظ عملکرد مطلوب در برنامههای بزرگ بسیار حیاتی است. ترتیب درخواستهای بهروزرسانی همیشه رعایت میشود؛ یعنی React همیشه ابتدا به درخواستهای اولیه رسیدگی میکند.
ما متوجه شدیم که تأخیر در پردازش درخواستهای آپدیت برای batching آنها مفید است. در ادامه، مدیریت شرایطی را بررسی میکنیم که در آن نیاز داریم تا قبل از استفاده از مقادیر جدید، منتظر تکمیل این بهروزرسانیها باشیم.
setState()
callbackپارامتر دوم متد setState()
یک تابع callback اختیاری است. این تابع زمانی اجرا میشود که عملیات setState()
کامل شده و کامپوننت re-render شده باشد. این تابع callback تضمین میکند که بعد از اعمال بهروزرسانی state اجرا شود:
handleSearch = (e) => { this.setState({ searchTerm: e.target.value },() => { // Do an API call with this.state.searchTerm }); }
componentDidUpdate
تابع componentDidUpdate
بلافاصله پس از آپدیت state اجرا میشود. برای جلوگیری از ایجاد یک حلقه بینهایت، همیشه باید از یک شرط استفاده کنیم تا مطمئن شویم که state قبلی و state فعلی یکسان نیستند:
componentDidUpdate(prevProps, prevState) { if (prevState.count !== this.state.count) { // Do something here } }
useEffect()
همانطور که در بخش مقدمه دیدیم، میتوانیم در هنگام آپدیت state از هوک useEffect
برای اعمال side effectها استفاده کنیم. متغیر state میتواند بهعنوان یک dependency در این هوک اضافه شود، بهطوری که با تغییر مقدار state، این هوک اجرا شود. میتوانیم از هوک useEffect
برای پی بردن به تغییرات state استفاده کنیم:
import React,{useState, useEffect} from 'react'; const App = () => { const [count, setCount] = useState(1); useEffect(() => { if (count > 5) { console.log('Count is more that 5'); } else { console.log('Count is less that 5'); } }, [count]); const handleClick = () => { setCount(count + 1); }; return ( <div> <p>{count}</p> <button onClick={handleClick}> add </button> </div> ); }; export default App;
تابع callback در هوک useEffect
تنها زمانی اجرا میشود که یکی از متغیرهای state که در آرایه dependency آن ذکر شده است، تغییر کند.
باید به این نکته توجه داشته باشیم که از استفاده بیمورد از هوک useEffect
خودداری کنیم. زیرا، هوک useEffect
برای هماهنگسازی کامپوننتها با سیستمهای خارجی، مانند زمانی که network و API call انجام میدهیم طراحی شده است. برای اطلاعات بیشتر مطالعه مستندات رسمی میتواند مفید باشد.
useState()
callbackهوک useState
در React مانند setState
تابع callback داخلی ندارد. با این حال، میتوانیم از آن برای دستیابی به عملکردی مشابه عملکرد هوک useEffect
استفاده کنیم، که به ما این امکان را میدهد تا پس از رندر شدن کامپوننت، side effectها را اجرا نماییم. کد قبلی که داشتیم به صورت زیر بود:
const [count, setCount] = useState(0); const handleClick = () => { setCount(1); setCount(1); setCount(1); };
برای آپدیت state، از یک تابع بهروزرسانی استفاده میکنیم، چیزی شبیه به این تابع: n => n + 1
، که در آن n
مقدار state قدیمی یا قبلی است. اکنون کدی که داریم به شکل زیر میباشد:
const handleClick = () => { setCount((n) => n + 1); setCount((n) => n + 1); setCount((n) => n + 1); };
زمانی که تابع handleClick
فراخوانی میشود، مقدار count
به ۳
تغییر میکند. این به این دلیل است که وقتی React از میان بلاکهای کد عبور میکند، state count
تغییر میکند.
اکنون این موضوع را بیشتر بررسی میکنیم. در اولین تابع setCount
:
n
برابر ۰
است، پس ۰ + ۱ = ۱
(React این را دستهبندی میکند و مقدار count
را به ۱
تغییر میدهد)n
برابر ۱
است، پس ۱ + ۱ = ۲
(React میبیند که مقدار count
تغییر کرده است. بنابراین، مقدار جدید count
را به مقدار قبلی اضافه کرده و دستهبندی میکند)n
برابر ۲
است، پس ۲ + ۱ = ۳
(React دوباره میبیند که مقدار count
تغییر پیدا کرده است. پس مقدار جدید count
را به مقدار قبلی اضافه کرده و دستهبندی میکند)در نهایت، فرآیند دستهبندی کامل میشود و React مقدار جدید count
را برابر ۳
تنظیم مینماید.
ما میتوانیم هر نام دلخواهی را به تابع آپدیتکننده اختصاص دهیم. در این مقاله برای این که مفهوم به راحتی قابل انتقال باشد از prevState
استفاده میکنیم. بنابراین داریم: setCount((prevState) => prevState + 1)
.
به طور پیشفرض، تا زمانی که کامپوننت در حال رندر شدن باشد یا در همان موقعیت ثابت باقی بماند، state حفظ میشود. به عنوان مثال:
import { useState } from "react"; import "./styles.css"; export default function App() { return ( <div className="App"> <h1>Hello CodeSandbox</h1> <User /> <User /> </div> ); } export function User() { const [age, setAge] = useState(0); const handleAddUser = () => { setAge((prevState) => prevState + 1); }; return ( <div> <h3>Age: {age}</h3> <button type="button" onClick={handleAddUser}> Add user </button> </div> ); }
ما دو کامپوننت child در App
داریم. اگر سعی کنیم بر روی دکمههای مختلف کلیک کنیم خواهیم دید که هر دو child به طور همزمان آپدیت نمیشوند. این به این دلیل است که آنها به عنوان دو کامپوننت مختلف با stateهای مختلف در نظر گرفته میشوند؛ و ما میدانیم تا زمانی که کامپوننتها در حال رندر شدن باشند یا در همان موقعیت باقی بمانند، state آنها حفظ میشود. به عنوان مثال:
import { useState } from "react"; import "./styles.css"; export default function App() { const [show, setShow] = useState(true); const handleAddUser = () => { setShow(!show); }; return ( <div className="App"> <h1>Hello CodeSandbox</h1> <button type="button" onClick={handleAddUser}> Toggle </button> {show ? <User name="Chimezie" /> : <User name="Innocent" />} </div> ); } export function User({ name }) { const [age, setAge] = useState(0); const handleAddUser = () => { setAge((prevState) => prevState + 1); }; return ( <div> <h3> Name: {name}, Age: {age} </h3> <button type="button" onClick={handleAddUser}> Add user </button> </div> ); }
در حال حاضر ما از یک شرط برای رندر کردن هر دو کامپوننت child استفاده میکنیم. با این حال، نامهایی داریم که برای تمایز بین زمانی که هر کامپوننت نمایش داده میشود یا رندر میشود، استفاده میکنیم. بر روی دکمهها کلیک میکنیم و بین هر دو کامپوننت جابهجا میشویم. مشاهده خواهیم کرد تا زمانی که کامپوننت در حال رندر شدن باشد و در همان موقعیت باقی بماند، state حفظ میشود.
برای ریست کردن state در کامپوننتهای child، روشهای مختلفی وجود دارد که میتوانیم از آنها استفاده کنیم.
در مثال شرطی بالا، ما هر دو کامپوننت را در همان موقعیت رندر میکنیم و به همین دلیل آنها از یک state مشترک استفاده میکنند و این state با وجود تغییر، حفظ میشود.
اما اگر آنها را به طور جداگانه رندر کنیم، هر بار که تغییر وضعیت میدهیم، state ریست میشود:
return ( <div className="App"> <button type="button" onClick={handleAddUser}> Toggle </button> {show && <User name="Chimezie" />} {!show && <User name="Innocent" />} </div> );
این اتفاق به این دلیل رخ میدهد که هر دو کامپوننت child در یک موقعیت رندر یکسان قرار ندارند. بنابراین زمانی که عدد age
را افزایش میدهیم و سپس به کامپوننت child بعدی تغییر وضعیت میدهیم، عدد age
به طور خودکار ریست میشود.
راه دیگری که برای ریست کردن state در کامپوننتهای child وجود دارد، استفاده از props key
است. معمولاً از props key
برای پیمایش در لیستها و منحصربهفرد کردن هر آیتم استفاده میکنیم. به طور مشابه، میتوانیم از آن برای ریست کردن state کامپوننت child نیز بهرهمند شویم. زمانی که props key
یک کامپوننت child تغییر میکند، React آن را به عنوان یک کامپوننت جدید در نظر میگیرد و بنابراین، state آن ریست میشود. با استفاده از مثال قبلی:
return ( <div className="App"> <button type="button" onClick={handleAddUser}> Toggle </button> {show ? ( <User key="Chimezie" name="Chimezie" /> ) : ( <User key="Innocent" name="Innocent" /> )} </div> );
در این مثال، ما props key
منحصربهفردی به هر دو کامپوننت child اختصاص میدهیم. با تغییر props key
در هر بار تغییر، state نیز با آن ریست میشود.
زمانی که در ریاکت state را مدیریت میکنیم، درک ماهیت asynchronous آن و تکنیک batching برای بهینهسازی عملکرد اهمیت زیادی دارد. ریاکت stateها را بهصورت دستهای آپدیت میکند تا از re-renderهای غیرضروری جلوگیری کند.
برای جلوگیری از مشکلاتی که در آن React پس از تغییر state، کامپوننتها را re-render نمیکند، بهتر است از تغییر مستقیم آبجکتها در state پرهیز کرده و از توابع setter مانند setState
یا setCount
استفاده کنیم. تکنیکهایی مانند استفاده از props key
برای کامپوننتهای child یا مدیریت dependencyها در هوک useEffect
نیز میتواند چالشها در هماهنگی state کامپوننتهای child و parent را برطرف کند.
استفاده از شیوههایی که در این مقاله بیان شد، اطمینان میدهد که برنامه React ما آپدیت stateها را بهطور کارآمد مدیریت کرده و هم عملکرد و هم تجربه کاربری را بهبود میبخشد.
۵۰ درصد تخفیف ویژه زمستان فرانت کست تا پایان هقته
کد تخفیف: wnt