আগের পর্বে আমরা useMemo দিয়ে ভারী গণনা ক্যাশ করা শিখেছিলাম। এই পর্বে আমরা দেখব useCallback — useMemo-এর "কাজিন" Hook, যেটা মান না, বরং ফাংশন রেফারেন্স স্থিতিশীল রাখে। এটা বোঝার জন্য আগে জানতে হবে React কীভাবে ঠিক করে child component re-render হবে কিনা।
সমস্যাটা কী? — প্রতিটা Render-এ নতুন ফাংশন
Advanced JS পর্ব ১-এর Closure মনে করো — প্রতিবার একটা ফাংশন-ভেতরে-ফাংশন লেখা হয়, একটা নতুন ফাংশন অবজেক্ট তৈরি হয়, যদিও তার কোড হুবহু একই। React component-এও একই ঘটনা ঘটে — প্রতিবার render হলে ভেতরের সব ফাংশন (event handler-সহ) নতুন করে তৈরি হয়।
function ParentComponent() {
const [count, setCount] = React.useState(0);
// প্রতিবার ParentComponent render হলে, handleClick একটা নতুন ফাংশন
function handleClick() {
console.log("ক্লিক");
}
return <ChildButton onClick={handleClick} />;
}
এখানে handleClick-এর কোড কখনো পাল্টায় না, কিন্তু প্রতিবার ParentComponent re-render হলে এটা একটা নতুন ফাংশন অবজেক্ট — মেমরিতে ভিন্ন জায়গায় রাখা। সাধারণত এটা কোনো সমস্যা না, কিন্তু React.memo-এর সাথে মিলিয়ে এটা একটা বাস্তব সমস্যা তৈরি করে (পরের পর্বে React.memo বিস্তারিত দেখব, এখানে শুধু প্রাসঙ্গিক অংশটুকু দেখব)।
React.memo-এর সাথে সংযোগ
React.memo একটা component-কে "বলে" — যদি props আগেরবারের মতোই থাকে, re-render কোরো না। কিন্তু ফাংশন props প্রতিবার "নতুন" হওয়ায়, React.memo সবসময় props "পাল্টেছে" বলে ধরে নেয়, আর optimization-টা কাজ করে না।
const ChildButton = React.memo(function ChildButton({ onClick }) {
console.log("ChildButton render হলো");
return <button onClick={onClick}>চাপো</button>;
});
function ParentComponent() {
const [count, setCount] = React.useState(0);
const [text, setText] = React.useState("");
function handleClick() {
console.log("ক্লিক");
}
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<ChildButton onClick={handleClick} />
{/* text পাল্টালে ParentComponent re-render হয়, handleClick নতুন হয়,
তাই React.memo থাকা সত্ত্বেও ChildButton re-render হয়ে যাবে! */}
</div>
);
}
useCallback দিয়ে সমাধান
useCallback ফাংশন রেফারেন্স "মনে রাখে" — শুধু dependency array পাল্টালেই নতুন ফাংশন তৈরি হয়, নাহলে আগেরটাই ফেরত দেয়।
function ParentComponent() {
const [count, setCount] = React.useState(0);
const [text, setText] = React.useState("");
const handleClick = React.useCallback(() => {
console.log("ক্লিক");
}, []); // কোনো dependency নেই, তাই সবসময় একই ফাংশন রেফারেন্স
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<ChildButton onClick={handleClick} />
{/* এখন text পাল্টালেও handleClick একই থাকে, ChildButton re-render হবে না */}
</div>
);
}
useCallback(fn, deps) আসলে useMemo(() => fn, deps)-এরই একটা শর্টকাট — পার্থক্য শুধু এটা একটা ভ্যালু না, একটা ফাংশন ক্যাশ করছে।
| Hook | কী ক্যাশ করে |
|---|---|
useMemo |
একটা গণনার ফলাফল (সংখ্যা, অ্যারে, অবজেক্ট) |
useCallback |
একটা ফাংশন নিজে |
লাইভ ডেমো — Re-render কাউন্ট পাশাপাশি
নিচে টেক্সট বক্সে টাইপ করো (এতে প্যারেন্ট re-render হয়) — লক্ষ্য করো "সাধারণ চাইল্ড"-এর re-render কাউন্ট বাড়তেই থাকে, কিন্তু "useCallback চাইল্ড"-এর কাউন্ট স্থির থাকে।
Dependency-সহ useCallback
useCallback-এ dependency দিলে, শুধু সেগুলো পাল্টালেই নতুন ফাংশন তৈরি হবে — এটাই useEffect আর useMemo-এর dependency array-এর মতোই কাজ করে।
function TodoList({ todos, onDelete }) {
const [filter, setFilter] = React.useState("all");
// filter পাল্টালেই শুধু নতুন handleFilterChange তৈরি হবে
const handleFilterChange = React.useCallback((newFilter) => {
console.log("ফিল্টার পাল্টালো:", newFilter, "আগেরটা ছিল:", filter);
setFilter(newFilter);
}, [filter]);
// ...
}
সতর্কতা — সাধারণ ভুল: useCallback-এর ভেতরের ফাংশন যদি কোনো state/prop ব্যবহার করে, সেটা অবশ্যই dependency array-তে থাকতে হবে — নাহলে ফাংশনটা "পুরনো" (stale) মান নিয়ে আটকে থাকবে।
// ❌ বাগ — count ব্যবহার হচ্ছে কিন্তু dependency-তে নেই
const handleClick = React.useCallback(() => {
console.log(count); // এই count সবসময় প্রথম render-এর মান দেখাবে (stale closure)
}, []); // count এখানে থাকা উচিত ছিল
// ✅ ঠিক — count dependency-তে আছে
const handleClick = React.useCallback(() => {
console.log(count); // এখন সবসময় সাম্প্রতিক count
}, [count]);
কখন useCallback ব্যবহার করবে না
useMemo-এর মতোই — useCallback নিজেও "ফ্রি" না। যদি চাইল্ড component React.memo দিয়ে wrap করা না থাকে, তাহলে useCallback ব্যবহার করার কোনো লাভ নেই — কারণ চাইল্ড এমনিতেই প্রতিবার re-render হবে, ফাংশন রেফারেন্স স্থিতিশীল হোক বা না হোক।
| ব্যবহার করবে | ব্যবহার করবে না |
|---|---|
ফাংশন একটা React.memo চাইল্ডে prop হিসেবে যাচ্ছে |
চাইল্ড React.memo দিয়ে wrap করা না |
ফাংশন কোনো useEffect-এর dependency |
সাধারণ inline event handler (onClick={() => ...}) |
একটা সম্পূর্ণ উদাহরণ — অপ্টিমাইজড টু-ডু লিস্ট
চলো Advanced JS সিরিজের To-do List প্রজেক্টের একটা React ভার্সন বানাই, যেখানে প্রতিটা TodoItem শুধু নিজের ডেটা পাল্টালেই re-render হবে, অন্য আইটেম টাইপ করলে না।
const TodoItem = React.memo(function TodoItem({ todo, onToggle, onDelete }) {
console.log("রেন্ডার হলো:", todo.text);
return (
<div>
<span onClick={() => onToggle(todo.id)}>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>মুছো</button>
</div>
);
});
function TodoApp() {
const [todos, setTodos] = React.useState([
{ id: 1, text: "বাজার করা", done: false },
{ id: 2, text: "কোড রিভিউ", done: false }
]);
const [newTodoText, setNewTodoText] = React.useState(""); // এটা পাল্টালে TodoItem-গুলো re-render হওয়া উচিত না
const handleToggle = React.useCallback((id) => {
setTodos((prev) =>
prev.map((t) => t.id === id ? { ...t, done: !t.done } : t)
);
}, []); // কোনো বাইরের ভ্যালু লাগছে না, তাই খালি dependency
const handleDelete = React.useCallback((id) => {
setTodos((prev) => prev.filter((t) => t.id !== id));
}, []);
return (
<div>
<input value={newTodoText} onChange={(e) => setNewTodoText(e.target.value)} />
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} onToggle={handleToggle} onDelete={handleDelete} />
))}
</div>
);
}
খেয়াল করো — setTodos((prev) => ...) ব্যবহার করা হয়েছে (React এর হাতেখড়ি পর্ব ৫-এর functional setState প্যাটার্ন), যাতে handleToggle/handleDelete-এর dependency-তে todos রাখা না লাগে — এতে ফাংশন দুটো সবসময় স্থিতিশীল থাকে, newTodoText-এ টাইপ করলে কোনো TodoItem re-render হবে না।
নিজে চেষ্টা করো
Exercise:
- উপরের re-render কাউন্ট ডেমো নিজে বানাও, তারপর
React.memoসরিয়ে দেখো useCallback দিয়েও re-render আটকায় কিনা। - একটা ইচ্ছাকৃত "stale closure" বাগ বানাও (dependency array-তে ভুলে কিছু বাদ দিয়ে), তারপর ঠিক করো।
- উপরের
TodoAppনিজে বানাও, console-এ দেখো টাইপ করার সময় কোনTodoItemre-render হচ্ছে (হওয়া উচিত না)। useCallbackসরিয়ে দিয়ে (সাধারণ ফাংশন ব্যবহার করে) একই প্রজেক্টে console log-এ পার্থক্য দেখো।
পরের পর্বে যা থাকছে
পরের পর্বে আমরা শিখব Custom Hook গভীরভাবে — React এর হাতেখড়ি পর্ব ১৭-এর বেসিকের চেয়ে এগিয়ে, একাধিক Hook মেলানো, আর একটা বাস্তব reusable data-fetching hook বানানো।