সরাসরি পর্বে যান
পর্ব ১৪ · ৯৩-এর মধ্যে · মডিউল ২

Hook-এর নিয়ম আর custom hook — useRef, useMemo আর নিজের hook

Rules of Hooks and Custom Hooks — useRef, useMemo and Your Own Hooks

Taught in Bangla, this lesson shows that React identifies hooks by call order, what really happens when the rules of hooks are broken, useRef for DOM handles and non-rendering values, useMemo and useCallback and when they help, what the React Compiler now does, useId, useDeferredValue, useTransition and use in brief, writing custom hooks that share logic but not state, useDebounce and useLocalStorage for Bazarghar, and a careful useFetch guarded by the linter.

এই পর্বে যা শিখবেন

  • Hook array-র মডেল দিয়ে বোঝা, ডাকার ক্রম কেন এত জরুরি
  • useRef, useMemo, useCallback কখন কাজের, কখন শুধু বাড়তি বোঝা
  • useId, useDeferredValue, useTransition আর React 19-এর use চেনা
  • “বাজারঘর”-এর জন্য useDebounce, useLocalStorage আর সাবধানী useFetch লেখা
  • eslint-plugin-react-hooks দিয়ে এসব ভুল আগেই ধরা

১ · React কীভাবে মনে রাখে — খাতার লাইন নম্বর

পাড়ার চায়ের দোকানের হিসাব খাতা ভাবুন। দোকানি নাম লেখেন না, শুধু লাইন নম্বর মনে রাখেন। প্রথম লাইনে রহিম ভাই, দ্বিতীয় লাইনে করিম ভাই, তৃতীয় লাইনে সুমন। প্রতিদিন তিনি একই ক্রমে খাতা মেলান। একদিন রহিম ভাই না এলে, আর দোকানি লাইন না ছেড়ে পরের জনকে প্রথম লাইনে বসালে, সবার বাকি উল্টোপাল্টা হয়ে যাবে।

পর্ব ১০-এ দেখেছেন, useState ডাকলে একটি মান পাওয়া যায়। কিন্তু একবার ভাবুন, useState(1) লেখার সময় আপনি কোনো নাম দেননি। qty নামটি তো আপনার ভেরিয়েবলের, React সেটি দেখতেই পায় না। তাহলে পরের render-এ React কীভাবে জানে কোন ডাকের কোন মান?

উত্তর: ক্রম দিয়ে। প্রতিটি component-এর জন্য React একটি খাতা রাখে। প্রথম hook পড়ে প্রথম ঘর, দ্বিতীয় hook পড়ে দ্বিতীয় ঘর। নিচের ছোট্ট মডেলে ঠিক এটুকুই আছে। এটি আসল React নয়, শুধু ধারণার একটি মডেল।

// ছোট্ট মডেল: hook-এর মান থাকে array-তে, কোনো নাম ছাড়া
const hooks = [];   // খাতা
let cursor = 0;     // এখন কোন ঘর পড়ছি

function useState(initial) {
  const slot = cursor;                  // এই ডাকের ঘর নম্বর
  if (!(slot in hooks)) hooks[slot] = initial;
  const setState = (next) => { hooks[slot] = next; };
  cursor = cursor + 1;                  // পরের ডাক, পরের ঘর
  return [hooks[slot], setState];
}
function render(component, props) {
  cursor = 0;                           // প্রতি render শুরু থেকে
  return component(props);
}
let buy;
function Card() {
  const [qty, setQty] = useState(1);
  const [note] = useState('note');
  buy = () => setQty(qty + 1);
  return { qty, note };
}
console.log('correct:', render(Card), hooks);
buy();
console.log('bought :', render(Card), hooks);

hooks.length = 0;                       // খাতা মুছে নতুন পরীক্ষা
function BadCard({ onSale }) {
  const [qty] = useState(1);
  if (onSale) useState('SALE');         // নিয়ম ভাঙা হলো
  const [note] = useState('note');
  return { qty, note };
}
console.log('sale on :', render(BadCard, { onSale: true }), hooks);
console.log('sale off:', render(BadCard, { onSale: false }), hooks);

চালালে দেখবেন:

correct: { qty: 1, note: 'note' } [ 1, 'note' ]
bought : { qty: 2, note: 'note' } [ 2, 'note' ]
sale on : { qty: 1, note: 'note' } [ 1, 'SALE', 'note' ]
sale off: { qty: 1, note: 'SALE' } [ 1, 'SALE', 'note' ]

প্রথম দুই লাইনে সব ঠিক। buy() শুধু ০ নম্বর ঘর বদলেছে। শেষ লাইনটি খেয়াল করুন। onSale বন্ধ হতেই note-এর ডাকটি ২ নম্বর ঘরের বদলে ১ নম্বর ঘরে পৌঁছেছে, তাই note-এ এসে বসেছে 'SALE'. কোনো লাইনে কোনো ভুল ছাপা হয়নি, শুধু মান ভুল। ক্রম ভাঙলে ঠিক এটাই ঘটে।

এবার দুটি render ধীরে ধীরে দেখা যাক। নিচের component-এ তিনটি hook আছে। “পরে” চেপে cursor আর খাতার ঘরগুলো খেয়াল করুন।

1function Card() {2  const [qty, setQty] = useState(1);3  const [note, setNote] = useState('');4  const box = useRef(null);5  return <button ...>{qty}</button>;6}
খাতা (hook array)
cursor
ঘর ০
ঘর ১
ঘর ২
  1. প্রথম render শুরু। খাতা খালি, cursor = 0. React এই component-কে প্রথমবার দেখছে।
  2. useState(1) ডাকা হলো। ০ নম্বর ঘর খালি, তাই সেখানে 1 বসল। cursor হলো 1.
  3. useState('') পেল ১ নম্বর ঘর। মান বসল ফাঁকা লেখা। cursor হলো 2.
  4. useRef(null) পেল ২ নম্বর ঘর। এখানে থাকে একটি ref বস্তু, এখানে তার নাম দিলাম ref-A. cursor হলো 3.
  5. ব্যবহারকারী ক্লিক করলেন। setQty(2) ০ নম্বর ঘরের মান বদলে 2 করে, আর নতুন render চায়। সরলতার জন্য মডেলে মান সরাসরি বসানো দেখাচ্ছি।
  6. দ্বিতীয় render শুরু। cursor আবার 0. Function-টি পুরোটা আবার চলছে, খাতা কিন্তু আগেরটাই।
  7. useState(1)-এর 1 এবার উপেক্ষিত। ০ নম্বর ঘরে মান আছে, তাই qty পেল 2.
  8. ১ নম্বর ঘর থেকে note পেল ফাঁকা লেখা। cursor হলো 2.
  9. ২ নম্বর ঘর থেকে একই ref-A ফিরল, নতুন বস্তু নয়। ডাকের সংখ্যা আর ক্রম আগের মতোই, তাই সব ঠিক।

এক function, প্রতিটি instance-এর আলাদা খাতা

Hook values live with each component instance, not in the function

একই Card function-এর দুটি instance, প্রতিটির নিজের hook খাতা function Card() useState · useState · useRef কোড একটাই Card: চাল Card: ডাল ঘর ০ · qty = 3 ঘর ১ · note = উপহার ঘর ২ · ref-A ঘর ০ · qty = 1 ঘর ১ · note = ফাঁকা ঘর ২ · ref-B
ছবি ১৪.১ — খাতা থাকে component-এর প্রতিটি instance-এর সাথে, function-এর ভেতরে নয়।

React-এ hook আসে ২০১৮ সালের React Conf-এর ঘোষণার পর, ফেব্রুয়ারি ২০১৯-এর 16.8 version-এ। ডিসেম্বর ২০১৮-তে Dan Abramov একটি লেখায় ব্যাখ্যা করেন, ক্রমের বদলে নামের চাবি নিলে কী কী সমস্যা হতো। এই পর্বের প্রথম ভাবনার প্রশ্নে সেই যুক্তি পাবেন।

২ · নিয়ম ভাঙলে আসলে কী হয়

Rules of Hooks মূলত দুটি কথা। এক: hook ডাকুন শুধু component বা custom hook-এর ওপরের স্তরে, কোনো early return-এর আগে। দুই: hook ডাকুন শুধু React function থেকে, সাধারণ JavaScript function থেকে নয়। অর্থাৎ শর্ত, লুপ, event handler, try/catch/finally আর useMemo বা useEffect-এর ভেতরের function-এ hook ডাকা যাবে না।

1function Card({ onSale }) {2  const [qty] = useState(1);3  if (onSale) {4    const [tag] = useState('ছাড়');5  }6  const [note] = useState('নোট');7}
এখনকার অবস্থা
onSale
cursor
note
  1. প্রথম render, ছাড় চালু। qty পেল ঘর ০, মান 1.
  2. শর্ত সত্য, তাই tag-এর hook ডাকা হলো। সে পেল ঘর ১, মান ছাড়।
  3. note পেল ঘর ২। খাতায় এখন তিনটি ঘর। React মনে রাখল, এই component-এ তিনটি hook.
  4. দ্বিতীয় render, ছাড় বন্ধ। cursor শুরু থেকে। qty ঘর ০ পড়ল, এখানে কোনো সমস্যা নেই।
  5. শর্ত মিথ্যা। tag-এর ডাক বাদ গেল, কিন্তু খাতার ঘর ১ তো রয়ে গেছে।
  6. note-এর ডাক এখন দ্বিতীয় ডাক, তাই সে পড়ল ঘর ১. তার হাতে এলো ছাড়, যা অন্য hook-এর মান। চায়ের দোকানের খাতার সেই গোলমাল।
  7. Function শেষ। React গুনে দেখল, আগে ছিল তিনটি, এবার দুটি। তাই সে থেমে গিয়ে error দেয়। মান ভুল হওয়ার চেয়ে থেমে যাওয়াই নিরাপদ।

আমাদের ছোট্ট মডেল ভুল মান দিয়ে চলতে থাকে, আসল React কিন্তু গোনে। ঠিক এই component আসল React-এ চালালে কী হয়, দেখুন। সত্যিকারের run, React 19.3.1, development build, jsdom-এ:

--- onSale true -> false
THROWN: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
--- onSale false -> true
React has detected a change in the order of Hooks called by Card. This will lead to bugs and errors if not fixed.

   Previous render            Next render
   ------------------------------------------------------
1. useState                   useState
2. useState                   useState
3. undefined                  useState
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

THROWN: Rendered more hooks than during the previous render.

দুই দিকেই পরিষ্কার error. তবে সবচেয়ে বিপজ্জনক ঘটনাটি অন্য। সংখ্যা ঠিক থাকলে React ধরতে পারে না। দুটি useState-এর ক্রম শর্তের ওপর অদলবদল করলে, একই সেটআপে সত্যিকারের ফল এটি:

render 1: a=A b=B
render 2: a=B b=A

এখানে কোনো error নেই, কোনো warning নেই, দুটি মান চুপচাপ জায়গা বদলেছে। সংখ্যা আর hook-এর ধরন একই ছিল বলে React-এর কিছু ধরার উপায় নেই। এজন্যই চলার সময়ের error-এর ভরসায় না থেকে লিন্টার চালাতে হয়, যা অংশ ৯-এ দেখবেন।

শর্ত লাগলে সমাধান সহজ। Hook সবসময় ডাকুন, শর্ত রাখুন তার ভেতরে বা ব্যবহারে। আর বড় শর্ত হলে আলাদা component বানান, কারণ প্রতিটি component নিজের খাতা পায়।

// ভুল: if (onSale) { useEffect(...) }
// ঠিক: effect সবসময় ডাকা হয়, শর্ত ভেতরে
useEffect(() => {
  if (onSale) document.title = "ছাড় চলছে";
}, [onSale]);

// ঠিক: শর্তের অংশ আলাদা component-এ
{onSale && <SaleBadge product={product} />}

আরও গভীরে: আসল React array নয়, একটি linked list ব্যবহার করে। প্রতিটি component-এর ভেতরের রেকর্ড (Fiber) তার memoizedState ঘরে প্রথম hook ধরে রাখে, আর প্রতিটি hook next দিয়ে পরেরটিকে দেখায়। পর্ব ১৫-তে Fiber দেখবেন। Development build-এ React প্রতিটি hook-এর নামের তালিকাও রাখে, তাই ওপরের টেবিলটি ছাপতে পারে। Production build-এ এই সহায়তাটুকু থাকে না, তখন error-এর জায়গায় শুধু একটি নম্বর দেখা যায়।

৩ · useRef — যে বাক্স বদলালে পর্দা নড়ে না

useState যেন দোকানের সাইনবোর্ড, বদলালে সবাই দেখে, পুরো দোকান গোছাতে হয়। useRef যেন দোকানির পকেটের চিরকুট, যখন খুশি বদলানো যায়, ক্রেতা কিছু টের পান না। useRef(0) ফেরত দেয় একটি সাধারণ object, { current: 0 }. প্রতিটি render-এ একই object ফেরত আসে, আর current বদলালে render হয় না।

এর দুটি কাজ। এক, DOM-এর আসল element ধরা, যেমন input-এ focus দেওয়া। দুই, এমন মান রাখা যা পর্দায় দেখানো হয় না, যেমন timer-এর id বা কতবার কিছু ঘটল তার গোনা। নিচের কোডে দুটোই আছে। “ঘরে যান” চেপে দেখুন, গোনা বাড়ছে কিন্তু পর্দার সংখ্যা বদলাচ্ছে না।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
function SearchBox() {
  const inputRef = useRef(null);   // DOM ধরার হাতল
  const clicks = useRef(0);        // পর্দায় না দেখানো গোনা
  const [shown, setShown] = useState(0);

  useEffect(() => {
    console.log('ref পেলাম:', inputRef.current.tagName);
  }, []);

  function goToInput() {
    inputRef.current.focus();
    clicks.current += 1;
    console.log('গোনা:', clicks.current);
  }
  return (
    <div>
      <input ref={inputRef} placeholder="পণ্য খুঁজুন" />
      <button onClick={goToInput}>ঘরে যান</button>
      <button onClick={() => setShown(clicks.current)}>সংখ্যা দেখান</button>
      <p>পর্দায় সংখ্যা: {shown}</p>
    </div>
  );
}
createRoot(document.getElementById('root')).render(<SearchBox />);
</script>

চালাতেই নিচে আসে ref পেলাম: INPUT. React element বসানোর পরে inputRef.current ভরে, তাই effect-এর ভেতরে এটি পাওয়া যায়। “ঘরে যান” দুবার চাপলে গোনা: 1, গোনা: 2 ছাপা হয়, পর্দায় তখনো 0. “সংখ্যা দেখান” চাপলে state বদলায়, render হয়, তখনই পর্দায় 2 আসে।

Render-এর সময় ref.current পড়বেন না, লিখবেনও না। শুধু প্রথমবার মান বসানোর ব্যতিক্রমটি ছাড়া। Ref পড়ুন বা লিখুন event handler আর effect-এর ভেতরে। মান পর্দায় দেখাতে হলে সেটি state হওয়া উচিত। অংশ ৯-এর লিন্টার এই ভুলটিও ধরে।

একটি React 19 খবর: function component এখন ref-কে সাধারণ prop হিসেবে নিতে পারে। আগে যে forwardRef লাগত, নতুন কোডে তা আর দরকার নেই। AI প্রায়ই এখনো forwardRef লেখে। তা ভুল নয়, তবে পুরোনো ধাঁচ।

function SearchInput({ ref, ...rest }) {
  return <input ref={ref} {...rest} />;
}

৪ · useMemo আর useCallback — কী জমা থাকে, কখন লাভ

মুদি দোকানের মাসের হিসাব ভাবুন। দুপুরে কেউ জিজ্ঞেস করলে দোকানি আগে হিসাব করা মোটটা বলে দেন। নতুন মাল না ঢুকলে আবার যোগ করার দরকার নেই। এই কৌশলের নাম memoization. useMemo খাতায় দুটি জিনিস রাখে: আগের ফল, আর যে মানগুলোর ওপর ফলটি নির্ভর করে। Render-এর সময় সে নির্ভরতার মানগুলো Object.is দিয়ে মেলায়। সব এক হলে আগের ফল, একটিও আলাদা হলে নতুন হিসাব।

নিচের ঝুড়িতে দুটি বোতাম। “রং বদলান” render ঘটায়, কিন্তু মোট টাকায় তার প্রভাব নেই। “১০ টাকা ছাড়” মোট বদলায়। render নম্বর লেখা হয় ref দিয়ে, effect-এর ভেতরে, তাই নিয়ম ভাঙে না।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const items = Array.from({ length: 1000 }, (_, i) => ({ price: (i % 50) + 1 }));

function totalOf(list, off) {
  console.log('  হিসাব চলল, ছাড়', off);
  return list.reduce((sum, p) => sum + p.price, 0) - off;
}
function Cart() {
  const [off, setOff] = useState(0);
  const [dark, setDark] = useState(false);
  const renders = useRef(0);
  const total = useMemo(() => totalOf(items, off), [off]);
  useEffect(() => {
    renders.current += 1;
    console.log('render নম্বর', renders.current);
  });
  return (
    <div style={{ background: dark ? '#ddd' : '#fff' }}>
      <p>মোট: {total} টাকা</p>
      <button onClick={() => setDark(!dark)}>রং বদলান</button>
      <button onClick={() => setOff(off + 10)}>১০ টাকা ছাড়</button>
    </div>
  );
}
createRoot(document.getElementById('root')).render(<Cart />);
</script>

“রং বদলান” দুবার, তারপর “১০ টাকা ছাড়” একবার চাপলে সত্যিকারের ফল:

  হিসাব চলল, ছাড় 0
render নম্বর 1
render নম্বর 2
render নম্বর 3
  হিসাব চলল, ছাড় 10
render নম্বর 4

চারটি render, হিসাব দুবার। রং বদলানোর দুই render-এ [off] একই ছিল, তাই হিসাব বাদ গেছে। এবার কোডে useMemo মুছে const total = totalOf(items, off); লিখে চালান। প্রতি render-এ “হিসাব চলল” দেখবেন। এখানে পর্দার ফলে কোনো পার্থক্য হবে না, শুধু কাজের পরিমাণে হবে। useMemo গতির জন্য, সঠিকতার জন্য নয়।

কখন লাভ, হিসাব করে

ধরুন একটি হিসাবে ৮ ms লাগে। একটি সেশনে component ১০০ বার render হলো, আর প্রথম render-সহ মোট ৫ বার নির্ভরতা নতুন ছিল, অর্থাৎ হিসাব সত্যিই লাগল ৫ বার।

  • useMemo ছাড়া: ১০০ × ৮ = ৮০০ ms.
  • useMemo সহ: ৫ × ৮ = ৪০ ms, সাথে ১০০ বার মেলানোর সামান্য খরচ।
  • Cache hit rate = (১০০ − ৫) ÷ ১০০ = ৯৫%. বেঁচেছে ৭৬০ ms.

এবার উল্টো দিক। নিজের কম্পিউটারে একটি filter আর reduce মেপেছিলাম। এটি Node 24-এ এক স্থানীয় run, সাধারণ ফল নয়: ১০০ পণ্যে প্রায় ০.০০৪ ms, ১০,০০০ পণ্যে প্রায় ০.১৫ ms, ১০ লাখে প্রায় ১৭ ms. অর্থাৎ বেশিরভাগ তালিকায় হিসাবটি এত সস্তা যে জমিয়ে রাখার লাভ চোখে পড়ে না। React-এর docs একটি ব্যবহারিক মাপ দেয়: console.time দিয়ে মেপে হিসাব মোট প্রায় ১ ms বা তার বেশি হলে তবেই ভাবুন। ধীর ফোনের কথাও মাথায় রাখুন, CPU throttling দিয়ে মাপুন।

useCallback আসলে কী

Component-এর ভেতরে লেখা function প্রতি render-এ নতুন হয়। useCallback সেই function-টিই জমা রাখে, তাকে চালায় না। useCallback(fn, deps) হুবহু useMemo(() => fn, deps)-এর সমান। নিজে থেকে এটি কিছুই দ্রুত করে না। লাভ হয় তখনই, যখন function-টি কোথাও মিলিয়ে দেখা হয়।

পরিস্থিতিহাতের memo লাগবে?
মাপা হিসাব ধীর, নির্ভরতা কম বদলায়হ্যাঁ, useMemo
মান বা function যাচ্ছে memo করা child-এহ্যাঁ, নইলে memo ব্যর্থ (পর্ব ২৩)
মানটি অন্য hook-এর dependencyপ্রায়ই হ্যাঁ, অথবা object-টি effect-এর ভেতরে নিন
ছোট তালিকা, সরল যোগনা
সাধারণ <button onClick>-এ functionনা, built-in element কিছু মেলায় না
“মান কখনো নতুন করে হিসাব হওয়া চলবে না”না, useRef বা useState নিন। React জমা মান ফেলে দিতে পারে।

৫ · React Compiler — হাতের memo কি বিদায়?

React Compiler একটি build-সময়ের টুল। আপনার component পড়ে সে নিজেই ঠিক করে কী জমা রাখবে, আর সেই কোড বসিয়ে দেয়। ৭ অক্টোবর ২০২৫-এ এর প্রথম stable version 1.0 আসে। React 17 ও তার পরের version-এ চলে। লেখার সময়ে babel-plugin-react-compiler-এর latest হলো 1.0.0. সাধারণত এটি build-এ আলাদা করে যোগ করতে হয়।

কিন্তু সে আসলে কী করে? পড়ে নয়, চালিয়ে দেখা যাক। সত্যিকারের run, compiler 1.0.0, Babel 7.29.7:

export function CartTotal({ items, off }) {
  const total = items.reduce((sum, p) => sum + p.price, 0) - off;
  return <p>মোট: {total} টাকা</p>;
}
import { c as _c } from "react/compiler-runtime";
export function CartTotal(t0) {
  const $ = _c(2);
  const { items, off } = t0;
  const total = items.reduce(_temp, 0) - off;
  let t1;
  if ($[0] !== total) {
    t1 = <p>মোট: {total} টাকা</p>;
    $[0] = total;
    $[1] = t1;
  } else {
    t1 = $[1];
  }
  return t1;
}

দুটি জিনিস লক্ষ করুন। প্রথমত, $ একটি জমা রাখার array, যেটি নিজেও একটি hook থেকে আসে। অর্থাৎ compiler-ও সেই খাতার ধারণাই ব্যবহার করে। দ্বিতীয়ত, রঙিন লাইনটি। reduce-এর হিসাব জমা হয়নি, প্রতি render-এ চলবে। জমা হয়েছে শুধু JSX. কারণ ফলটি একটি সংখ্যা। একই compiler-এ items.filter(...) চালিয়ে দেখলাম, সেখানে array-টি items আর off-এর ওপর জমা হয়। তাই “compiler আছে মানে সব ধীর হিসাব বাঁচল”, এই কথা অন্তত এই run-এ সত্য নয়।

কী করবেন

React দলের পরামর্শ: নতুন কোডে memoization compiler-এর ওপর ছাড়ুন, আর যেখানে ঠিক কী জমা থাকবে তা নিয়ন্ত্রণ করতে হয়, সেখানে useMemo বা useCallback রাখুন। পুরোনো কোড থেকে হাতের memo সরাতে হলে পরীক্ষা করে সরান। আর কোনো হিসাব সত্যিই ধীর হলে compile করা কোড খুলে দেখুন। যে কোড নিয়ম ভাঙে, compiler সেটি বাদ দিয়ে বাকি অ্যাপ optimize করে, তাই লিন্টারের কথা শোনা এখানেও লাভের।

৬ · আরও চারটি hook, সংক্ষেপে — useId, useDeferredValue, useTransition, use

useId একটি স্থায়ী, অনন্য id দেয়। মূল কাজ label আর input বা aria-describedby জোড়া লাগানো। হাতে id="qty" লিখলে একই component দুবার বসালে id দুটি মিলে যায়। সত্যিকারের run-এ React 19.3.1 দুটি ডাকে দিয়েছে _r_0_ আর _r_1_, কিন্তু এই আকৃতির ওপর ভরসা করবেন না, version ভেদে বদলেছে। List-এর key হিসেবে এটি ব্যবহার করবেন না, key আসে ডেটা থেকে (পর্ব ১১)।

function QtyField() {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>পরিমাণ</label>
      <input id={id} type="number" />
    </>
  );
}

useDeferredValue আর useTransition একই সমস্যার দুই হাতল। সমস্যাটি হলো, ভারী render টাইপ করা আটকে দেয়। সমাধান: জরুরি কাজ (অক্ষর পর্দায় বসা) আগে, ভারী কাজ পরে, আর নতুন অক্ষর এলে ভারী কাজ থামিয়ে আবার শুরু।

useDeferredValueuseTransitionDebounce
কী নেয়একটি মানset ডাকার functionএকটি মান, আর সময়
কখন বেছে নেবেনset হাতে নেই, মান prop থেকে আসেset আপনার হাতে, isPending দেখাতে চানকাজটি render নয়, যেমন network
নির্দিষ্ট দেরি?না, যন্ত্রের গতি অনুযায়ীনাহ্যাঁ, যেমন 300 ms
Request কমায়?নানাহ্যাঁ
const [text, setText] = useState("");      // input: জরুরি
const deferred = useDeferredValue(text);   // তালিকা: পরে
// <SlowList query={deferred} />  SlowList-কে memo করতে হয়

const [isPending, startTransition] = useTransition();
function selectTab(next) {
  startTransition(() => setTab(next));     // ভারী ট্যাব বদল
}

একটি ফাঁদ: controlled input-এর state transition-এর ভেতরে বদলানো যায় না, কারণ টাইপ করা তাৎক্ষণিক হতে হয়। Input-এর জন্য এক state, ভারী অংশের জন্য আরেকটি রাখুন, অথবা useDeferredValue নিন। memo ছাড়া useDeferredValue-এর লাভ পাওয়া যায় না, কারণ parent-এর render-এ child এমনিই render হয়। এই hook দুটি React 18-এ (মার্চ ২০২২) আসে, বিস্তারিত পর্ব ২৩-এ।

use এসেছে React 19-এ (ডিসেম্বর ২০২৪)। এটি render-এর সময় একটি Promise বা context-এর মান পড়ে। React-এর docs স্পষ্ট বলে, নামে use হলেও এটি hook নয়, তাই একে if আর লুপের ভেতরে ডাকা যায়। Rules of Hooks-এর এটিই ব্যতিক্রম। তবে এটি try/catch-এ ডাকা যায় না। Promise pending থাকলে component থেমে নিকটতম Suspense-এর fallback দেখায়, reject হলে error boundary ধরে।

function Reviews({ reviewsPromise, show }) {
  if (!show) return null;
  const reviews = use(reviewsPromise);     // শর্তের পরেও চলে
  return <ul>{reviews.map((r) => <li key={r.id}>{r.text}</li>)}</ul>;
}
// ভুল: use(fetch("/api/reviews")), প্রতি render-এ নতুন Promise

Promise-টি অবশ্যই cache করা হতে হবে, প্রতি render-এ একই object. Render-এর ভেতরে নতুন Promise বানালে fallback বারবার ঘুরতে পারে, আসল content না-ও আসতে পারে। Promise সাধারণত আসে event handler, route loader বা Server Component থেকে (পর্ব ৫৩)।

৭ · নিজের hook লেখা — logic ভাগ, state নয়

পর্ব ১৩-এ effect দিয়ে অনেক কিছু করেছেন। একই effect-এর ধাঁচ তিন জায়গায় দেখলে সেটি একটি function-এ তুলে নেওয়া যায়। যে function নিজে hook ডাকে, তার নাম use দিয়ে শুরু করলেই সে custom hook. আলাদা কোনো syntax নেই।

দর্জির মাপের ছক ভাবুন। ছকটি সবার জন্য এক, কিন্তু প্রতিটি ক্রেতার মাপ আলাদা খাতায় ওঠে। Custom hook হলো সেই ছক। ছক ভাগ করলে দুজনের মাপ এক হয়ে যায় না। ঠিক তেমনি custom hook logic ভাগ করে, state ভাগ করে না।

Custom hook চলার সময় যেন আলাদা কিছু নয়। তার ভেতরের hook ডাকগুলো সরাসরি যে component ডেকেছে তার খাতায় ঘর পায়। নিচের stepper-এ useCounter-কে দুবার ডেকে দেখুন।

1function useCounter(start) {2  const [n, setN] = useState(start);3  return [n, () => setN(n + 1)];4}5function Cart() {6  const [a, incA] = useCounter(1);7  const [b, incB] = useCounter(5);8  // বোতাম: onClick={incA}9}
খাতাগুলো
Cart ঘর ০
Cart ঘর ১
Wishlist ঘর ০
  1. Cart প্রথম render. লাইন ৬ useCounter(1) ডাকল। এখনো কোনো ঘর তৈরি হয়নি।
  2. useCounter-এর ভেতরের useState চলল। ঘরটি হলো Cart-এর ঘর ০, মান 1. useCounter-এর নিজের কোনো খাতা নেই।
  3. লাইন ৭ আবার একই function ডাকল, ভেতরের useState পেল পরের ঘর। Cart-এর ঘর ১, মান 5. একই কোড, আলাদা state.
  4. incA চাপা হলো। শুধু ঘর ০ হলো 2. ঘর ১ কেউ ছোঁয়নি, কারণ incA-র setN ঘর ০-র সাথেই বাঁধা।
  5. আবার render. ডাকের ক্রম আগের মতো, তাই a পেল 2, b পেল 5. Hook-এর নিয়ম custom hook-এর ভেতরেও খাটে।
  6. অন্য component Wishlist useCounter(9) ডাকল। সে নিজের খাতায় ঘর ০ পেল, মান 9. Cart-এর সাথে কোনো সম্পর্ক নেই। দুই component-এ মান ভাগ করতে চাইলে custom hook যথেষ্ট নয়।
ভালো custom hook-এর নিয়ম
  • নাম: use, তারপর বড় হাতের অক্ষর, যেমন useDebounce. ভেতরে কোনো hook না থাকলে সেটি সাধারণ function, use লিখবেন না।
  • কাজ দিয়ে নাম: useOnlineStatus ভালো, useMount বা useEffectOnce-এর মতো জীবনচক্রের নাম ভালো নয়।
  • সব তুলে ফেলবেন না: React docs নিজেই বলে, সামান্য পুনরাবৃত্তি ঠিক আছে।
  • State ভাগ করতে: ওপরে তুলুন (পর্ব ১৬), context নিন (পর্ব ১৭), অথবা store (পর্ব ১৮)।
  • পরীক্ষা: পরে Testing Library-র renderHook দিয়ে এদের আলাদা করে test করা যায় (পর্ব ৭৩ ও ৭৪)।

৮ · বাজারঘরের দুটি hook — useDebounce আর useLocalStorage

বাজারঘরের খোঁজার ঘরে প্রতিটি অক্ষরে server-এ request গেলে অপচয়। Debounce মানে টাইপ থামার কিছুক্ষণ পরে তবে কাজ করা। লিফটের দরজার মতো: কেউ ঢুকলেই দরজা বন্ধের সময় আবার গোড়া থেকে শুরু। শেষজন ঢোকার পরেই দরজা বন্ধ হয়। পর্ব ১৩-এর cleanup এখানে আসল কারিগর।

1function useDebounce(value, delay) {2  const [late, setLate] = useState(value);3  useEffect(() => {4    const id = setTimeout(() => setLate(value), delay);5    return () => clearTimeout(id);6  }, [value, delay]);7  return late;8}
delay = 300 ms
সময়
value
চালু timer
late
request
  1. 0 ms, প্রথম অক্ষর। value হলো চ। Effect timer A বসাল, যা ৩০০ ms-এ বাজবে।
  2. 120 ms, দ্বিতীয় অক্ষর। value বদলাল, তাই আগের effect-এর cleanup চলল। Timer A বাতিল, নতুন timer B বাজবে 120 + 300 = 420 ms-এ।
  3. 240 ms, তৃতীয় অক্ষর। Timer B-ও বাতিল। নতুন timer C বাজবে 240 + 300 = 540 ms-এ।
  4. 540 ms. আর কোনো অক্ষর আসেনি, তাই timer C বাজল। setLate('চাল') নতুন render ঘটাল।
  5. Request গেল। late বদলাতে যে effect-টি তার ওপর নির্ভর করে, সেটি চলল। তিন অক্ষরে একটি মাত্র request, শেষ অক্ষরের ৩০০ ms পরে।

হিসাব: ১০ অক্ষর, প্রতিটির ফাঁক ১২০ ms, delay ৩০০ ms. ফাঁক delay-এর চেয়ে ছোট, তাই প্রতিটি timer বাতিল হয়, শুধু শেষটি বাজে। Request ১০টির বদলে ১টি, যা যায় ৯ × ১২০ + ৩০০ = ১৩৮০ ms-এ। খরচ একটাই: শেষ অক্ষরের পরে ৩০০ ms অপেক্ষা। Delay বেশি বাড়ালে খোঁজা ধীর লাগে।

এবার মনে রাখার পালা। ব্যবহারকারী শেষ খোঁজা ফিরে এলেও দেখতে চান, তাই ব্রাউজারের localStorage. কিন্তু এই পাতার preview একটি sandbox-করা iframe-এ চলে, যেখানে localStorage ছুঁলেই error হয়। বাস্তবেও এমন হয়: কিছু প্রাইভেট মোড বা কড়া সেটিংসে storage বন্ধ বা ভরা থাকতে পারে। তাই কোডটি আগে পরীক্ষা করে, না পেলে memory-তে রাখে। নিচের ঘরে টাইপ করে দেখুন।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
let store;
try { localStorage.setItem('t', '1'); localStorage.removeItem('t'); store = localStorage; }
catch { const m = new Map(); console.log('localStorage বন্ধ, memory নিলাম');
  store = { getItem: (k) => m.get(k) ?? null, setItem: (k, v) => m.set(k, v) }; }
function useStoredText(key, first) {
  const [text, setText] = useState(() => store.getItem(key) ?? first);
  useEffect(() => { store.setItem(key, text); }, [key, text]);
  return [text, setText];
}
function useDebounce(value, delay) {
  const [late, setLate] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setLate(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);
  return late;
}
const products = ['চাল', 'ডাল', 'চা পাতা', 'চিনি', 'আটা'];
function Search() {
  const [q, setQ] = useStoredText('bazar-q', 'চ');
  const late = useDebounce(q, 300);
  useEffect(() => { console.log('খোঁজ চলল:', late); }, [late]);
  return <><input value={q} onChange={(e) => setQ(e.target.value)} />
    <p>{products.filter((p) => p.includes(late)).join(', ') || 'কিছু নেই'}</p></>;
}
createRoot(document.getElementById('root')).render(<Search />);
</script>

চালাতেই সত্যিকারের ফল:

localStorage বন্ধ, memory নিলাম
খোঁজ চলল: চ

প্রথম লাইন বলছে, এই sandbox-এ localStorage সত্যিই আটকানো। store-এর আকৃতি একই রাখায় বাকি কোড জানেই না কোনটি চলছে। এবার ঘরে দ্রুত কয়েকটি অক্ষর লিখুন। প্রতি অক্ষরে নয়, থামার প্রায় ৩০০ ms পরে একবার “খোঁজ চলল” ছাপা হবে। useDebounce আর useStoredText দুটোই সাধারণ function, যেকোনো component-এ বসানো যায়।

বাজারঘরের আসল কোডে শুধু লেখা নয়, object-ও রাখতে হয়। তাই hook-টি JSON সামলায়, আর storage বন্ধ বা জমা মান নষ্ট থাকলে শুরুর মানে ফিরে যায়।

function readJson(key, fallback) {
  try {
    const raw = window.localStorage.getItem(key);
    return raw === null ? fallback : JSON.parse(raw);
  } catch {
    return fallback;   // storage বন্ধ, অথবা JSON নষ্ট
  }
}

export function useLocalStorage(key, initial) {
  const [value, setValue] = useState(() => readJson(key, initial));
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch {
      // storage ভরা বা বন্ধ: মান তবু memory-তে থাকে
    }
  }, [key, value]);
  return [value, setValue];
}

এই hook-এর তিনটি সীমা জেনে রাখুন। এক: SSR-এ window নেই, আর server-এর HTML ও browser-এর প্রথম render না মিললে hydration সমস্যা হয় (পর্ব ৫৬)। ওপরের try error আটকায়, mismatch আটকায় না। দুই: key বদলালে value নতুন করে পড়া হয় না, তাই key-কে স্থির ধরে নিয়েছি। তিন: অন্য ট্যাবের বদল এটি দেখে না। useSyncExternalStore দিয়ে এর ভালো সমাধান হয়। আর login token বা গোপন তথ্য localStorage-এ রাখবেন না, কারণ XSS হলে script তা পড়তে পারে।

৯ · useFetch সাবধানে, আর লিন্টার দিয়ে পাহারা

Data আনার hook সবচেয়ে বেশি লেখা হয়, ভুলও হয় সবচেয়ে বেশি। ব্যবহারকারী “চ” লিখলেন, request গেল। তারপর “চা” লিখলেন, আরেকটি গেল। দ্বিতীয় উত্তর আগে এলো, প্রথমটি পরে। পরে যেটি এলো, সেটিই পর্দায় বসল। এটি race condition. দুটি নকল server-উত্তর দিয়ে সত্যিকারের পরীক্ষা (React 19.3.1, jsdom): c-এর উত্তর ধীর, ch-এর দ্রুত।

naive
  server answered q=ch
  server answered q=c
  screen: box q=ch, data=results for c
safe
  aborted q=c
  server answered q=ch
  screen: box q=ch, data=results for ch

সাধারণ সংস্করণে খোঁজার ঘরে ch, অথচ ফল c-এর। নিরাপদ সংস্করণ নিচে। AbortController পুরোনো request বাতিল করে। আর ফলের সাথে তার url রাখা হয়েছে, তাই loading আলাদা state নয়, হিসাব থেকে আসে।

export function useFetch(url) {
  const [result, setResult] = useState({ url: null, data: null, error: null });

  useEffect(() => {
    const controller = new AbortController();
    fetch(url, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then((data) => setResult({ url, data, error: null }))
      .catch((error) => {
        if (error.name !== "AbortError") setResult({ url, data: null, error });
      });
    return () => controller.abort();          // cleanup: পুরোনো request বাতিল
  }, [url]);

  const loading = result.url !== url;         // এই url-এর ফল এখনো আসেনি
  return {
    loading,
    data: loading ? null : result.data,
    error: loading ? null : result.error,
  };
}

তবু এটিকে পূর্ণ সমাধান ভাববেন না। এতে cache নেই, একই ডেটার দুই component দুবার request পাঠায়, retry নেই। বড় অ্যাপে TanStack Query (পর্ব ১৯), router loader (পর্ব ২০) বা framework-এর data layer (পর্ব ৫৩) ব্যবহার করা হয়। React docs-ও অ্যাপের ভেতরে হাতে লেখা fetching-এর বদলে এমন পথই পছন্দ করে।

eslint-plugin-react-hooks, সত্যিকারের run

পর্ব ০৫-এ ESLint-এর পরিবেশ বানিয়েছেন। এবার তাতে React hook-এর নিয়ম যোগ করি। লেখার সময়ে version: ESLint 10.14.0, eslint-plugin-react-hooks 7.1.1, Node 24.15.0. এই plugin-এর recommended দলে এখন ১৬টি rule, যার বেশিরভাগ React Compiler-এর পরীক্ষা থেকে আসে। Compiler ব্যবহার না করলেও এগুলো কাজ করে।

npm i -D eslint eslint-plugin-react-hooks globals
import reactHooks from "eslint-plugin-react-hooks";
import globals from "globals";

export default [
  {
    files: ["src/**/*.jsx"],
    languageOptions: {
      ecmaVersion: "latest",
      sourceType: "module",
      globals: globals.browser,
      parserOptions: { ecmaFeatures: { jsx: true } },
    },
  },
  reactHooks.configs.flat.recommended,
];

এবার ইচ্ছা করে ভুলে ভরা একটি component:

import { useEffect, useRef, useState } from "react";

// ইচ্ছা করে চারটি ভুল: লিন্টার কী বলে দেখার জন্য
export function ProductCard({ product, onSale }) {
  if (!product) {
    return null;
  }
  const [qty, setQty] = useState(1);

  if (onSale) {
    useEffect(() => {
      document.title = "ছাড় চলছে";
    }, []);
  }

  const renders = useRef(0);
  renders.current = renders.current + 1;

  useEffect(() => {
    console.log(product.name, qty);
  }, []);

  return (
    <button onClick={() => setQty(qty + 1)}>
      {product.name}: {qty}
    </button>
  );
}

সত্যিকারের run. লম্বা বার্তা ছোট করেছি, “…” মানে বাদ দেওয়া অংশ।

$ npx eslint src

~/bazarghar/src/ProductCard.jsx
   8:25  error    React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render   react-hooks/rules-of-hooks
  11:5   error    React Hook "useEffect" is called conditionally. … Did you accidentally call a React Hook after an early return?   react-hooks/rules-of-hooks
  16:19  error    React Hook "useRef" is called conditionally. …   react-hooks/rules-of-hooks
  17:3   error    Error: Cannot access refs during render … Cannot update ref during render   react-hooks/refs
  17:21  error    Error: Cannot access refs during render … Cannot access ref value during render   react-hooks/refs
  19:3   error    React Hook "useEffect" is called conditionally. …   react-hooks/rules-of-hooks
  21:6   warning  React Hook useEffect has missing dependencies: 'product.name' and 'qty'. Either include them or remove the dependency array   react-hooks/exhaustive-deps

✖ 7 problems (6 errors, 1 warning)

প্রথম error-টি লক্ষ করুন। useState-এর আগে কোনো if নেই, কিন্তু একটি early return আছে। লিন্টার বুঝেছে, ওই return-এর কারণে পরের সব hook শর্তাধীন। ওপরের “চুপচাপ অদলবদল” ভুলটিও এই নিয়মেই ধরা পড়ে। সব hook ওপরে তুলে, শর্ত ভেতরে নিয়ে, ref effect-এ লিখে, dependency পূরণ করে আবার চালালে কোনো problem থাকে না, exit code 0.

আর সাধারণ useFetch? প্রচলিত ধাঁচে effect-এর শুরুতে setLoading(true) লিখলে এই version-এর plugin সরাসরি error দেয়। সত্যিকারের run:

~/bazarghar/src/NaiveFetch.jsx
  8:5  error  Error: Calling setState synchronously within an effect can trigger cascading renders
…
>  8 |     setLoading(true);
     |     ^^^^^^^^^^ Avoid calling setState() directly within an effect
  react-hooks/set-state-in-effect

✖ 1 problem (1 error, 0 warnings)

এজন্যই ওপরের useFetch-এ loading হিসাব করে বের করা হয়েছে। সেই ফাইল, useDebounce আর useLocalStorage, তিনটিতেই একই লিন্টার 0 problem দিয়েছে। exhaustive-deps সতর্কবার্তা মুছতে // eslint-disable লিখবেন না। সেটি বাগ ঢেকে রাখে। পড়ুন, তারপর dependency ঠিক করুন, অথবা React 19.2-তে আসা useEffectEvent কাজে লাগে কি না ভাবুন।

১০ · AI-এর যুগে — hook-এর কোড চলছে মানেই ঠিক নয়

Hook-এর ভুলের বিশেষত্ব হলো, সাধারণ পরীক্ষায় তা দেখা যায় না। দ্রুত নেটওয়ার্কে race ধরা পড়ে না, আর ছোট ডেটায় বাড়তি render চোখে পড়ে না। AI-এর কোড তাই আরও সাবধানে দেখতে হয়।

AI-এর ভুলকেন সমস্যাকীভাবে ধরবেন
useFetch-এ cleanup নেইপুরোনো উত্তর নতুনের ওপর বসেDevTools-এ Slow 4G দিয়ে দ্রুত টাইপ
সব জায়গায় useMemo, useCallbackকোড ভারী, লাভ অজানাProfiler দিয়ে মাপা, compiler ব্যবহার
Dependency সরিয়ে eslint-disableStale মান থেকে বাগDiff-এ eslint-disable খোঁজা
Render-এ ref.current লেখাআচরণ অনিশ্চিতলিন্টার react-hooks/refs
Custom hook দিয়ে state “ভাগ”দুই component-এর মান আলাদাই থাকেদুই জায়গায় বসিয়ে পরীক্ষা
অচেনা package-এর hook, যেমন বানানো use-debounce-proPackage না-ও থাকতে পারে, বা নিরাপদ না-ও হতে পারেnpm view দিয়ে যাচাই, ছোট হলে নিজে লেখা
Token localStorage-এXSS হলে চুরিনিরাপত্তা পর্ব ৪৫-৪৬ মেলানো

AI-কে কাজ দেওয়ার সময় version আর পরীক্ষার শর্ত বলে দিন:

আমি React 19 আর eslint-plugin-react-hooks 7 ব্যবহার করি। বাজারঘর নামের দোকানের খোঁজার ঘরের জন্য একটি useProductSearch custom hook লিখে দাও। শর্ত: ১. শুধু React-এর নিজের hook, কোনো নতুন npm package নয়। ২. ৩০০ ms debounce, আর পুরোনো request AbortController দিয়ে বাতিল। ৩. effect-এর ভেতরে সরাসরি setState নয়, কোনো eslint-disable নয়। ৪. কোন কোন পরিস্থিতিতে এই hook ভাঙতে পারে, তার তালিকা দাও। ৫. কোন React docs পাতা দেখে এই ধরন নিয়েছ, বলো। না জানলে অনুমান না করে বলো জানো না।

AI-এর লেখা hook হাতে পেলে — যাচাইয়ের চেকলিস্ট

পরীক্ষা ও ইন্টারভিউতে: Hook শর্তের ভেতরে নয় কেন? React ডাকার ক্রম দিয়ে hook চেনে, ক্রম বদলালে ভুল ঘর পড়া হয়। useRef আর useState-এর পার্থক্য? Ref বদলালে render হয় না, state বদলালে হয়। useMemo আর useCallback? প্রথমটি ফল জমায়, দ্বিতীয়টি function; useCallback(fn, d) মানে useMemo(() => fn, d). Custom hook কি state ভাগ করে? না, logic ভাগ করে; প্রতিটি ডাকের state আলাদা। Debounce আর useDeferredValue? Debounce নির্দিষ্ট সময় অপেক্ষা করে আর request কমায়; deferred value render-কে থামানোর যোগ্য করে, request কমায় না।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১React hook-কে নাম দিয়ে চিনল না কেন? useState('qty') লিখলেই তো শর্তের সমস্যা মিটে যেত।

এই প্রশ্নটি ২০১৮ সালে hook ঘোষণার পরে বহু মানুষ তুলেছিলেন। ডিসেম্বর ২০১৮-তে Dan Abramov “Why Do Hooks Rely on Call Order?” লেখায় নামের চাবির সমস্যাগুলো একে একে দেখান।

  • নামের সংঘাত: Custom hook-এর ভেতরে কেউ 'name' চাবি দিয়ে state যোগ করলেন। যে component আগে থেকেই ওই চাবি ব্যবহার করছে, সে ভেঙে যাবে। অর্থাৎ library-র ভেতরের একটি ছোট বদল বাইরের কোড ভাঙতে পারে।
  • একই hook দুবার ডাকা যায় না: useFormInput() সবসময় একই চাবি দিলে নামের ঘর আর ঠিকানার ঘর একই state-এ গিয়ে পড়ে। ক্রমের ব্যবস্থায় প্রতিটি ডাক নিজেই আলাদা।
  • হীরার সমস্যা: দুটি custom hook যদি একই নিচের hook ব্যবহার করে, নাম-ভিত্তিক ব্যবস্থায় তাদের state মিশে যায়। Function ডাক একটি গাছ তৈরি করে, তাই ক্রমের ব্যবস্থায় এই সংঘাত হয় না।

Symbol দিয়েও একই hook দুবার ডাকার সমস্যা মেটে না, কারণ চাবি একটিই। অর্থাৎ ক্রম বেছে নেওয়া হয়েছে custom hook-কে জুড়ে জুড়ে ব্যবহারের যোগ্য করতে। দাম হিসেবে একটি অস্বাভাবিক নিয়ম মানতে হয়, আর সেই নিয়ম পাহারার জন্য লিন্টার লাগে। আগে class-এর যুগে mixin-এ ঠিক এই নামের সংঘাতের অভিজ্ঞতা হয়েছিল, যার সমালোচনা ২০১৬ সালের “Mixins Considered Harmful” লেখায় আছে।

মূল উপলব্ধি: যে নিয়মটি অদ্ভুত লাগে, সেটি প্রায়ই অন্য একটি বড় ক্ষমতার দাম। Rules of Hooks হলো জুড়ে নেওয়ার ক্ষমতার (composition) দাম।

প্র ০২Ref-এ রাখলে render বাঁচে। তাহলে সব state ref-এ রেখে দরকারমতো হাতে পর্দা বদলালে কেমন হয়?

লোভনীয় চিন্তা, আর ঠিক এভাবেই পুরোনো jQuery-যুগের কোড চলত। সমস্যা তিনটি স্তরে।

এক, পর্দা আর ডেটার মিল। পর্ব ০৭-এ দেখেছেন, React-এর মূল প্রতিশ্রুতি হলো UI ডেটার ফল। মান ref-এ রাখলে React জানেই না তা বদলেছে। আপনাকে নিজে খুঁজে প্রতিটি জায়গা বদলাতে হবে, আর একটি বাদ পড়লেই পর্দা পুরোনো থাকবে। Declarative ধারণাটিই হারিয়ে যায়।

দুই, render বিশুদ্ধ হওয়ার নিয়ম। React 18-এর concurrent rendering-এ একটি render শুরু হয়ে থেমে যেতে, বাতিল হতে বা আবার চলতে পারে। Function যদি শুধু props আর state-এর ওপর নির্ভর করে, এতে সমস্যা নেই। কিন্তু render-এর ভেতরে পরিবর্তনশীল ref পড়লে একই component-এর দুই অংশ দুই সময়ের মান দেখতে পারে। Transition আর useDeferredValue-এর মতো সুবিধাও ref-এর মানের ক্ষেত্রে কাজ করে না, কারণ ref-এ কোনো “পুরোনো আর নতুন সংস্করণ” থাকে না।

তিন, compiler. React Compiler ধরে নেয় render বিশুদ্ধ। Ref পর্দার মান ঠিক করলে তার হিসাব ভুল হয়, তাই লিন্টারের refs rule এই ধরন আটকায়।

তবে ref ভুল টুল নয়। টানা স্ক্রলের অবস্থান, timer-এর id, অ্যানিমেশনের প্রতি ফ্রেমের মান, বাইরের লাইব্রেরির instance, এসব ref-এরই জায়গা। প্রশ্নটি সহজ: এই মান কি পর্দার চেহারা ঠিক করে? করলে state, না করলে ref.

মূল উপলব্ধি: Render বাঁচানো লক্ষ্য নয়, লক্ষ্য হলো পর্দা আর ডেটার মিল। আগে ঠিকভাবে লিখুন, তারপর মেপে গতি বাড়ান।

প্র ০৩React Compiler stable হয়েছে। এখন কি সব useMemo মুছে ফেলা উচিত?

না, একবারে নয়। এখানে তিনটি আলাদা প্রশ্ন আছে।

নতুন কোড: ২০২৫ সালের ১.০ ঘোষণায় React দল বলেছে, নতুন কোডে memoization compiler-এর ওপর ছাড়ুন, আর সূক্ষ্ম নিয়ন্ত্রণ লাগলে useMemo, useCallback ব্যবহার করুন। সাধারণ উদাহরণ হলো effect-এর dependency হিসেবে ব্যবহৃত মান, যেখানে আপনি নিশ্চিত হতে চান কখন effect চলবে।

পুরোনো কোড: একই ঘোষণা বলে, হাতের memo সরালে compile করা ফল বদলাতে পারে। তাই হয় রেখে দিন, নয় ভালো করে পরীক্ষা করে সরান। পরীক্ষা ছাড়া একসাথে হাজার লাইন মোছা বুদ্ধিমানের কাজ নয়। লিন্টারের preserve-manual-memoization rule ঠিক এই জায়গাটি দেখে।

ধীর হিসাব: এই পর্বের সত্যিকারের run মনে করুন। reduce-এর ফল সংখ্যা হওয়ায় compiler হিসাবটি জমায়নি, জমিয়েছে JSX. যদি ওই হিসাবটিই সত্যিই ভারী হতো, সেখানে হাতের useMemo আলাদা ফল দিত। এটি একটি run-এর পর্যবেক্ষণ, compiler-এর আচরণ version-এর সাথে বদলাতে পারে। তাই নিজে compile করা কোড দেখুন, অনুমান করবেন না।

আরেকটি দিক হলো compiler-এর নিজের সীমা। যে component নিয়ম ভাঙে, সে সেটি বাদ দেয়, আর কিছু library-র সাথে মেলে না। Community-তে কেউ মনে করেন compiler চালু করাই এখন স্বাভাবিক পথ, কেউ বড় পুরোনো কোডে ধাপে ধাপে চালু করতে চান। দুটোই যুক্তিসংগত। React docs-ও ধাপে ধাপে চালুর পথ বলে দেয়।

মূল উপলব্ধি: নতুন টুল মানে পুরোনো জ্ঞান বাতিল নয়। Cache কীসের ওপর নির্ভর করে তা বুঝলে তবেই টুলের ফল যাচাই করা যায়।

প্র ০৪নিজে useFetch লিখব, নাকি TanStack Query-র মতো library নেব?

এখানে সত্যিকারের trade-off আছে, আর উত্তর অ্যাপের আকারের ওপর নির্ভর করে।

আমাদের useFetch race সমস্যা মেটায়, তবু একে “server state” সামলানোর পূর্ণ ব্যবস্থা বলা যায় না। একটু বড় অ্যাপে যা যা লাগে: একই request দুই component থেকে এলে একবারে সারা (deduplication), একটি পাতা থেকে ফিরে এলে পুরোনো ডেটা সঙ্গে সঙ্গে দেখিয়ে পেছনে নতুন আনা, ব্যর্থ হলে retry, ট্যাবে ফিরলে refetch, pagination, আর ডেটা বদলের পরে cache মেলানো। এগুলো একে একে হাতে যোগ করলে শেষে একটি অসম্পূর্ণ library-ই তৈরি হয়, যার test আর রক্ষণাবেক্ষণের দায় আপনার।

আবার সব জায়গায় library নেওয়াও ঠিক নয়। প্রতিটি নির্ভরতা bundle বাড়ায়, আর শেখার খরচ আছে। একটি ছোট পাতায় একটি মাত্র GET থাকলে ছোট hook-ই যথেষ্ট, এমনকি শেখার জন্য নিজে লেখা খুবই ভালো। আর Next.js-এর মতো framework-এ ডেটা আনা অনেক সময় Server Component-এ হয়, যেখানে client-এর hook-ই লাগে না।

React docs নিজেও বলে, Effect-এ data আনার কিছু সীমা আছে, যেমন server-এ চলে না, network waterfall তৈরি হয়, cache থাকে না। সেজন্য framework-এর ব্যবস্থা বা client-side cache ব্যবহারের পরামর্শ দেয়। পর্ব ১৯-এ আপনি বাজারঘরের তালিকা TanStack Query-তে সরিয়ে পার্থক্যটি দেখবেন।

সিদ্ধান্তের সহজ ছক: request কয়েকটি, cache লাগে না, তাহলে ছোট hook. ডেটা বহু জায়গায় ব্যবহৃত, বদলায়, ফিরে দেখা হয়, তাহলে library.

মূল উপলব্ধি: নিজে লেখা মানে সমস্যাটি বোঝা। আর বোঝার পরে অনেক সময়ই সঠিক সিদ্ধান্ত হয় নিজে না লেখা।

প্র ০৫Class component-এর logic ভাগ করার পুরোনো পথ mixin, HOC, render prop থেকে custom hook কীভাবে আলাদা?

React-এ ২০১৯-এর আগে state রাখতে হলে class লিখতে হতো। একই logic দুই component-এ ব্যবহারের পথগুলোর প্রতিটিতে একটি করে খরচ ছিল।

  • Mixin: একটি object-এর method class-এ মিশিয়ে দেওয়া। কোন মান কোথা থেকে এলো বোঝা যেত না, নামের সংঘাত হতো। এজন্য ২০১৬ সালে React দল এর বিরুদ্ধে লেখে।
  • Higher-order component (HOC): একটি component-কে আরেকটি component-এ মোড়ানো। withUser(withTheme(Page))-এ DevTools-এ গাছ গভীর হতো, প্রতিটি মোড়ক prop যোগ করত, তাই নামের সংঘাতও ফিরত।
  • Render prop: Function-কে prop হিসেবে দেওয়া। ঘটনা পরিষ্কার, কিন্তু কয়েকটি একসাথে হলে JSX ডান দিকে সরে যেত। এটিকে অনেকে “wrapper hell” বলতেন।

Custom hook এই তিন সমস্যাই কমায়। কোনো বাড়তি component নেই, তাই গাছ একই থাকে। মান আসে সাধারণ ভেরিয়েবলে, const user = useUser(), তাই উৎস চোখে দেখা যায়। আর নাম আপনিই দেন, তাই সংঘাত নেই। Class-এ একটি কাজের কোড componentDidMount, componentDidUpdate, componentWillUnmount-এ ছড়িয়ে থাকত, hook-এ তা এক জায়গায় থাকে।

তবে HOC আর render prop মরে যায়নি। Component-এর চারপাশে কিছু বসাতে হলে, যেমন authorization-এর দেয়াল বা error boundary, এখনো এদের দেখা যায়। আর পুরোনো কোডবেসে class থাকবে আরও বহু বছর, React এখনো class সমর্থন করে। Error boundary এখনো মূলত class দিয়েই লেখা হয়।

মূল উপলব্ধি: ভালো abstraction শুধু কোড কমায় না, কোড পড়ার সময় “এই মান কোথা থেকে এলো” প্রশ্নের উত্তর সহজ করে।

অনুশীলন

হিসাব, ফল অনুমান, সমালোচনা আর একটি সত্যিকারের কমান্ড।

  1. Debounce-এর হিসাব: Delay ৩০০ ms. একজন ৬ অক্ষর লিখলেন, প্রতিটির ফাঁক ১০০ ms (প্রথমটি ০ ms-এ)। তারপর শেষ অক্ষরের ৫০০ ms পরে আবার ৪ অক্ষর, একই ফাঁকে। মোট কয়টি request যাবে, কখন কখন? Debounce ছাড়া কয়টি যেত?

    প্রথম দলের শেষ অক্ষর ৫ × ১০০ = ৫০০ ms-এ। ফাঁক ৩০০-র কম, তাই আগের timer-গুলো বাতিল। Request ১ যায় ৫০০ + ৩০০ = ৮০০ ms-এ। দ্বিতীয় দল শুরু ৫০০ + ৫০০ = ১০০০ ms-এ, যা ৮০০-র পরে, তাই request ১ আগেই গেছে। এই দলের শেষ অক্ষর ১০০০ + ৩ × ১০০ = ১৩০০ ms-এ। Request ২ যায় ১৬০০ ms-এ।

    মোট ২টি, debounce ছাড়া ১০টি, অর্থাৎ ৮০% কম।

  2. ফল অনুমান: অংশ ১-এর ছোট্ট মডেলের useState আর render ধরে নিন, খাতা খালি। নিচের কোড কী ছাপবে? আসল React-এ দ্বিতীয় render-এ কী হতো?
    function Box({ vip }) {
      const [a] = useState(10);
      if (vip) useState(20);
      const [b] = useState(30);
      console.log(a, b);
    }
    render(Box, { vip: true });
    render(Box, { vip: false });

    প্রথম render-এ খাতা হয় [10, 20, 30], ছাপা হয় 10 30. দ্বিতীয় render-এ a পড়ে ঘর ০, b-এর ডাক এখন দ্বিতীয় ডাক, তাই সে পড়ে ঘর ১, ছাপা হয় 10 20.

    আসল React দ্বিতীয় render শেষে ডাকের সংখ্যা মিলিয়ে Rendered fewer hooks than expected error দিত।

  3. সমালোচনা করুন: AI নিচের hook দিয়েছে। অন্তত চারটি সমস্যা বের করুন।
    function useProducts(q) {
      const [items, setItems] = useState([]);
      const [loading, setLoading] = useState(false);
      useEffect(() => {
        setLoading(true);
        fetch("/api/products?q=" + q)
          .then((r) => r.json())
          .then((d) => { setItems(d); setLoading(false); });
      }, []); // eslint-disable-line
      return { items, loading };
    }
    • [] dependency: q বদলালেও আবার আনে না। eslint-disable ঠিক এই বাগটিই ঢেকেছে।
    • Cleanup নেই: ঠিক করে [q] দিলেই race শুরু হবে, তাই AbortController লাগবে।
    • res.ok দেখা হয়নি, .catch নেই: 500 এলে loading চিরকাল সত্য থাকতে পারে।
    • q সরাসরি URL-এ: encodeURIComponent বা URLSearchParams লাগে, নইলে & বা স্পেস query ভাঙবে।
    • Effect-এর শুরুতে সরাসরি setLoading(true): set-state-in-effect rule-এ error. loading হিসাব করে বের করুন।
    • প্রতিটি অক্ষরে request: useDebounce দিয়ে q আগে ধীর করুন।
  4. নিজে চালান: পর্ব ০৫-এর ধাঁচের একটি Vite প্রজেক্টে npm i -D eslint-plugin-react-hooks করুন, config-এ reactHooks.configs.flat.recommended যোগ করুন। একটি component-এ early return-এর পরে useState লিখে npx eslint . চালান। Hook-টি ওপরে তুলে আবার চালান। কী বদলাল?

    প্রথমবার react-hooks/rules-of-hooks error আসবে: React Hook "useState" is called conditionally, আর exit code হবে 1. Hook ওপরে তোলার পরে error চলে যায়, exit code 0. এটি CI-তে রাখলে (পর্ব ৮১) এই ভুল merge-ই হবে না। সরাসরি parser সমস্যা হলে দেখুন .jsx ফাইল config-এর files-এ আছে কি না, আর parserOptions.ecmaFeatures.jsx চালু কি না।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি component-এর re-render-এ hook-এর ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. setQty ডাকা হলো, নতুন render-এর অনুরোধ গেল
  2. React hook-এর পড়ার অবস্থান আবার শুরুতে আনল
  3. Component function ওপর থেকে চলল, প্রতিটি hook নিজের ঘর পড়ল
  4. React ডাকের সংখ্যা আগের render-এর সাথে মেলাল
  5. পরিবর্তন DOM-এ বসল, তারপর বদলে যাওয়া effect-গুলো চলল

React একটি component-এর ভেতরের দুটি useState-কে আলাদা করে চেনে কীভাবে?

  • ভেরিয়েবলের নাম দেখে
  • প্রাথমিক মান দেখে
  • ডাকার ক্রম দেখে
  • কোডের লাইন নম্বর দেখে

if (!product) return null; লাইনের পরে useState ডাকা হয়েছে। কী ভুল?

  • Component null ফেরত দিতে পারে না
  • কিছু render-এ hook ডাকা হবে না, তাই ক্রম বদলাবে
  • product-এর type ভুল
  • কোনো ভুল নেই

কোন মানটি useRef-এ রাখা সবচেয়ে মানানসই?

  • ঝুড়িতে কয়টি পণ্য আছে, যা পর্দায় দেখানো হয়
  • Controlled input-এর লেখা
  • ব্যবহারকারীর বাছাই করা filter
  • setTimeout-এর id, যা পরে বাতিল করতে লাগবে

একটি হিসাবে ১০ ms লাগে। Component ২০০ বার render হলো, আর প্রথম render-সহ মোট ৪ বার নির্ভরতা নতুন ছিল। useMemo দিলে হিসাবের মোট সময় কত, মেলানোর খরচ বাদে?

  • ৪০ ms
  • ২০০ ms
  • ১৯৬০ ms
  • ২০০০ ms

Header আর Sidebar দুটি component-ই useSearchText() custom hook ডাকে, যার ভেতরে একটি useState আছে। Header-এ লেখা বদলালে Sidebar-এ কী হবে?

  • একই লেখা দেখাবে, কারণ hook একটিই
  • Error হবে, একই hook দুবার ডাকা যায় না
  • কিছুই বদলাবে না, দুজনের state আলাদা
  • Sidebar render হবে, তবে পুরোনো লেখা দেখাবে

React 19-এর use সম্পর্কে কোনটি ঠিক?

  • এটি শুধু context পড়তে পারে
  • একে if-এর ভেতরে ডাকা যায়, তবে try/catch-এর ভেতরে নয়
  • use(fetch(url)) লেখাই সবচেয়ে ভালো ধরন
  • একে event handler-এর ভেতরে ডাকতে হয়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. React hook চেনে ডাকার ক্রম দিয়ে। তাই hook থাকবে ওপরের স্তরে, শর্ত আর লুপের বাইরে।
  2. useRef বদলালে render হয় না। Render-এর সময় নয়, event আর effect-এ পড়ুন বা লিখুন।
  3. useMemo ফল জমায়, useCallback function জমায়। আগে মাপুন, compiler-এর ফলও দেখুন।
  4. Custom hook logic ভাগ করে, state নয়। নাম use দিয়ে শুরু।
  5. Data-র hook-এ cleanup অবশ্যই লাগবে, আর eslint-plugin-react-hooks সবসময় চালু রাখুন।

পরের পর্বে: Hook-এর খাতা কোথায় থাকে, তা এবার খুলে দেখব। Virtual DOM, reconciliation, Fiber আর key-এর আসল কাজ।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান