اگر تا پیش از این با ریاکت کار کرده باشید، احتمالاً میدانید که مدیریت وضعیت (state) یا استفاده از ویژگیهای چرخهی حیات، تنها از طریق کلاسها امکان پذیر بود. این موضوع گاهی باعث پیچیدگی و افزایش حجم کد میشد. اما از زمانی که هوکها (Hooks) در نسخه 16.8 ری اکت معرفی شدند، رویکرد توسعه کامپوننتها دستخوش تغییر مثبتی شد.
هوکها (Hooks) در واقع توابعی هستند که امکان استفاده از قابلیتهایی مانند state، اثرات جانبی (side effects) و حتی اشتراک منطق بین کامپوننتها را در قالب کامپوننتهای تابعی (Functional Components) فراهم میکنند. استفاده از هوکها باعث سادهتر شدن کد، خوانایی بیشتر و مدیریت بهتر منطق برنامه میشود. بنابراین اگر به دنبال کدی تمیزتر، ساختاری مدرنتر و تجربهای بهتر در توسعه با ریاکت هستید، هوکها (Hooks) نقطه شروع بسیار خوبی هستند.
در این مقاله، نگاهی کاربردی و قابل فهم به انواع هوکهای پایه میاندازیم، مزایای آنها را بررسی میکنیم و با مثالهایی واضح نحوه استفاده از آنها را آموزش میدهیم. سپس در مقالهی بعدی به بررسی و آموزش هوکهای پیشرفتهتر و تخصصیتری اما کمتر شناخته شده، میپردازیم.
انواع ریاکت هوکها - React Hooks
هوک (Hook) یک تابع ویژه است که به توسعه دهندگان این امکان را میدهد تا بدون استفاده از کلاسها، از امکاناتی مانند state و ویژگیهای دیگر ری اکت استفاده کنند. این باعث میشود که کد سادهتر، خواناتر و قابل مدیریتتر باشد، زیرا قابلیتها مستقیماً درون کامپوننتها اضافه میشوند.
چرا باید از هوکها استفاده کنیم؟ مزایای استفاده از هوکها
- مدیریت وضعیت (state management) راحتتر میشود.
- اپلیکیشن عملکرد بهینهتری دارد.
- منطق بین چند کامپوننت به سادگی قابل اشتراک میشود.
قوانین استفاده از هوکها
برای استفادهی صحیح از هوکها باید این سه قانون را رعایت کنید:
- هوکها فقط باید داخل کامپوننتهای تابعی ری اکت فراخوانی شوند و نمیتوان آنها را در توابع معمولی یا کلاسها استفاده کرد.
- هوکها فقط باید در سطح بالای کامپوننت فراخوانی شوند، یعنی نباید آنها را داخل شرطها (if)، حلقهها (for) یا توابع تو در تو (nested functions) قرار داد.
- هوکها نباید به صورت شرطی فراخوانی شوند، یعنی استفاده از آنها در شرایطی مانند مثال زیر مجاز نیست:
function MyComponent() {
if (isLoggedIn) {
const [user, setUser] = useState(null); // ❌
}
}رعایت این قوانین برای درست کار کردن هوکها (Hooks) و تضمین اجرای صحیح آنها توسط ری اکت ضروری است.
هوک useState
در ریاکت (React)، برای مدیریت وضعیت کامپوننتها در کامپوننتهای تابعی، از هوکی به نام useState استفاده میشود. این هوک به شما امکان میدهد تا دادههایی که ممکن است در طول زمان تغییر کنند را درون کامپوننت خود ذخیره و مدیریت کنید.
برای استفاده از این هوک، ابتدا باید آن را از پکیج ری اکت ایمپورت نمایید:
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) را در کامپوننتهای تابعی ری اکت انجام دهید. این اعمال جانبی میتوانند شامل موارد زیر باشند:
- فراخوانی 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 به تنهایی دشوارتر خواهد بود.
مشکل چیست؟
در معماری ری اکت، بهترین روش برای مدیریت 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 یک هوک در ری اکت است که به شما اجازه میدهد درون کامپوننت خود یک reducer تعریف و استفاده کنید. هوک useReducer مشابه هوک useState است و امکان تعریف منطق سفارشی برای مدیریت state را فراهم میکند.
useReducer یک ابزار برای مدیریت state پیچیدهتر در کامپوننتهای ری اکت است که با استفاده از آن، به جای استفاده از چندین 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 در ری اکت این امکان را فراهم میکند که تعریف یک تابع را بین رندرها ذخیره (cache) کنیم. هدف اصلی این هوک این است که از باز تعریف تابع در هر بار رندر جلوگیری کند، مگر اینکه وابستگیهای مشخص شده (dependencies) تغییر کرده باشند. این هوک در واقع یک تابع بهینهسازی شده و ذخیره شده در حافظه (memoized callback) را بازمیگرداند.
مفهوم memoization
Memoization به این معناست که خروجی یک تابع با ورودی مشخص را ذخیره کنیم تا اگر همان ورودی دوباره استفاده شد، نیاز به محاسبهی مجدد نباشد. در هوک useCallback، این تکنیک باعث میشود تابع تنها زمانی دوباره ساخته شود که یکی از وابستگیها تغییر کند. این کار مخصوصاً زمانی کاربرد دارد که بخواهیم توابع سنگین یا پرهزینه (resource-intensive functions) را از اجرای غیرضروری در هربار رندر جدا کنیم. به این ترتیب، این توابع تنها زمانی اجرا یا بازتعریف میشوند که واقعاً نیاز باشد.
نحوه استفاده:
const memoizedCallback = useCallback(() => {
// تابع شما
}, [dependency1, dependency2]);
useCallback چرا مهم است؟
فرض کنید در یک کامپوننت والد، تابعی مانند handleClick تعریف شده و به یک کامپوننت فرزند به عنوان prop ارسال میشود. اگر این تابع در هر بار رندر والد دوباره تعریف شود، حتی اگر باقی props تغییر نکرده باشند، کامپوننت فرزند همچنان دوباره رندر خواهد شد. این موضوع برخلاف انتظاری است که معمولاً داریم؛ چرا که تصور میکنیم تا زمانی که دادهای مثل todos تغییر نکند، نیازی به رندر مجدد نیست.
اما واقعیت این است که در ری اکت، هر بار که یک کامپوننت رندر میشود، تمام توابع درون آن از نو تعریف میشوند. این موضوع به دلیل مفهومی به نام برابری ارجاعی (referential equality) اتفاق میافتد. از آن جا که نسخهی جدید تابع از نظر ارجاعی با نسخه قبلی متفاوت است، ری اکت آن را به عنوان 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 در ری اکت برای بهینهسازی عملکرد کامپوننتها طراحی شده است. این هوک با ذخیرهسازی (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)، منطقهای تکراری را به شیوهای ساخت یافتهتر باز استفاده کنند و معماری پروژه را حرفهایتر شکل دهند.
در عصر امروز توسعه رابطهای کاربری، تسلط بر هوکهای ریاکت نه یک مزیت انتخابی، بلکه ضرورتی انکارناپذیر برای هر توسعهدهندهی حرفهای فرانت اند است. یادگیری دقیق و استفادهی هوشمندانه از این ابزارها، مسیر رسیدن به اپلیکیشنهایی تمیز، مقیاسپذیر و قابل نگهداری را هموار میسازد.
