Advance React শিখুন বাংলায় (পর্ব ৪) — useCallback শিখুন

useCallback

আগের পর্বে আমরা 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:

  1. উপরের re-render কাউন্ট ডেমো নিজে বানাও, তারপর React.memo সরিয়ে দেখো useCallback দিয়েও re-render আটকায় কিনা।
  2. একটা ইচ্ছাকৃত "stale closure" বাগ বানাও (dependency array-তে ভুলে কিছু বাদ দিয়ে), তারপর ঠিক করো।
  3. উপরের TodoApp নিজে বানাও, console-এ দেখো টাইপ করার সময় কোন TodoItem re-render হচ্ছে (হওয়া উচিত না)।
  4. useCallback সরিয়ে দিয়ে (সাধারণ ফাংশন ব্যবহার করে) একই প্রজেক্টে console log-এ পার্থক্য দেখো।

পরের পর্বে যা থাকছে

পরের পর্বে আমরা শিখব Custom Hook গভীরভাবে — React এর হাতেখড়ি পর্ব ১৭-এর বেসিকের চেয়ে এগিয়ে, একাধিক Hook মেলানো, আর একটা বাস্তব reusable data-fetching hook বানানো।

একটি মন্তব্য পোস্ট করুন

নবীনতর পূর্বতন

যোগাযোগ ফর্ম