اگر تا پیش از این با ریاکت کار کرده باشید، احتمالاً میدانید که مدیریت وضعیت (state) یا استفاده از ویژگیھای چرخهی حیات تنها از طریق کلاسھا امکان پذیر بود. این موضوع گاھی باعث پیچیدگی و افزایش حجم کد میشد. اما از زمانی که ھوکھا (Hooks) در نسخه 16.8 ری اکت معرفی شدند، رویکرد توسعه کامپوننتھا دستخوش تغییر مثبتی شد.
ھوکھا (Hooks) در واقع توابعی ھستند که امکان استفاده از قابلیتھایی مانند state، اثرات جانبی (side effects) و حتی اشتراک منطق بین کامپوننتھا را در قالب کامپوننتھای تابعی (Functional Components) فراھم میکنند. استفاده از هوکھا باعث سادهتر شدن کد، خوانایی بیشتر و مدیریت بهتر منطق برنامه میشود.
در این مقاله قصد داریم نگاھی کاربردی و قابل فهم به انواع ھوکھا (Hooks) بیندازیم، مزایای آنھا را بررسی کنیم و با مثالھایی واضح نحوه استفاده از آنھا را آموزش دھیم. اگر به دنبال کدی تمیزتر، ساختاری مدرنتر و تجربهای بهتر در توسعه با ریاکت ھستید، ھوکھا (Hooks) نقطه شروع بسیار خوبی ھستند.
ریاکت ھوکھا - React Hooks
ھوک (Hook) یک تابع ویژه است که به توسعه دھندگان این امکان را میدھد تا بدون استفاده از کلاسھا، از امکاناتی مانند state و ویژگیھای دیگر React استفاده کنند. این باعث میشود که کد سادهتر، خواناتر و قابل مدیریتتر باشد، زیرا قابلیتھا مستقیماً درون کامپوننتھا اضافه میشوند.
مزایای استفاده از ھوک (Hook) ھا:
- مدیریت وضعیت (state management) راحتتر میشود.
- اپلیکیشن عملکرد بهینهتری دارد.
- منطق بین چند کامپوننت به سادگی قابل اشتراک میشود.
قوانین استفاده از ھوکھا
برای استفادهی صحیح از ھوکھا باید این سه قانون را رعایت کنید:
- ھوکھا فقط باید داخل کامپوننتھای تابعی React فراخوانی شوند و نمیتوان آنھا را در توابع معمولی یا کلاسھا استفاده کرد.
- ھوکھا فقط باید در سطح بالای کامپوننت فراخوانی شوند، یعنی نباید آنھا را داخل شرطھا (if)، حلقهھا (for) یا توابع تو در تو (nested functions) قرار داد.
- ھوکھا نباید به صورت شرطی فراخوانی شوند، یعنی استفاده از آنھا در شرایطی مانند مثال زیر مجاز نیست:
function MyComponent() {
if (isLoggedIn) {
const [user, setUser] = useState(null); // ❌
}
}
رعایت این قوانین برای درست کار کردن ھوکھا (Hooks) و تضمین اجرای صحیح آنھا توسط React ضروری است.
هوک useState در ریاکت
در ریاکت (React)، برای مدیریت وضعیت کامپوننتھا در کامپوننتھای تابعی، از ھوکی به نام useState استفاده میشود. این ھوک به شما امکان میدھد تا دادهھایی که ممکن است در طول زمان تغییر کنند را درون کامپوننت خود ذخیره و مدیریت کنید.
برای استفاده از این ھوک، ابتدا باید آن را از پکیج React ایمپورت نمایید:
import { useState } from "react";
سپس در داخل تابع کامپوننت، با فراخوانی useState میتوانید یک مقدار اولیه تعریف کرده و تابعی برای تغییر آن مقدار دریافت کنید:
import { useState } from "react";
function FavoriteColor() {
const [color, setColor] = useState("");
}
- state: متغیری است که وضعیت فعلی را نگه میدارد.
- setState: تابعی است که برای بروزرسانی مقدار state استفاده میشود.
- initialValue: مقدار اولیهای که میخواھید state با آن مقدار شروع شود.
مثال ساده:
import { useState } from "react";
function FavoriteColor() {
const [color, setColor] = useState("red");
return (
<>
<button onClick={() => setColor("blue")}>Blue</button>
<button onClick={() => setColor("pink")}>Pink</button>
<button onClick={() => setColor("green")}>Green</button>
</>
);
}
در این مثال:
- مقدار پیش فرض color برابر "red" است.
- با کلیک روی ھر دکمه، مقدار جدیدی برای رنگ انتخاب میشود.
- کامپوننت پس از ھر تغییر مقدار، مجدداً رندر میشود تا رنگ جدید نمایش داده شود.
ھیچگاه state را بهطور مستقیم تغییر ندھید!
برای بروزرسانی مقدار state، باید از تابعی استفاده کنیم که توسط ھوک useState در اختیار ما قرار گرفته است. این تابع معمولاً با پیشوند set شروع میشود، مانند setColor:
color = "red"; // نادرست ❌
setColor("red"); // درست ✅
استفاده از setColor باعث میشود ریاکت متوجه تغییر مقدار شود و کامپوننت را مجدداً بازرندر (Re-render) کند تا وضعیت جدید نمایش داده شود.
هوک useEffect در ریاکت
ھوک useEffect به شما این امکان را میدھد که اعمال جانبی (side effects) را در کامپوننتھای تابعی React انجام دھید. این اعمال جانبی میتوانند شامل موارد زیر باشند:
- فراخوانی API (درخواست به سرور)
- بروزرسانی دستی DOM
- تایمرھا و فواصل زمانی (setTimeout / setInterval)
- تغییر عنوان صفحه (document title)
- پاکسازی منابع (cleanup)
بیایید از یک تایمر به عنوان مثال استفاده کنیم:
import { useState, useEffect } from "react";
import ReactDOM from "react-dom/client";
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
setCount((count) => count + 1);
}, 1000);
});
return <h1>I've rendered {count} times!</h1>;
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Timer />);
این کد مکرراً شمارش را ادامه میدهد در حالی که ما فقط انتظار داشتیم یکبار شمارش انجام شود! دلیل این پدیده این است که هوک useEffect در هربار رندر شدن اجرا میشود. یعنی زمانی که count تغییر میکند، رندر جدیدی انجام میشود، و این رندر دوباره useEffect را اجرا میکند، و این چرخه بینهایت ادامه پیدا میکند.
برای اینکه useEffect فقط یکبار بعد از رندر اولیه اجرا شود، باید یک آرایه وابستگی خالی به عنوان آرگومان دوم به آن بدھیم:
useEffect(() => {
// فقط در اولین رندر اجرا میشود
}, []);
این باعث میشود که شمارش فقط یکبار انجام شود، درست پس از بارگذاری اولیه کامپوننت.
حالا مثال دیگری را بررسی کنیم. در این مثال، از ھوک useEffect ھمراه با وابستگی به یک متغیر (count) استفاده شده است. ھر بار که مقدار count تغییر کند، اثر (effect) اجرا میشود و مقدار calculation بروزرسانی خواھد شد:
import { useState, useEffect } from "react";
import ReactDOM from "react-dom/client";
function Counter() {
const [count, setCount] = useState(0);
const [calculation, setCalculation] = useState(0);
useEffect(() => {
setCalculation(() => count * 2);
}, [count]); // ← به عنوان وابستگی قرار داده شده است count
return (
<>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>+</button>
<p>Calculation: {calculation}</p>
</>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Counter />);
اگر چندین مقدار (مثل state یا props) وجود داشته باشند که روی اجرای useEffect تاثیر میگذارند، ھمهی آنھا باید در آرایهی وابستگیھا قرار بگیرند.
پاکسازی اثر (Effect Cleanup)
برخی از اثرھا (effects) نیاز به پاکسازی دارند تا از نشت حافظه (memory leak) جلوگیری شود. این شامل مواردی مانند:
- setInterval و setTimeout
- شنود رویداد (event listeners)
- اشتراکھا (subscriptions)
- و سایر اثرھایی است که ممکن است بعد از ترک کامپوننت ھمچنان فعال باقی بمانند.
با بازگرداندن یک تابع از داخل useEffect، میتوان ھنگام پاک شدن یا بروزرسانی کامپوننت، منابع را آزاد کرد.
import { useState, useEffect } from "react";
import ReactDOM from "react-dom/client";
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
let timer = setTimeout(() => {
setCount((count) => count + 1);
}, 1000);
return () => clearTimeout(timer) //پاکسازی اثر اینجا اتفاق می افتد
}, []);
return <h1>I've rendered {count} times!</h1>;
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Timer />);
ھوک useContext در ریاکت
React Context روشی برای مدیریت سراسری state در برنامه است. با استفاده از این ھوک میتوان state را به ھمراه ھوک useState بین کامپوننتھایی که به صورت عمیق تو در تو ھستند به راحتی به اشتراک گذاشت، کاری که فقط با useState به تنهایی دشوارتر خواھد بود.
مشکل چیست؟
در معماری React، بھترین روش برای مدیریت state این است که آن را در بالاترین کامپوننتی قرار دھیم که به آن نیاز دارد. حالا تصور کنید چندین کامپوننت تو در تو داریم و ھم کامپوننت بالا (والد) و ھم یکی از کامپوننتھای پایین (فرزند دور) باید به آن state دسترسی داشته باشند. اگر از Context استفاده نکنیم، مجبور خواھیم بود state را به صورت prop از ھر لایه عبور دھیم تا به مقصد برسد. این روند که به آن prop drilling گفته میشود، نه تنها کد را پیچیده و شلوغ میکند، بلکه نگهداری و توسعهی آن را نیز دشوار میسازد.
import { useState } from "react";
import ReactDOM from "react-dom/client";
function Component1() {
const [user, setUser] = useState("Jesse Hall");
return (
<>
<h1>{`Hello ${user}!`}</h1>
<Component2 user={user} />
</>
);
}
function Component2({ user }) {
return (
<>
<h1>Component 2</h1>
<Component3 user={user} />
</>
);
}
function Component3({ user }) {
return (
<>
<h1>Component 3</h1>
<Component4 user={user} />
</>
);
}
function Component4({ user }) {
return (
<>
<h1>Component 4</h1>
<Component5 user={user} />
</>
);
}
function Component5({ user }) {
return (
<>
<h1>Component 5</h1>
<h2>{`Hello ${user} again!`}</h2>
</>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Component1 />);
حتی با اینکه کامپوننتھای ٢ تا ۴ به آن state نیازی نداشتند، مجبور بودند آن را به صورت prop به کامپوننت بعدی منتقل کنند تا در نھایت به کامپوننت ۵ برسد. این دقیقاً یکی از مشکلات اصلی prop drilling است! کامپوننتھایی که نیازی به داده ندارند صرفاً به عنوان واسطه عمل میکنند، که باعث میشود کد شما:
- شلوغتر شود،
- سختتر قابل خواندن باشد،
- در بلندمدت نگهداری آن دشوارتر گردد.
راه حل، استفاده از React Context است! برای ایجاد Context ابتدا باید تابع createContext را از ریاکت ایمپورت کرده و آن را مقداردھی اولیه کنیم:
import { useState, createContext } from "react";
import ReactDOM from "react-dom/client";
const UserContext = createContext();
در مرحلهی بعد، از Context Provider استفاده میکنیم تا آن دسته از کامپوننتھایی که به این state نیاز دارند را درون آن بپوشانیم (wrap):
function Component1() {
const [user, setUser] = useState("Jesse Hall");
return (
<UserContext.Provider value={user}>
<h1>{`Hello ${user}!`}</h1>
<Component2 user={user} />
</UserContext.Provider>
);
}
با این کار، تمام کامپوننتھای داخل Provider میتوانند به مقدار به اشتراک گذاشته شده بدون نیاز به عبور دادن prop از ھر لایهی میانی دسترسی داشته باشند.
برای استفاده از Context در یک کامپوننت فرزند، باید با استفاده از ھوک useContext به آن دسترسی پیدا کنیم. ابتدا باید useContext را به ھمراه سایر موارد از ریاکت ایمپورت کنیم:
import { useState, createContext, useContext } from "react";
سپس میتوانیم در داخل کامپوننت فرزند، به مقدار Context این گونه دسترسی داشته باشیم:
function Component5() {
const user = useContext(UserContext);
return (
<>
<h1>Component 5</h1>
<h2>{`Hello ${user} again!`}</h2>
</>
);
}
به این ترتیب، مقدار user ھمان مقداری است که از طریق UserContext.Provider در سطح بالا به اشتراک گذاشته شده است. مثال کامل:
import { useState, createContext, useContext } from "react";
import ReactDOM from "react-dom/client";
const UserContext = createContext();
function Component1() {
const [user, setUser] = useState("Jesse Hall");
return (
<UserContext.Provider value={user}>
<h1>{`Hello ${user}!`}</h1>
<Component2 />
</UserContext.Provider>
);
}
function Component2() {
return (
<>
<h1>Component 2</h1>
<Component3 />
</>
);
}
function Component3() {
return (
<>
<h1>Component 3</h1>
<Component4 />
</>
);
}
function Component4() {
return (
<>
<h1>Component 4</h1>
<Component5 />
</>
);
}
function Component5() {
const user = useContext(UserContext);
return (
<>
<h1>Component 5</h1>
<h2>{`Hello ${user} again!`}</h2>
</>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Component1 />);
این باعث میشود که شمارش فقط یکبار انجام شود، درست پس از بارگذاری اولیه کامپوننت.
ھوک useRef در ریاکت
در ریاکت، زمانی که بخواھیم مقدار خاصی را در طول رندرھای مختلف حفظ کنیم، بدون اینکه باعث رندر مجدد کامپوننت شود، ھوک useRef به کمک ما میآید. این ھوک یکی از ابزارھای ریاکت است که استفاده از آن بسیار ساده است. اگر بخواھیم با استفاده از useState تعداد دفعاتی که اپلیکیشن رندر میشود را بشماریم، وارد یک حلقهی بینهایت خواھیم شد، چون useState خودش باعث رندر مجدد میشود. برای حل این مشکل از useRef استفاده میکنیم، چون تغییر مقدار آن باعث رندر نمیشود.
کاربردھای اصلی useRef:
- نگهداری مقدار قابل تغییر (mutable) بدون ایجاد رندر مجدد.
- دسترسی مستقیم به عناصر DOM
مثال: شمارش تعداد رندرھا با useRef:
import { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom/client";
function App() {
const [inputValue, setInputValue] = useState("");
const count = useRef(0);
useEffect(() => {
count.current = count.current + 1;
});
return (
<>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<h1>Render Count: {count.current}</h1>
</>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
ھوک useRef فقط یک خروجی دارد: یک شیء که تنها ویژگی آن current است.
وقتی useRef را مقداردھی اولیه میکنیم (مثلا useRef(0))، در واقع معادل این است که بنویسیم:
const count = { current: 0 };
برای دسترسی به این مقدار میتوانیم از count.current استفاده کنیم.
دسترسی به عناصر DOM با استفاده از useRef
بهطور کلی، در React توصیه میشود که مدیریت DOM را به خود ریاکت بسپارید. اما در برخی موارد خاص، میتوان از useRef برای دسترسی مستقیم به عناصر DOM بدون مشکل استفاده کرد. در React، میتوان با افزودن ویژگی ref به یک عنصر JSX (مثل <input ref={myRef}} />
) میتوان به آن عنصر در DOM مستقیماً دسترسی پیدا کرد.
مثال: استفاده از useRef برای focus:
import { useRef } from "react";
import ReactDOM from "react-dom/client";
function App() {
const inputElement = useRef();
const focusInput = () => {
inputElement.current.focus();
};
return (
<>
<input type="text" ref={inputElement} />
<button onClick={focusInput}>Focus Input</button>
</>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
کد بالا با استفاده از ھوک useRef، به عنصر <input>
دسترسی مستقیم پیدا میکند. با تعریف یک رفرنس (inputElement) و اتصال آن به input از طریق ویژگی ref، میتوان پس از رندر شدن کامپوننت، به input دسترسی داشت. تابع focusInput با فراخوانی ()inputElement.current.focus
، باعث فوکوس گرفتن input میشود. این تابع ھنگام کلیک روی دکمه اجرا میشود.
پیگیری تغییرات state با useRef
ھوک useRef را میتوان برای ردیابی مقادیر قبلی state نیز استفاده کرد. دلیل آن این است که مقادیر ذخیره شده در useRef بین رندرھا حفظ میشوند، بدون آنکه باعث رندر مجدد شوند. در ادامه از useRef برای ذخیره مقدار قبلی state استفاده میکنیم:
import { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom/client";
function App() {
const [inputValue, setInputValue] = useState("");
const previousInputValue = useRef("");
useEffect(() => {
previousInputValue.current = inputValue;
}, [inputValue]);
return (
<>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<h2>Current Value: {inputValue}</h2>
<h2>Previous Value: {previousInputValue.current}</h2>
</>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
در این مثال، از ترکیبی از useState، useEffect و useRef استفاده شده است، به این صورت که:
- useState: برای نگهداری مقدار فعلی ورودی (input) استفاده میشود.
- useRef: برای ذخیره مقدار قبلی input به کار میرود.
- در داخل useEffect، ھر بار که مقدار inputValue تغییر کند (مثلاً کاربر متنی در input وارد کند)، مقدار جدید در useRef ذخیره میشود تا برای مقایسه در رندر بعدی در دسترس باشد.
هوک useReducer در ریاکت
useReducer یک ھوک در React است که به شما اجازه میدھد درون کامپوننت خود یک reducer تعریف و استفاده کنید. ھوک useReducer مشابه ھوک useState است و امکان تعریف منطق سفارشی برای مدیریت state را فراھم میکند.
useReducer یک ابزار برای مدیریت state پیچیدهتر در کامپوننتھای React است که با استفاده از آن، به جای استفاده از چندین useState برای کنترل بخشھای مختلف داده، شما تمام منطق تغییر state را در یک تابع به نام reducer متمرکز میکنید.
ھوک useReducer دو آرگومان ورودی دریافت میکند:
useReducer(<reducer>, <initialState>)
- reducer: تابعی است که منطق تغییر state را بر اساس نوع اکشن پیادهسازی میکند.
- initialState: مقدار اولیه state است که میتواند یک مقدار ساده یا معمولاً یک شیء (object) باشد.
عملکرد useReducer به چه صورت است؟
- ابتدا یک مقدار اولیه برای state تعریف میشود؛ برای مثال یک آرایه از وظایف.
- سپس یک تابع reducer نوشته میشود. این تابع مشخص میکند که در واکنش به ھر نوع اکشن (مانند "DELETE" یا "COMPLETE")، وضعیت state چگونه باید تغییر کند.
- به جای اینکه مستقیماً مقدار state را بروزرسانی کنیم، از تابع dispatch استفاده میکنیم. این تابع یک "اکشن" (action) را به تابع reducer ارسال میکند.
- تابع reducer پس از دریافت اکشن، آن را بررسی کرده و بر اساس نوع آن، یک نسخهی جدید از state را باز میگرداند.
مثال:
import { useReducer } from "react";
import ReactDOM from "react-dom/client";
const initialTodos = [
{
id: 1,
title: "Todo 1",
complete: false,
},
{
id: 2,
title: "Todo 2",
complete: false,
},
];
const reducer = (state, action) => {
switch (action.type) {
case "COMPLETE":
return state.map((todo) => {
if (todo.id === action.id) {
return { ...todo, complete: !todo.complete };
} else {
return todo;
}
});
default:
return state;
}
};
function Todos() {
const [todos, dispatch] = useReducer(reducer, initialTodos);
const handleComplete = (todo) => {
dispatch({ type: "COMPLETE", id: todo.id });
};
return (
<>
{todos.map((todo) => (
<div key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.complete}
onChange={() => handleComplete(todo)}
/>
{todo.title}
</label>
</div>
))}
</>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Todos />);
برای نمونه، در کدی که مدیریت لیست todo را انجام میدھد:
- ھوک useReducer وظیفه نگهداری وضعیت todo ھا را بر عهده دارد.
- زمانی که کاربر روی checkbox یک وظیفه کلیک میکند، یک اکشن با نوع "COMPLETE" به dispatch ارسال میشود.
- تابع reducer، با بررسی id مربوطه، وظیفهی مورد نظر را یافته و وضعیت complete آن را برعکس میکند (true یا false). سپس لیست جدیدی از وظایف را باز میگرداند.
هوک useCallback در ریاکت
ھوک useCallback در React این امکان را فراھم میکند که تعریف یک تابع را بین رندرھا ذخیره (cache) کنیم. ھدف اصلی این ھوک این است که از باز تعریف تابع در ھر بار رندر جلوگیری کند، مگر اینکه وابستگیھای مشخص شده (dependencies) تغییر کرده باشند. این ھوک در واقع یک تابع بهینهسازی شده و ذخیره شده در حافظه (memoized callback) را بازمیگرداند.
مفهوم memoization
Memoization به این معناست که خروجی یک تابع با ورودی مشخص را ذخیره کنیم تا اگر ھمان ورودی دوباره استفاده شد، نیاز به محاسبهی مجدد نباشد. در ھوک useCallback، این تکنیک باعث میشود تابع تنها زمانی دوباره ساخته شود که یکی از وابستگیھا تغییر کند. این کار مخصوصاً زمانی کاربرد دارد که بخواھیم توابع سنگین یا پرھزینه (resource-intensive functions) را از اجرای غیرضروری در ھربار رندر جدا کنیم. به این ترتیب، این توابع تنها زمانی اجرا یا بازتعریف میشوند که واقعاً نیاز باشد.
نحوه استفاده:
const memoizedCallback = useCallback(() => {
// تابع شما
}, [dependency1, dependency2]);
useCallback چرا مهم است؟
فرض کنید در یک کامپوننت والد، تابعی مانند handleClick تعریف شده و به یک کامپوننت فرزند به عنوان prop ارسال میشود. اگر این تابع در ھر بار رندر والد دوباره تعریف شود، حتی اگر باقی props تغییر نکرده باشند، کامپوننت فرزند ھمچنان دوباره رندر خواھد شد. این موضوع برخلاف انتظاری است که معمولاً داریم؛ چرا که تصور میکنیم تا زمانی که دادهای مثل todos تغییر نکند، نیازی به رندر مجدد نیست.
اما واقعیت این است که در React، ھر بار که یک کامپوننت رندر میشود، تمام توابع درون آن از نو تعریف میشوند. این موضوع به دلیل مفھومی به نام برابری ارجاعی (referential equality) اتفاق میافتد. از آن جا که نسخهی جدید تابع از نظر ارجاعی با نسخه قبلی متفاوت است، React آن را به عنوان prop جدید میشناسد و باعث میشود کامپوننت فرزند نیز مجدداً رندر شود.
در مثال زیر تصور میشود که کامپوننت Todos فقط در صورت تغییر todos باید رندر شود:
function App() {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);
const addTodo = () => {
setTodos([...todos, "New Todo"]);
};
return (
<>
<Todos todos={todos} addTodo={addTodo} />
<button onClick={() => setCount(count + 1)}>افزایش</button>
</>
);
}
اما در این حالت، با ھر بار کلیک روی دکمه و افزایش مقدار count، کامپوننت App مجدداً رندر میشود و تابع addTodo نیز از نو ساخته میشود. در نتیجه، Todos ھم بیدلیل رندر خواھدشد.
برای جلوگیری از این اتفاق، میتوانیم با استفاده از ھوک useCallback کاری کنیم که تابع addTodo فقط یکبار ساخته شود و در حافظه باقی بماند:
const addTodo = useCallback(() => {
setTodos((t) => [...t, "New Todo"]);
}, []);
در این حالت، تا زمانی که ھیچ کدام از وابستگیھا تغییر نکنند (در این مثال ھیچ وابستگیای وجود ندارد)، نسخهی ثابت تابع حفظ میشود و از رندرھای غیرضروری جلوگیری میگردد.
ھوک useMemo در ریاکت
ھوک useMemo در React برای بهینهسازی عملکرد کامپوننتھا طراحی شده است. این ھوک با ذخیرهسازی (memoization) نتایج محاسبات سنگین، از اجرای مجدد آنھا در ھر بار رندر جلوگیری میکند، مگر آنکه وابستگیھای مشخص شده تغییر کرده باشند.
ھوکھای useMemo و useCallback شباھتھایی دارند، اما تفاوت اصلی آنھا در خروجی است:
- useMemo : یک مقدار ذخیره شده بر میگرداند.
- useCallback : یک تابع ذخیره شده بر میگرداند.
در اینجا یک مثال از یک تابع با عملکرد ضعیف آورده شده است. تابع expensiveCalculation در ھر بار رندر شدن کامپوننت اجرا میشود، حتی اگر نیازی به اجرای مجدد آن نباشد:
import { useState } from "react";
import ReactDOM from "react-dom/client";
const App = () => {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);
const calculation = expensiveCalculation(count);
const increment = () => {
setCount((c) => c + 1);
};
const addTodo = () => {
setTodos((t) => [...t, "New Todo"]);
};
return (
<div>
<div>
<h2>My Todos</h2>
{todos.map((todo, index) => {
return <p key={index}>{todo}</p>;
})}
<button onClick={addTodo}>Add Todo</button>
</div>
<hr />
<div>
Count: {count}
<button onClick={increment}>+</button>
<h2>Expensive Calculation</h2>
{calculation}
</div>
</div>
);
};
const expensiveCalculation = (num) => {
console.log("Calculating...");
for (let i = 0; i < 1000000000; i++) {
num += 1;
}
return num;
};
برای حل مشکل عملکردی بالا، میتوانیم از ھوک useMemo استفاده کنیم تا نتیجهی تابع سنگین expensiveCalculation را ذخیره کنیم. با این کار، این تابع فقط زمانی دوباره اجرا میشود که واقعاً نیاز باشد.
کافی است اجرای تابع را داخل useMemo قرار دھیم تا بتوانیم زمان اجرای آن را کنترل کنیم. این ھوک یک آرایهی وابستگی به عنوان پارامتر دوم دریافت میکند که تعیین میکند چه زمانی تابع دوباره اجرا شود. اگر مقادیر داخل این آرایه تغییر نکنند، useMemo ھمان نتیجهی قبلی را بر میگرداند و از اجرای مجدد تابع جلوگیری میشود.
در مثال زیر، تابع سنگین فقط زمانی اجرا می شود که مقدار count تغییر کند. اگر فقط یک todo جدید اضافه شود (بدون تغییر در count)، تابع دوباره اجرا نخواھد شد:
import { useState, useMemo } from "react";
import ReactDOM from "react-dom/client";
const App = () => {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);
const calculation = useMemo(() => expensiveCalculation(count), [count]);
const increment = () => {
setCount((c) => c + 1);
};
const addTodo = () => {
setTodos((t) => [...t, "New Todo"]);
};
return (
<div>
<div>
<h2>My Todos</h2>
{todos.map((todo, index) => {
return <p key={index}>{todo}</p>;
})}
<button onClick={addTodo}>Add Todo</button>
</div>
<hr />
<div>
Count: {count}
<button onClick={increment}>+</button>
<h2>Expensive Calculation</h2>
{calculation}
</div>
</div>
);
};
const expensiveCalculation = (num) => {
console.log("Calculating...");
for (let i = 0; i < 1000000000; i++) {
num += 1;
}
return num;
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
با استفاده از useMemo، تابع expensiveCalculation دیگر در ھر رندر اجرا نمیشود و فقط زمانی که count تغییر کند دوباره محاسبه خواھد شد. این باعث افزایش محسوس سرعت و عملکرد اپلیکیشن میشود.
جمع بندی
ھوکھا در ریاکت نقطهی عطفی در توسعهی کامپوننتھای تابعی به شمار میروند. آن ھا این امکان را فراھم میکنند تا قابلیتھایی مانند مدیریت وضعیت (state)، اعمال جانبی (side effects)، مدیریت حافظه و اشتراک منطق، به سادهترین و مدرنترین شکل در دل کامپوننتھا پیادهسازی شوند. استفاده از ھوکھایی ھمچون useState، useEffect، useContext، useReducer، useRef، useCallback و useMemo نه تنها به ساختاری منظمتر و خواناتر در کدنویسی منجر میشود، بلکه نقش مهمی در بھبود عملکرد اپلیکیشن ایفا میکند.
البته باید توجه داشت که اینھا تنھا بخشی از ھوکھای موجود در ریاکت ھستند. ابزارھای پیشرفتهتری مانند useLayoutEffect، useImperativeHandle، useTransition و useDeferredValue نیز برای سناریوھای خاص و بهینهسازیھای پیشرفته کاربرد دارند. ھمچنین توسعهدھندگان میتوانند با تعریف ھوکھای سفارشی (Custom Hooks)، منطقھای تکراری را به شیوهای ساخت یافتهتر باز استفاده کنند و معماری پروژه را حرفهایتر شکل دھند.
در عصر امروز توسعه رابطھای کاربری، تسلط بر ھوکھای ریاکت نه یک مزیت انتخابی، بلکه ضرورتی انکارناپذیر برای ھر توسعهدھندهی حرفهای است. یادگیری دقیق و استفادهی ھوشمندانه از این ابزارھا، مسیر رسیدن به اپلیکیشنھایی تمیز، مقیاسپذیر و قابل نگهداری را ھموار میسازد.