این یکی از مهمترین مفاهیمی است که اگر درست بفهمی، نصف باگهای عجیب ریاکت برایت قابل توضیح میشود. بگذار از پایه شروع کنم.
۱. اول ببینیم واقعاً چه اتفاقی میافتد
فرض کن این کامپوننت را داری:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>شمارنده: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}وقتی کاربر روی دکمه میزند:
1. setCount(count + 1) صدا زده میشود.
2. ریاکت میگوید: «state عوض شد. باید این کامپوننت را دوباره اجرا کنم.»
3. کل تابع Counter از نو اجرا میشود. (این خیلی مهم است)
4. این بار count مقدار جدیدی دارد، پس JSX جدیدی برگردانده میشود.
5. ریاکت این JSX جدید را با JSX قبلی مقایسه میکند (diffing) و فقط تفاوتها را روی DOM واقعی اعمال میکند.
این «اجرای مجدد تابع» همان چیزی است که به آن re-render میگوییم.
۲. حالا snapshot یعنی چه؟
وقتی میگویم «هر بار state عوض میشود، ریاکت یک snapshot جدید میسازد»، منظورم این است:
هر بار تابع کامپوننت اجرا میشود، تمام مقادیر state و props آن اجرا، مثل یک عکس فوری، قفل میشوند. آن اجرا، آن مقادیر را برای همیشه دارد. این عکس فوری هرگز تغییر نمیکند.
بگذار با یک مثال concrete نشانت بدهم:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return <button onClick={handleClick}>{count}</button>;
}بیایم مرحله به مرحله ببینیم چه اتفاقی میافتد:
اجرای اول (initial render):
· count = 0
· تابع handleClick ساخته میشود. این تابع الان count = 0 را در خودش «قفل» کرده.
· JSX: <button>0</button>
· این یک snapshot است: { count: 0, handleClick(نسبت به count=0) }
کاربر کلیک میکند:
· handleClick اجرا میشود.
· داخلش count همان 0 است (چون در snapshot اول قفل شده).
· setCount(0 + 1) یعنی setCount(1).
· ریاکت میگوید state عوض شد. کامپوننت را دوباره اجرا کن.
اجرای دوم (re-render):
· count = 1
· یک handleClick جدید ساخته میشود. این تابع جدید count = 1 را قفل کرده.
· JSX: <button>1</button>
· snapshot جدید: { count: 1, handleClick(نسبت به count=1) }
کاربر دوباره کلیک میکند:
· handleClick جدید اجرا میشود.
· این بار count همان 1 است.
· setCount(1 + 1) یعنی setCount(2).
و به همین ترتیب...
نکته کلیدی اینجاست: هر snapshot، مستقل و جدا از snapshotهای دیگر است. مقادیر داخلش هرگز تغییر نمیکنند. وقتی میگوییم state عوض شد، در واقع داریم یک snapshot جدید میسازیم، نه اینکه داخل snapshot قبلی دست ببریم.
۳. اشتباه رایجی که این مفهوم را روشن میکند
این کد را ببین:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
return <button onClick={handleClick}>{count}</button>;
}سوال: اگر کاربر کلیک کند، count چند میشود؟
جواب شهودی: «3 تا اضافه شده، پس میشود 3.»
جواب واقعی: میشود 1.
چرا؟ چون در این snapshot، count = 0 است. هر سه فراخوانی setCount، مقدار 0 + 1 = 1 را میفرستند. ریاکت سه بار میگوید «1 شو» که در نهایت همان 1 میشود.
این دقیقاً همان مفهوم snapshot است. count که داخل handleClick میبینی، همان مقدار قفلشده در snapshot جاری است. همهجا در این اجرا، count فقط یک معنا دارد: 0.
اگر بخواهی این 3 تا واقعاً جمع شوند، باید از فرم تابعی استفاده کنی:
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
چون اینجا به count قفلشده دست نمیزنی، بلکه به ریاکت میگویی: «آخرین مقدار state را بگیر و یکی اضافه کن.» ریاکت این کارها را به ترتیب انجام میدهد و میشود 3.
@perfino_com