এই পর্বে যা শিখবেন
- Effect কোথায় লাগে আর কোথায় লাগে না, তা এক প্রশ্নে আলাদা করা
- Mount, update, unmount-এ setup আর cleanup-এর সঠিক ক্রম বলতে পারা
- Timer, listener আর subscription পরিষ্কারভাবে বন্ধ করা; StrictMode-এর দ্বিগুণ চলা বোঝা
- Loading, error আর race condition সামলে effect-এ data আনা
- Object dependency-র অসীম চক্র, stale closure আর derived state-এর ভুল ধরা ও সারানো
১ · Effect-এর কাজ: বাইরের জগতের সাথে মিল রাখা
“বাজারঘর”-এর কার্ট পাতায় তিনটি দাবি এল। ব্রাউজারের ট্যাবে লেখা থাকবে “কার্ট (৩)”। ফ্ল্যাশ সেলের countdown চলবে। আর স্টকের খবর আসবে socket থেকে। পর্ব ১০-এ আপনি state আর render শিখেছেন, কিন্তু এই তিনটি কাজের কোনোটিই JSX ফেরত দিয়ে হয় না। ট্যাবের লেখা, timer আর socket থাকে React-এর বাইরে।
পর্ব ০৮ থেকে মনে করুন, render মানে শুধু হিসাব: props আর state নিয়ে UI-র বর্ণনা ফেরত দেওয়া। Render-এর ভেতরে timer চালু করা যায় না, কারণ React একটি component যেকোনো সময়ে যতবার দরকার ততবার render করতে পারে। তাহলে বাইরের কাজ করব কোথায়? উত্তর দুটি:
- Event handler: নির্দিষ্ট একটি ঘটনার ফলে যা হবে। “অর্ডার করুন” চাপলে POST অনুরোধ।
- Effect: পাতায় component থাকার কারণেই যা হবে, যেকোনো ক্লিক ছাড়াই। কার্ট পাতা খোলা আছে, তাই socket সংযোগও খোলা থাকবে।
import { useEffect } from 'react';
function CartTitle({ count }) {
useEffect(() => {
document.title = `কার্ট (${count})`; // React-এর বাইরের জিনিস
}, [count]); // count বদলালে আবার মিলবে
return <h2>আপনার কার্টে {count}টি পণ্য</h2>;
}useEffect দুটি জিনিস নেয়: একটি setup ফাংশন আর একটি dependency array. React প্রথমে পাতায় DOM বসায়, তারপর setup চালায়। মূল বাক্যটি মনে রাখুন: effect ঘটনায় সাড়া দেয় না, বাইরের জগৎকে React-এর এখনকার state-এর সাথে মিলিয়ে রাখে। ইংরেজিতে একে বলে synchronize.
ধরুন আপনি হোটেলে উঠেছেন। রিসেপশন রুম নম্বর দিল 201. ঘরে ঢুকে ফ্যান চালালেন (setup). রুম বদলে 305 হলো। এখন আগে 201-এর ফ্যান বন্ধ করবেন (cleanup), তারপর 305-এর ফ্যান চালাবেন (নতুন setup). রুম একই থাকলে কিছুই করতে হয় না। চেক-আউটের সময় শেষবার ফ্যান বন্ধ। এখানে রুম নম্বর হলো dependency, আর ফ্যান হলো বাইরের জগৎ। Effect-এর পুরো নিয়ম এই গল্পেই আছে।
Effect-এর চোখে একটি component-এর জীবন
Mount, update and unmount from the effect's point of view
ইতিহাস: ২০১৯ সালের শুরুতে React 16.8-এর আগে একই কাজ লেখা হতো class-এর তিনটি lifecycle method-এ: componentDidMount, componentDidUpdate, componentWillUnmount. একটি subscription-এর কোড তিন জায়গায় ছড়িয়ে যেত। Hook সেই তিন জায়গাকে এক useEffect-এ আনল। পুরোনো কোডে এই নামগুলো এখনো দেখবেন, আর পর্ব ০৪-এ দেখা Observer-এর ধারণাও এখানে ফিরে আসে।
২ · Mount, update, unmount: ক্রম ধরে দেখা
একটি component যখন প্রথম পাতায় আসে, তাকে বলে mount. Props বা state বদলে আবার render হলে update. পাতা থেকে সরে গেলে unmount. React-এর কাজ দুই ধাপে: render-এ হিসাব, আর commit-এ DOM বদল। Effect চলে commit-এর পরে। ব্রাউজার সাধারণত আগে পর্দা আঁকে (paint), তারপর effect চলে, যাতে পর্দা আটকে না থাকে। তবে ক্লিকের মতো সরাসরি কাজের ফলে হওয়া update-এ React effect paint-এর আগেও চালাতে পারে।
নিচের component ব্যবহারকারীর কার্টের খবরে subscribe করে। “পরে” চেপে দেখুন প্রতিটি ধাপে কোন লাইন চলে:
1function CartBadge({ userId }) {2 const [n, setN] = useState(0);3 useEffect(() => {4 const off = cartFeed.subscribe(userId, setN);5 return () => off();6 }, [userId]);7 return <b>{n}</b>;8}
| userId | |
| খোলা subscription | |
| পাতায় আছে? |
- Mount, render. React
userId = 1নিয়ে ফাংশন চালায়। Effect শুধু খাতায় লেখা হয়, এখনো চলে না। - Commit. DOM-এ
<b>0</b>বসল। Component এখন পাতায় আছে। - Setup. DOM তৈরি হওয়ার পরে effect চলল। User 1-এর subscription খুলল।
- একই userId-তে আবার render. Parent আবার render হলো, কিন্তু
1আর1একই। React effect ছুঁলোই না, subscription যেমন ছিল তেমনই। - userId হলো 2. নতুন render আর commit হলো। পুরোনো subscription এখনো user 1-এরই, কারণ cleanup চলেনি।
- আগে cleanup. আগের render-এর
offচলল, যেটি user 1-কে চেনে। খোলা subscription নেই। - তারপর নতুন setup. User 2-এর subscription খুলল। যেকোনো মুহূর্তে subscription একটিই।
- Unmount. Component সরে গেল। শেষ cleanup চলল user 2-এর জন্য। মোট setup ২টি, cleanup ২টি: হিসাব মিলে গেছে।
এবার সত্যিকারের React 19-এ নিজে চালান। তিনটি effect-এর তিন ধরনের dependency, আর component-টি mount, id বদল, একই id-তে render আর unmount হবে:
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
function Cart({ id }) {
console.log('render, id =', id);
useEffect(() => {
console.log('A setup');
return () => console.log('A cleanup');
});
useEffect(() => {
console.log('B setup');
return () => console.log('B cleanup');
}, []);
useEffect(() => {
console.log('C setup, id =', id);
return () => console.log('C cleanup, id =', id);
}, [id]);
return <p>কার্ট #{id}</p>;
}
const root = createRoot(document.getElementById('root'));
const step = (ms, name, job) => setTimeout(() => {
console.log('--- ' + name);
job();
}, ms);
step(0, 'mount', () => root.render(<Cart id={1} />));
step(50, 'id 1 থেকে 2', () => root.render(<Cart id={2} />));
step(100, 'একই id', () => root.render(<Cart id={2} />));
step(150, 'unmount', () => root.unmount());
</script>আসল আউটপুটে id 1 থেকে 2 অংশে দেখবেন render, id = 2, তারপর A cleanup, C cleanup, id = 1, তারপর A setup, C setup, id = 2. অর্থাৎ আগে সব cleanup, তারপর সব setup. আর cleanup দেখছে পুরোনো id = 1, কারণ সে আগের render-এর ফাংশন। একই id অংশে শুধু A চলে, C চলে না। B চলেছে মাত্র একবার, আর তার cleanup এসেছে unmount-এ।
Parent আর child থাকলে আরেকটি নিয়ম কাজে লাগে। সত্যিকারের run-এ setup-এর ক্রম ছিল child setup, তারপর parent setup. আর unmount-এ parent cleanup, তারপর child cleanup.
৩ · Dependency array: কখন চলবে, তা আপনি বাছেন না
| লেখার ধরন | কখন চলে | কোথায় মানায় |
|---|---|---|
useEffect(fn) | প্রতিটি commit-এর পরে | খুব কম ক্ষেত্রে; সাধারণত ভুলে হয় |
useEffect(fn, []) | শুধু প্রথম commit-এর পরে | Props বা state পড়ে না এমন বাইরের কাজ |
useEffect(fn, [a, b]) | প্রথমবার, আর a বা b বদলালে | সবচেয়ে সাধারণ রূপ |
নতুনদের সবচেয়ে বড় ভুল ধারণা হলো, “কখন চালাতে চাই” ভেবে dependency লেখা। আসল নিয়ম উল্টো: effect-এর ভেতরে যে props, state বা component-এর ভেতরে বানানো মান পড়া হয়, সব তালিকায় থাকবে। আপনি তালিকা বাছেন না; কোড পড়ে তালিকা আপনাআপনি ঠিক হয়। ইচ্ছেমতো বাদ দিলে React-কে মিথ্যা বলা হয়।
তুলনা হয় Object.is দিয়ে, প্রতিটি ঘর আলাদা করে। সংখ্যা আর string মান দিয়ে মেলে। Object, array আর function মেলে পরিচয় দিয়ে, যাকে বলে reference equality. এই পার্থক্য পরে অসীম চক্রের কারণ হবে।
তালিকা মুখস্থ না করে টুলকে দিয়ে যাচাই করান। ESLint-এর exhaustive-deps নিয়ম ঠিক এই কাজ করে। পর্ব ০৫-এ ESLint বসিয়েছিলেন। নিচের কার্ট component-এ দুটি ভুল আছে:
export function Cart({ userId, items }) {
const [total, setTotal] = useState(0);
const [cart, setCart] = useState(null);
useEffect(() => {
fetch(`/api/cart/${userId}`).then(r => r.json()).then(setCart);
}, []);
useEffect(() => {
setTotal(items.reduce((sum, item) => sum + item.price, 0));
}, [items]);
// ...সত্যিকারের run (eslint 10.14, eslint-plugin-react-hooks 7.1.1):
$ npx eslint src/Cart.jsx
~/bazarghar/src/Cart.jsx
9:6 warning React Hook useEffect has a missing dependency: 'userId'.
Either include it or remove the dependency array
react-hooks/exhaustive-deps
12:5 error Error: Calling setState synchronously within an effect
can trigger cascading renders
…
> 12 | setTotal(items.reduce((sum, item) => sum + item.price, 0));
| ^^^^^^^^ Avoid calling setState() directly within an effect
… react-hooks/set-state-in-effect
✖ 2 problems (1 error, 1 warning)
প্রথম সতর্কতার অর্থ: userId বদলালেও পুরোনো ব্যবহারকারীর কার্ট থেকে যাবে। এটি আসল বাগ, আর গ্রাহক একে দেখবেন ভুল কার্ট হিসেবে। দ্বিতীয়টির সমাধান আসবে ৮ নম্বর অংশে। লাইন নম্বর দুটি পুরো ফাইলের, উপরের অংশে শুধু দরকারি লাইন দেখানো হয়েছে।
// eslint-disable-next-line react-hooks/exhaustive-deps লিখে সতর্কতা চাপা দেওয়া প্রায় সবসময় বাগ লুকিয়ে রাখে। React-এর নথিও একে dependency নিয়ে মিথ্যা বলার সাথে তুলনা করে। Dependency সরাতে চাইলে কোড বদলান: মানটি component-এর বাইরে নিন, effect-এর ভেতরে আনুন, অথবা updater function ব্যবহার করুন।
৪ · Cleanup: যা খুলেছেন, তা বন্ধ করুন
Cleanup হলো setup থেকে ফেরত দেওয়া একটি ফাংশন। নিয়ম সহজ: cleanup setup-এর আয়না। যা শুরু করেছেন, তা থামান; যা যোগ করেছেন, তা সরান। Cleanup না দিলে component চলে গেলেও কাজ পেছনে চলতে থাকে। একে বলে leak, মানে ফুটো: মেমরি, timer আর সংযোগ জমতে থাকে, যা চলতি পাতায় ধীরে ধীরে সমস্যা বাড়ায়।
| কাজ | Setup | Cleanup |
|---|---|---|
| Timer | setInterval(fn, 1000) | clearInterval(id) |
| Event listener | addEventListener('resize', fn) | removeEventListener('resize', fn) |
| Subscription | feed.subscribe(fn) | ফেরত পাওয়া off() |
| WebSocket | new WebSocket(url) | ws.close() |
| Data আনা | fetch(url, { signal }) | controller.abort() |
removeEventListener-এ একই ফাংশন দিতে হয়। Cleanup-এ নতুন করে () => {} লিখলে সেটি অন্য ফাংশন, listener সরবে না। তাই ফাংশনটি setup-এর ভেতরে নাম দিয়ে রাখুন, আর একই নাম দুই জায়গায় ব্যবহার করুন।
নিচের কোডে দুটি একই রকম ঘড়ি। একটিতে cleanup নেই, অন্যটিতে আছে। 250 ms পরে দুটিই পাতা থেকে সরানো হবে:
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
let leaked;
function Clock({ label, clean }) {
const [n, setN] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(label + ': tick');
setN(x => x + 1);
}, 100);
if (!clean) { leaked = id; return; }
return () => { clearInterval(id); console.log(label + ': cleanup'); };
}, []);
return <p>{label}: {n}</p>;
}
function App() {
const [show, setShow] = useState(true);
useEffect(() => {
setTimeout(() => { console.log('--- সরানো হলো'); setShow(false); }, 250);
}, []);
if (!show) return <p>ঘড়ি নেই</p>;
return <><Clock label="ভুল" /><Clock label="ঠিক" clean /></>;
}
createRoot(document.getElementById('root')).render(<App />);
setTimeout(() => {
clearInterval(leaked);
console.log('ডেমো শেষ: ফুটো timer জোর করে থামানো হলো');
}, 650);
</script>আউটপুটে --- সরানো হলো লাইনের পরে ঠিক: cleanup এসে “ঠিক” ঘড়িটি থেমে যায়। কিন্তু “ভুল” ঘড়ি পাতায় নেই, তবু ভুল: tick ছাপা হতে থাকে। Component মরে গেছে, timer মরেনি। শেষের জোর করে থামানো লাইনটি না থাকলে এটি পাতা বন্ধ না হওয়া পর্যন্ত চলত। প্রতিবার পাতা ঘুরে এলে একটি করে নতুন ফুটো যোগ হতো।
লক্ষ করুন, setN(x => x + 1)-এ পুরোনো n পড়া হয়নি, তাই [] সৎ dependency তালিকা। এই কৌশল ৯ নম্বর অংশে আবার দেখবেন।
৫ · StrictMode: দুবার চলা কেন উপকারী
নিজের project-এ effect-এ console.log লিখে অনেকে অবাক হন: এক mount-এ লেখা আসে দুবার। এটি বাগ নয়। StrictMode development-এ প্রতিটি component mount করার পরপরই একবার নকল unmount আর পুনরায় mount করে। React 18 (২০২২) থেকে এই পরীক্ষা চালু। Vite-এর React template-এ <StrictMode> শুরু থেকেই দেওয়া থাকে।
সত্যিকারের run: React 19.3.1, Vite 8.3.5, Edge ব্রাউজার। একই কোড, একই <StrictMode>, প্রথমে development, পরে production build:
$ npx vite # development
setup: resize listener যোগ
cleanup: resize listener সরানো
setup: resize listener যোগ
$ npx vite build && npx vite preview # production
setup: resize listener যোগ
তাহলে লাভ কী? Cleanup ভুলে গেলে সাথে সাথে ধরা পড়ে। একটি নকল socket দিয়ে একই পরীক্ষা, development-এ:
# cleanup নেই
connect, খোলা সংযোগ = 1
connect, খোলা সংযোগ = 2 ← বাগ, এখনই দেখা গেল
# cleanup আছে
connect, খোলা সংযোগ = 1
disconnect, খোলা সংযোগ = 0
connect, খোলা সংযোগ = 1 ← ঠিক
একটি যাচাই-প্রশ্ন মনে রাখুন: ব্যবহারকারী কি setup, cleanup, setup আর শুধু setup-এর পার্থক্য বুঝতে পারছেন? পারলে cleanup অসম্পূর্ণ। ব্যবহারকারী তো পাতা ছেড়ে ফিরেও আসেন, আর তখন production-এও একই ক্রম হয়। StrictMode শুধু সেই দৃশ্যটি প্রথম সেকেন্ডেই দেখিয়ে দেয়।
অনেকে useRef-এর flag দিয়ে দ্বিতীয়বার চলা আটকান, বা StrictMode তুলে দেন। এতে লক্ষণ চাপা পড়ে, বাগ যায় না। সমস্যা সাধারণত অন্য জায়গায়: যে কাজ দুবার হওয়া ক্ষতিকর (যেমন অর্ডারের POST), সেটি effect-এ থাকারই কথা নয়, event handler-এ থাকার কথা। এই পাতার live block-গুলোর React production build, তাই সেখানে দ্বিগুণ চলা দেখবেন না; এটি দেখতে নিজের Vite project লাগবে।
৬ · Effect-এ data আনা: loading, error আর race condition
“বাজারঘর”-এর সার্চ বাক্সে যা লেখা হবে, তার ফল দেখাতে হবে। Network-ও বাইরের জগৎ, তাই এখানে effect বৈধ। কিন্তু fetch লিখলেই কাজ শেষ নয়। তিনটি জিনিস আপনাকে সামলাতে হয়: ফল আসার আগে কী দেখাবেন, ব্যর্থ হলে কী দেখাবেন, আর উত্তর উল্টো ক্রমে এলে কী করবেন।
রেস্তোরাঁয় আপনি ভাত অর্ডার করলেন, একটু পরে মত বদলে বললেন নুডলস। রান্নাঘরে নুডলস আগে হলো, ওয়েটার টেবিলে দিল। এরপর দেরিতে ভাত এল, আর ওয়েটার নুডলস সরিয়ে ভাত রেখে গেল। আপনি শেষ যা চেয়েছিলেন, টেবিলে আছে তার উল্টো। এটাই race condition: ফল নির্ভর করছে কোনটি আগে শেষ হলো তার ওপর। সমাধান দুটি: ওয়েটারের খাতায় পুরোনো অর্ডার কেটে দেওয়া, অথবা রান্নাঘরে খবর পাঠিয়ে সেটি বন্ধ করা।
ধরুন “চাল”-এর উত্তর আসতে 300 ms লাগে, “ডাল”-এর 100 ms. ব্যবহারকারী “চাল” লেখার 50 ms পরে “ডাল” লিখলেন:
| সময় | |
| সার্চ বাক্সে | |
| অনুরোধ ১ (চাল) | |
| অনুরোধ ২ (ডাল) | |
| ভুল কোডের পর্দা | |
| ঠিক কোডের পর্দা |
- 0 ms. Effect চলল, অনুরোধ ১ বেরোল। এই effect-এর নিজের
ignoreএখনfalse. - 50 ms.
qবদলাল। আগে cleanup চলে অনুরোধ ১-এরignore-কেtrueকরল। তারপর নতুন effect অনুরোধ ২ পাঠাল, যারignoreআলাদা। - 150 ms.
50 + 100: পরের অনুরোধের উত্তর আগে এল। দুই কোডেই পর্দায় ডাল-এর ফল, যা ঠিক। - 300 ms. অনুরোধ ১-এর উত্তর দেরিতে এল। ভুল কোড
setResultচালিয়ে পর্দা নষ্ট করল। ঠিক কোডignore === trueদেখে উত্তরটি ফেলে দিল। - ফল. ভুল কোডে বাক্সে “ডাল”, কিন্তু তালিকায় চাল। কোনো error নেই, কোনো লাল লেখা নেই। দ্রুত network-এ এই বাগ দেখাই যায় না, তাই ব্যবহারকারীর ফোনে প্রথম ধরা পড়ে।
এখানে কোনো সত্যিকারের network নেই। setTimeout দিয়ে উত্তরের দেরি বানানো হয়েছে। একই ঘটনা ভুল আর ঠিক, দুই কোডে পাশাপাশি চলছে:
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const delay = { চাল: 300, ডাল: 100 };
const fakeFetch = q => new Promise(done =>
setTimeout(() => done(q + '-এর ফল'), delay[q]));
function Search({ q, fixed }) {
const name = fixed ? 'ঠিক' : 'ভুল';
const [result, setResult] = useState('লোড হচ্ছে');
useEffect(() => {
let ignore = false;
fakeFetch(q).then(r => {
if (fixed && ignore) { console.log(name, 'ফেলে দিল:', r); return; }
console.log(name, 'বসাল:', r);
setResult(r);
});
return () => { ignore = true; };
}, [q]);
return <p>{name}: বাক্সে {q}, তালিকায় {result}</p>;
}
function App() {
const [q, setQ] = useState('চাল');
useEffect(() => { setTimeout(() => setQ('ডাল'), 50); }, []);
return <><Search q={q} /><Search q={q} fixed /></>;
}
createRoot(document.getElementById('root')).render(<App />);
setTimeout(() => console.log(document.body.innerText.trim()), 450);
</script>শেষ দুই লাইনে আসল ফল: ভুল: বাক্সে ডাল, তালিকায় চাল-এর ফল আর ঠিক: বাক্সে ডাল, তালিকায় ডাল-এর ফল. তার আগে ঠিক ফেলে দিল: চাল-এর ফল. প্রতিটি effect-এর নিজের let ignore আছে, কারণ প্রতিটি চলার closure আলাদা। Cleanup শুধু নিজের চলার flag বদলায়। পরের অনুশীলনে delay বদলে দেখবেন, বাগটি কেন মাঝে মাঝে লুকিয়ে যায়।
আসল fetch: AbortController
ignore flag উত্তর ফেলে দেয়, কিন্তু অনুরোধ চলতেই থাকে। AbortController ব্রাউজারকে অনুরোধটি বাতিল করতে বলে, ফলে ডেটাও বাঁচে। এটি ওয়েব প্ল্যাটফর্মের অংশ, React-এর নয়। Loading আলাদা state না রেখে হিসাব করে নেওয়া হয়েছে: ফলটি এখনকার q-এর না হলে এখনো লোড হচ্ছে। State-এর এমন আকার পর্ব ০৬-এর discriminated union দিয়েও লেখা যায়।
function Products({ q }) {
const [result, setResult] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/products?q=${encodeURIComponent(q)}`, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`); // 404, 500-এ fetch নিজে ব্যর্থ হয় না
return res.json();
})
.then(data => setResult({ q, items: data.items }))
.catch(err => {
if (err.name === 'AbortError') return; // আমরাই বাতিল করেছি, এটি ত্রুটি নয়
setResult({ q, failed: true });
});
return () => controller.abort();
}, [q]);
if (result?.q !== q) return <p>লোড হচ্ছে...</p>;
if (result.failed) return <p role="alert">পণ্য আনা গেল না। আবার চেষ্টা করুন।</p>;
return <p>{q}: {result.items}টি পণ্য</p>;
}একটি সত্যিকারের run. Vite-এর স্থানীয় server-এ নকল API, rice-এর দেরি 300 ms, বাকি শব্দে 100 ms; Playwright দিয়ে Edge চালিয়ে network আর console ধরা হয়েছে। REQ, RES, FAILED লেবেলগুলো আমাদের script-এর, আর favicon-এর একটি অসম্পর্কিত লাইন বাদ দেওয়া হয়েছে। Run-টি development-এ, StrictMode চালু:
REQ api/products?q=rice ← প্রথম setup
REQ api/products?q=rice ← StrictMode-এর দ্বিতীয় setup
[log] বাতিল: rice AbortError
FAILED api/products?q=rice net::ERR_ABORTED
RES 200 api/products?q=rice
[log] ফল এল: rice
state: rice: 2টি পণ্য
REQ api/products?q=dal
state: লোড হচ্ছে...
REQ api/products?q=oil ← 20 ms পরে আরেকটি শব্দ
[log] বাতিল: dal AbortError
FAILED api/products?q=dal net::ERR_ABORTED
RES 200 api/products?q=oil
[log] ফল এল: oil
state: oil: 2টি পণ্য
REQ api/products?q=fail
RES 500 api/products?q=fail
[error] Failed to load resource: the server responded with a status of 500 (Internal Server Error)
[log] ব্যর্থ: fail HTTP 500
state: পণ্য আনা গেল না। আবার চেষ্টা করুন।
তিনটি কথা খেয়াল করুন। বাতিল হওয়া অনুরোধ DevTools-এর Network ট্যাবে “canceled” হিসেবে দেখা যায়, অর্থাৎ StrictMode-এর দ্বিতীয় অনুরোধটিও এখানে ক্ষতি করেনি। HTTP 500-তে fetch reject করে না, তাই res.ok যাচাই না করলে error ধরা পড়ত না। আর abort করলেও server হয়তো কাজ শেষ করে ফেলেছে; বাতিল হওয়া শুধু ব্রাউজারের দিকের নিশ্চয়তা।
ignore flag | AbortController | |
|---|---|---|
| উত্তর ফেলে দেয়? | হ্যাঁ | হ্যাঁ |
| Network বাঁচায়? | না | হ্যাঁ, যদি উত্তর তখনো আসা শেষ না হয় |
| যেকোনো Promise-এ চলে? | হ্যাঁ | শুধু যা signal নেয় |
| কোড | খুব ছোট | AbortError আলাদা করে সামলাতে হয় |
আরেকটি প্রচলিত ভুল: useEffect(async () => {...}). Async ফাংশন Promise ফেরত দেয়, অথচ effect-এর ফেরত মান হতে হবে cleanup ফাংশন বা কিছুই না। async/await চাইলে ভেতরে একটি async ফাংশন বানিয়ে ডাকুন।
এটিই শেষ কথা নয়। বড় অ্যাপে প্রতিটি পাতায় এই কোড হাতে লিখলে cache নেই, retry নেই, একই data বারবার আসে। React-এর নথিও framework-এর data fetching বা TanStack Query-র মতো library-র পরামর্শ দেয়। পর্ব ১৪-এ এই কোড একটি custom hook-এ তুলবেন, পর্ব ১৯-এ server state library-তে নেবেন, আর পর্ব ৫৩-এ Next.js-এ সার্ভারে data আনবেন। Effect কখনো সার্ভারে চলে না, তাই শুধু effect-এ আনা data প্রথম HTML-এ থাকে না; পর্ব ০৩-এ এর ফল দেখেছেন। তবু ভেতরের কৌশলটি এখানে বোঝা জরুরি, কারণ library-গুলো এই সমস্যাই সামলায়।
৭ · Object dependency আর অসীম চক্র
পণ্যের তালিকায় filter object পাঠাতে গিয়ে এই কোড লেখা খুব সহজ। প্রতিটি লাইন যুক্তিসঙ্গত মনে হয়, ESLint-এর dependency-র দাবিও পূরণ হয়েছে। কিন্তু:
1function ProductGrid({ category }) {2 const [items, setItems] = useState([]);3 const filter = { category };4 useEffect(() => {5 loadProducts(filter).then(setItems);6 }, [filter]);7 return <Grid items={items} />;8}
| Render নম্বর | |
| এই render-এর filter | |
| Object.is(নতুন, পুরোনো) | |
| মোট অনুরোধ |
- Render ১. লাইন ৩-এ একটি object তৈরি হলো। ধরুন এর পরিচয় #1.
- Effect. প্রথমবার, তাই effect চলল। অনুরোধ ১ বেরোল।
- উত্তর এল.
setItemsstate বদলাল, তাই render ২. লাইন ৩ আবার চলে একই চেহারার, কিন্তু নতুন object #2 বানাল।Object.is(#2, #1)মিথ্যা। - Effect আবার. React ভাবল dependency বদলেছে। অনুরোধ ২ বেরোল।
- চক্র. উত্তর এলে render ৩, object #3, অনুরোধ ৩। ভেতরের মান একবারও বদলায়নি, তবু এটি থামবে না।
- সমাধান. Dependency-তে object না দিয়ে
[category]দিন, আরfiltereffect-এর ভেতরে বানান। এবারObject.is('চাল', 'চাল')সত্য। এক অনুরোধেই কাজ শেষ।
useEffect(() => {
const filter = { category }; // ভেতরে: dependency আর object নয়
loadProducts(filter).then(setItems);
}, [category]); // string, মান দিয়ে মেলেসত্যিকারের run-এ { category: 'চাল' } object-কে dependency দিয়ে ভেতরে সরাসরি একটি state বাড়ানো হয়েছিল। React 19.3.1 development-এ Console লাল হলো, ব্রাউজারও চলল:
[error] Maximum update depth exceeded. This can happen when a component
calls setState inside useEffect, but useEffect either doesn't have a
dependency array, or one of the dependencies changes on every render.
… (3 সেকেন্ডের run-এ এই লাইন ২,৭২৪ বার)
renders after 1.5s: 69843
renders after 3.0s: 141647
# একই কোড, production build (vite preview)
renders after 1.5s: 145142
renders after 3.0s: 296969 ← Console-এ কোনো লেখা নেই
এই সংখ্যা একটি স্থানীয় run-এর, সাধারণ ফল নয়। তবে দুটি শিক্ষা সবখানেই খাটে: এটি কোনো exception নয়, পাতাও ভেঙে পড়েনি, তাই ব্যবহারকারী শুধু গরম ফোন আর ধীর পাতা পাবেন। আর production-এ কোনো সতর্কতাই নেই। ESLint আগেই ধরতে পারে: একই কোডে সে বলেছে The 'filter' object makes the dependencies of useEffect Hook (at line 39) change on every render.
Effect-এ যদি fetch থাকে, তবে চক্রের গতি বাঁধা পড়ে উত্তরের সময়ে। ধরে নিন উত্তর আসে 100 ms-এ। প্রতি ব্যবহারকারী তখন 1000 ÷ 100 = 10 req/s পাঠাবেন। এক সময়ে খোলা আছে এমন ব্যবহারকারী ধরে নিন ৫,০০০ জন (অনুমান)। মোট 10 × 5000 = 50,000 req/s, অথচ কাজের অনুরোধ দরকার ছিল ৫,০০০টি, প্রতিজনের একটি। একটি ছোট frontend বাগ এভাবে নিজের backend-এর ওপর চাপ তৈরি করে।
এটি শুধু কাগজের অঙ্ক নয়। Cloudflare-এর প্রকাশিত post-mortem অনুযায়ী, ১২ সেপ্টেম্বর ২০২৫-এর dashboard ও API বিভ্রাটের একটি কারণ ছিল এই ধরনের ভুল: dependency array-তে এমন object, যা প্রতিবার নতুন তৈরি হচ্ছিল, ফলে একই API ডাক এক render-এ বহুবার হচ্ছিল আর একটি ভেতরের service-এর ওপর চাপ বাড়াচ্ছিল। ঘটনাটির আরও কারণ ছিল; বিস্তারিত ভাবনার প্রশ্নে দেখবেন।
৮ · হয়তো আপনার effect লাগবেই না
React-এর নথিতে একটি পুরো অধ্যায়ের নাম “You Might Not Need an Effect”. সবচেয়ে সহজ পরীক্ষা: বাইরের কোনো সিস্টেম জড়িত আছে কি? না থাকলে effect সম্ভবত ভুল জায়গা। সবচেয়ে সাধারণ ভুল হলো derived state: যে মান props বা state থেকে হিসাব করা যায়, তাকে আলাদা state-এ রাখা।
নিচে একই হিসাব দুইভাবে। ১ কেজি থেকে ২ কেজি, প্রতি কেজি 120 টাকা:
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const price = 120;
let badCount = 0, goodCount = 0; // শুধু গোনার জন্য
function Bad({ qty }) {
const [total, setTotal] = useState(0);
useEffect(() => { setTotal(qty * price); }, [qty]);
console.log('Bad render', ++badCount, 'qty', qty, 'total', total);
return <p>Bad: {qty} কেজি = {total} টাকা</p>;
}
function Good({ qty }) {
const total = qty * price;
console.log('Good render', ++goodCount, 'qty', qty, 'total', total);
return <p>Good: {qty} কেজি = {total} টাকা</p>;
}
const root = createRoot(document.getElementById('root'));
const show = qty => root.render(<><Bad qty={qty} /><Good qty={qty} /></>);
show(1);
setTimeout(() => show(2), 100);
setTimeout(() => console.log('মোট render: Bad', badCount, 'Good', goodCount), 200);
</script>আসল আউটপুটের শেষ লাইন মোট render: Bad 4 Good 2. শুধু বাড়তি render নয়, ভুল মানও আছে: Bad render 1 qty 1 total 0 আর Bad render 3 qty 2 total 120. শেষেরটিতে ২ কেজির দাম 120 টাকা, মুহূর্তের জন্য হলেও ভুল হিসাব। Good-এ এমন মুহূর্ত থাকতেই পারে না, কারণ total আলাদা কোনো জিনিস নয়, এটি সবসময় qty থেকেই আসে।
k বার মান বদলালে এই নকশায় Good-এর render k + 1, Bad-এর 2(k + 1): প্রতিটি হিসাবে একটি বাড়তি পাস। আমাদের run-এ k = 1, তাই 2 আর 4. কোনো input-এ ২০ অক্ষর লিখলে হবে ২১ বনাম ৪২. হিসাব ভারী হলে useMemo, যা পর্ব ১৪-এ আসছে; তবে সেটিও render-এর সময়েই চলে, effect-এ নয়।
| যা করতে চান | Effect-এ লিখবেন না, লিখুন |
|---|---|
| Props বা state থেকে মান বের করা | Render-এ সরাসরি হিসাব |
| “অর্ডার করুন” চাপলে POST | Event handler |
| কার্টে যোগ হলে toast দেখানো | যে handler কার্টে যোগ করে, তার ভেতরেই |
| Prop বদলালে সব state মুছে ফেলা | Component-এ key দেওয়া (পর্ব ১৫) |
| State বদলালে parent-কে জানানো | একই event-এ দুটোই বদলানো, অথবা state উপরে তোলা |
| এক effect-এর state থেকে পরের effect চালানো | একটি handler-এ সব পরের state হিসাব করা |
| বাইরের store-এর মান পড়া | useSyncExternalStore |
Event handler-এর ব্যাপারটি কেন এত জরুরি, তা একটি ঝুঁকির উদাহরণে দেখুন:
// ভুল: ঘটনার কাজ effect-এ
useEffect(() => {
if (confirmed) fetch('/api/orders', { method: 'POST', body });
}, [confirmed, body]);
// ঠিক: ঘটনার কাজ ঘটনার জায়গায়
async function handleConfirm() {
await fetch('/api/orders', { method: 'POST', body });
setConfirmed(true);
}প্রথম রূপে confirmed সত্য থাকা অবস্থায় component আবার mount হলে অর্ডার আবার যাবে। Development-এ StrictMode-ও এটি দুবার পাঠাতে পারে। আর body বদলালে আরেকটি অর্ডার। মাপকাঠিটি মনে রাখুন: “কারণ ব্যবহারকারী চাপলেন” হলে event handler; “কারণ component পাতায় দেখা যাচ্ছে” হলে effect.
৯ · Stale closure, useEffectEvent আর useLayoutEffect
পর্ব ১০-এ দেখেছেন, প্রতিটি render-এর নিজের snapshot থাকে। Effect-এর ফাংশনও একটি closure: যে render-এ তৈরি, সেই render-এর মান মনে রাখে। Dependency ভুল হলে পুরোনো closure-ই চলতে থাকে। একে বলে stale closure, বাসি হয়ে যাওয়া closure. যেন গত বছরের দামের তালিকা পকেটে নিয়ে বাজারে গেছেন: দোকানের দাম বদলেছে, আপনার কাগজ বদলায়নি।
1function Sale() {2 const [n, setN] = useState(0);3 useEffect(() => {4 const id = setInterval(() => {5 setN(n + 1);6 }, 1000);7 return () => clearInterval(id);8 }, []);9 return <p>{n}</p>;10}
| সময় | |
| React-এর আসল n | |
| Interval-এর চোখে n | |
| setN পায় | |
| পর্দায় |
- Render ১. এই render-এর
nহলো 0. - Effect একবারই চলল. Dependency
[], তাই interval-এর ফাংশন বানানো হলো render ১-এর ভেতরে। এর চোখেnচিরকাল 0. - 1 s. ফাংশন চলল:
setN(0 + 1). State হলো 1, render ২ হলো। কিন্তু[]-এর কারণে effect আর চলল না, পুরোনো ফাংশনই রইল। - 2 s. আবার
setN(0 + 1), অর্থাৎ 1. মান একই, পর্দাও একই। - 3 s. একই ঘটনা। কোনো error নেই, শুধু ঘড়ি থেমে আছে। সত্যিকারের run-এ log-এ বারবার
tick দেখছে n = 0এসেছে, আর React মান না বদলানো update-এ একবার বাড়তি render করে থেমে গেছে। - সমাধান.
setN(x => x + 1): পুরোনো মান React দেয়, closure থেকে পড়তে হয় না।[]এখন সৎ, interval-ও বারবার নতুন করে বসে না।
[n] দিলেও ঘড়ি ঠিক চলত, তবে প্রতি সেকেন্ডে interval বন্ধ করে নতুন বসত। Updater function তাই এখানে পরিষ্কার সমাধান। exhaustive-deps নিয়ম এই ভুলও ধরে দেয়।
useEffectEvent: effect-এর ভেতরের “ঘটনা”
ধরুন স্টকের feed-এ সংযোগ হলে একটি বার্তা দেখাবেন, যা theme মেনে চলে। theme dependency-তে দিলে theme বদলালেই সংযোগ কেটে আবার জুড়বে, যা কেউ চায় না। না দিলে stale closure. এই দ্বন্দ্বের জন্য useEffectEvent এসেছে। এটি React 19.2 (অক্টোবর ২০২৫) থেকে React-এর অংশ, আর এই লেখার সময়ে react.dev-এর reference-এ কোনো experimental চিহ্ন ছাড়াই আছে। আমরা React 19.3.1-এ চালিয়ে দেখেছি।
function Room({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
console.log('connected', roomId, 'theme =', theme); // সবসময় সর্বশেষ মান
});
useEffect(() => {
console.log('connect', roomId);
const id = setTimeout(() => onConnected(), 50);
return () => { clearTimeout(id); console.log('disconnect', roomId); };
}, [roomId]); // onConnected এখানে নয়
return <p>{roomId} {theme}</p>;
}# সত্যিকারের run, production build, React 19.3.1
connect stock-1
connected stock-1 theme = light
-- theme বদল ← কোনো পুনঃসংযোগ নেই
-- room বদল
disconnect stock-1
connect stock-2
connected stock-2 theme = dark ← সর্বশেষ theme দেখছে
Effect Event শুধু effect বা অন্য Effect Event-এর ভেতর থেকে ডাকা যায়। Render-এর সময়ে ডাকা, child-এ পাঠানো বা dependency-তে রাখা চলে না। Hook-এর নিয়ম মেনে component-এর শীর্ষেই লিখতে হয়। আর নথি স্পষ্ট বলে: dependency এড়ানোর কৌশল হিসেবে এটি ব্যবহার করবেন না, শুধু সেই যুক্তিতে ব্যবহার করুন যা সত্যিই effect থেকে ঘটা একটি ঘটনা।
useLayoutEffect: paint-এর আগে
useLayoutEffect দেখতে একই, কিন্তু DOM বসার পরে আর ব্রাউজার আঁকার আগে চলে, আর আঁকা আটকে রাখে। দরকার পড়ে তখনই, যখন DOM মেপে অবস্থান ঠিক করতে হয়, যেমন tooltip-এর উচ্চতা দেখে তাকে উপরে না নিচে বসাবেন, যাতে ঝিলিক না দেখা যায়। নথির সতর্কতা: এটি performance-এর ক্ষতি করতে পারে, তাই সম্ভব হলে useEffect-ই ব্যবহার করুন।
আরও গভীরে: React effect-গুলো Fiber node-এ তালিকা আকারে রাখে, আর render-এর সময় আগের dependency-র সাথে Object.is মিলিয়ে কোনটি চালাতে হবে তা চিহ্নিত করে (পর্ব ১৫)। StrictMode-এর দ্বিগুণ চলার পেছনে ছিল reusable state-এর পরিকল্পনা। React 19.2-এর <Activity> লুকানো অংশের state রেখে তার effect-এর cleanup চালায়, আর দেখানোর সময় আবার setup চালায়। তাই “mount একবারই হয়” ধরে লেখা effect ভবিষ্যতে আরও ভাঙবে। Effect-এ state বসানো নিয়ে নতুন set-state-in-effect lint নিয়মটিও এই দর্শনেরই অংশ।
পরীক্ষা ও ইন্টারভিউতে:
- Cleanup কখন চলে? Dependency বদলে পুনরায় চলার আগে, পুরোনো মান দিয়ে, আর unmount-এ।
- Development-এ effect দুবার চলে কেন? StrictMode cleanup-এর অভাব ধরতে setup, cleanup, setup চালায়; production-এ এটি হয় না।
- Object dependency-তে অসীম চক্র কেন? প্রতি render-এ নতুন object, তাই
Object.isমিথ্যা; effect-এর setState আবার render ঘটায়। - Search-এ race condition কীভাবে ঠেকাবেন? Cleanup-এ
ignore = trueঅথবাcontroller.abort(). useEffectআরuseLayoutEffect-এর পার্থক্য? দ্বিতীয়টি paint-এর আগে চলে এবং paint আটকে রাখে।
১০ · AI-এর যুগে effect যাচাই
Effect-এর বাগ AI-এর লেখা কোডে বিশেষ করে বিপজ্জনক, কারণ দ্রুত localhost-এ সব ঠিক দেখায়। Race condition লাগে ধীর network, leak লাগে বারবার পাতা ঘোরা, আর দ্বিগুণ POST লাগে ঠিক সেই সময়টি। AI প্রায়ই পুরোনো টিউটোরিয়ালের ধরন মেনে লেখে, যেগুলো ছিল class আর শুরুর দিকের hook-এর যুগের।
| AI-এর ভুল | কেন সমস্যা | যাচাইয়ের উপায় |
|---|---|---|
| Timer বা listener-এ cleanup নেই | Leak, দ্বিগুণ কাজ | StrictMode-এ দেখুন, পাতা ঘুরে আসুন |
| Fetch-এ race-এর ব্যবস্থা নেই | পুরোনো ফল নতুনকে ঢেকে দেয় | DevTools-এ network throttling চালান |
eslint-disable দিয়ে dependency চাপা | Stale closure লুকিয়ে থাকে | এমন লাইন খুঁজে ব্যাখ্যা চান |
| Derived মানে effect আর state | বাড়তি render, মুহূর্তের ভুল মান | Lint-এর set-state-in-effect দেখুন |
useEffect(async () => ...) | Cleanup-এর জায়গায় Promise যায় | Console আর lint দেখুন |
| Effect-এ POST বা payment | একই অর্ডার একাধিকবার যেতে পারে | Event handler-এ নিন, server-এ idempotency রাখুন (পর্ব ৩৩) |
useEvent import করা, বা বানানো option | React-এ এই নামে API নেই; নাম ছিল শুধু প্রস্তাবে | react.dev-এ নাম মেলান, build চালান |
| Fetch-এর জন্য অচেনা নতুন package | অদরকারি dependency, ঝুঁকি | Diff পড়ুন, package আসলেই আছে কি না দেখুন |
AI-কে শুধু “useEffect দিয়ে করে দাও” না বলে কাজের ধরন আর সীমা বলে দিন:
AI-এর লেখা effect হাতে পেলে
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১Class-এর তিনটি lifecycle method সবাই বুঝত। তাহলে React কেন “mount, update, unmount” ছেড়ে “মিল রাখা”-র ভাবনায় গেল?
Class-এর method সময় ধরে কোড ভাগ করত, উদ্দেশ্য ধরে নয়। একটি chat সংযোগের কথা ভাবুন। componentDidMount-এ connect, componentWillUnmount-এ disconnect, আর room বদলালে componentDidUpdate-এ দুটোই। একটি কাজ তিন জায়গায় ছড়িয়ে গেল, আর এক জায়গায় অন্য কাজের কোড জমল: একই componentDidMount-এ title, timer, data আনা সব একসাথে।
আসল বিপদ ছিল সেই তৃতীয় জায়গাটি। অনেকেই componentDidUpdate লিখতে ভুলে যেতেন। ফলে room বদলালেও পুরোনো room-এ সংযোগ থাকত, আর সেই বাগ পরীক্ষায় ধরা পড়ত না, কারণ পরীক্ষক সাধারণত পাতা খুলেই দেখেন, prop বদলে দেখেন না। ২০১৮ সালের React Conf-এ Hook প্রস্তাব করা হয়, আর ২০১৯ সালের শুরুতে React 16.8-এ তা আসে।
useEffect ভাবনাটিকে উল্টে দিল। প্রশ্ন আর “কখন চলবে” নয়, প্রশ্ন “এই মানগুলোর সাথে বাইরের জিনিসটি কেমন থাকবে, আর কীভাবে থামবে”। Mount আর update-এর ভেদ মুছে গেল, কারণ দুটোই একই মিল করার কাজ। Dan Abramov তাঁর “A Complete Guide to useEffect” প্রবন্ধে ঠিক এই মানসিক বদলটিই ব্যাখ্যা করেন। তাই effect-কে lifecycle-এর নতুন নাম ভাবলে পদে পদে অসুবিধা হয়।
এর দাম আছে। Class-এ ভুল করা সহজ ছিল, কিন্তু ভাবনাটি সরল ছিল। Effect-এ নিয়ম কম, কিন্তু closure, dependency আর reference equality-র ধারণা ছাড়া কোড বোঝা যায় না। তাই অনেকের কাছে useEffect React-এর সবচেয়ে কঠিন অংশ, আর সমালোচনাও ন্যায্য। exhaustive-deps lint, StrictMode আর নতুন নথি সেই দামই কমানোর চেষ্টা।
মূল উপলব্ধি: Effect-কে সময়ের ঘটনা না ভেবে একটি শর্ত ভাবুন: “এই মানগুলো থাকলে বাইরের জগৎ এমন থাকবে।” বাকি নিয়ম এই বাক্য থেকেই আসে।
প্র ০২StrictMode development-এ effect দুবার চালায়, এতে অনেকে বিরক্ত হন। React কি সত্যিই ব্যবহারকারীর কোড ভাঙার ঝুঁকি নিল? এর পেছনের যুক্তি কী?
React 18-এর ঘোষণায় (মার্চ ২০২২) কারণটি স্পষ্ট বলা ছিল। React ভবিষ্যতে এমন সুবিধা আনতে চায়, যেখানে UI-র একটি অংশ সরিয়ে আবার আগের state-সহ ফিরিয়ে আনা যাবে, যেমন ট্যাব বদলে ফিরে এলে সাথে সাথে আগের পর্দা। এজন্য component-গুলোকে একাধিকবার effect তৈরি ও ধ্বংস হওয়া সইতে হবে। Mount মাত্র একবার হয়, এই ধারণার ওপর লেখা কোড তখন ভেঙে যাবে। তাই পরীক্ষাটি আগেই development-এ আনা হলো: mount, নকল unmount, আগের state-সহ আবার mount.
পরিকল্পনাটি কথাতেই থাকেনি। React 19.2-এর <Activity> লুকানো অংশের effect-এর cleanup চালায়, state রেখে দেয়, আর দেখানোর সময় আবার setup চালায়। অর্থাৎ StrictMode যা নকল করত, তা এখন আসল ফিচারের আচরণ।
সমালোচনাও আছে, আর তা অযৌক্তিক নয়। Development-এ দুটি network অনুরোধ দেখলে নতুন ডেভেলপার ভাবেন কোডে বাগ আছে। Analytics-এর log দ্বিগুণ হয়, স্থানীয় ডেটাবেসে একই লেখা দুবার পড়ে। অনেকে তাই StrictMode তুলে দেন, বা useRef-এর flag বসান। কিন্তু এই দুটি পথই পরীক্ষার ফল মুছে ফেলে, সমস্যা নয়। আমাদের নকল socket-এ cleanup ছাড়া খোলা সংযোগ 2 হয়েছিল। ব্যবহারকারী পাতা ছেড়ে ফিরে এলে production-এও ঠিক এটাই হতো, শুধু ধরা পড়ত অনেক পরে, আর জটিল অবস্থায়।
একটি ব্যবহারিক সীমাও মনে রাখুন। দুবার চলা ক্ষতিকর হলে প্রথমে ভাবুন কাজটি effect-এর কি না। অর্ডার বা payment হলে উত্তর “না”। Data আনা হলে cancel বা cache লাগে, যা library দেয়। পুরো অ্যাপে একবারই চলার কাজ হলে নথি component-এর বাইরে রাখার পরামর্শ দেয়।
মূল উপলব্ধি: StrictMode ব্যবহারকারীর সামনে নয়, আপনার সামনে কোড ভাঙে। ভুল আগে ও সস্তায় ধরার জন্য এই বিরক্তি ছোট দাম।
প্র ০৩Dependency array-র একটি ছোট ভুল কীভাবে একটি বড় cloud প্রতিষ্ঠানের dashboard আর API-তে বিভ্রাটের অংশ হতে পারে? এই ঘটনা থেকে কোন প্রকৌশল শিক্ষা নেওয়া যায়?
Cloudflare তাদের ১২ সেপ্টেম্বর ২০২৫-এর ঘটনার যে বিশ্লেষণ প্রকাশ করেছে, সেখানে বলা হয়েছে, dashboard-এর একটি useEffect-এর dependency array-তে ভুল করে এমন একটি object রাখা হয়েছিল, যা প্রতিবার state বা props বদলালে নতুন তৈরি হতো। React সেটিকে সবসময় “নতুন” ধরল। ফলে যে API ডাক একবার হওয়ার কথা, তা এক dashboard render-এই বহুবার হলো। এই ডাক যাচ্ছিল Tenant Service-এ, যা API অনুরোধের অনুমোদনের অংশ। নতুন version ছাড়ার সময় সেই service-এর ওপর চাপ বেড়ে তা ভেঙে পড়ে, আর dashboard ও অনেক API প্রভাবিত হয়।
লক্ষ করুন, একটি কারণে এত বড় ফল হয়নি। সমস্যাটি ছিল কয়েকটি স্তর মিলে। প্রথমত, frontend-এর বাগ বাড়তি অনুরোধ বানাল। দ্বিতীয়ত, service-টির ক্ষমতা সেই চাপের জন্য যথেষ্ট ছিল না। তৃতীয়ত, service-টি অনুমোদনের পথে ছিল, তাই এর ব্যর্থতা অন্য অনেক API-তে ছড়াল। সবগুলোই একসাথে ঘটায় ছোট ভুলটি বড় হলো। এটি system design-এর পরিচিত ছবি, যাকে অনেকে Swiss cheese model বলেন।
সংস্থাটি যে প্রতিকারগুলোর কথা বলেছে, তাও শিক্ষণীয়: বাগের hotfix, পুনরায় চেষ্টায় random delay যোগ করা, অনুরোধটি retry কি না তা চিহ্নিত করা, ধাপে ধাপে rollout যাতে খারাপ update নিজে থেকে ফেরে, service-এ বেশি সম্পদ, আর আগেভাগে সতর্কতা। লক্ষ করুন, শুধু React কোড ঠিক করাই সমাধান হিসেবে যথেষ্ট ধরা হয়নি।
আপনার জন্য শিক্ষা তিনটি। ছোট স্তরে: exhaustive-deps আর object-এর সতর্কতা CI-তে রাখুন, আর DevTools-এর Network ট্যাবে দেখুন এক পাতায় একই অনুরোধ কতবার যায়। Backend-এ: client-কে নিরীহ ধরবেন না। Rate limit, cache আর timeout রাখুন, কারণ আপনার নিজের frontend-ও ভুল করে “আক্রমণ” করতে পারে। আর খেয়াল করুন, production-এ React কোনো সতর্কতা দেয় না, আমাদের run-ও তাই দেখিয়েছে। ধরার দায়িত্ব monitoring-এর।
মূল উপলব্ধি: Frontend-এর একটি লাইন backend-এর লোড ঠিক করে দিতে পারে। প্রতিটি effect-কে তাই শুধু UI-র কোড নয়, সিস্টেমের চাপের উৎস হিসেবেও পড়ুন।
প্র ০৪Effect-এ data আনা বৈধ, তবু অনেকে বলেন এটি এড়িয়ে চলুন। কেন? আর ছোট project-এও কি library লাগবে?
React-এর নথি নিজেই effect-এ fetch-এর কয়েকটি দুর্বলতা বলে। প্রথমত, effect সার্ভারে চলে না, তাই প্রথম HTML-এ data থাকে না, আর JavaScript লোড হওয়ার পরে আবার অনুরোধ যায়। দ্বিতীয়ত, parent আর child দুজনই নিজের data আনলে একটির পর একটি অনুরোধ হয়, যাকে বলে network waterfall. তৃতীয়ত, cache নেই: পাতা ছেড়ে ফিরলেই আবার সব আনতে হয়, আর Back বোতামে আগের পর্দা সাথে সাথে আসে না। চতুর্থত, race condition আর loading-এর কোড প্রতিবার হাতে লিখতে হয়।
একটি বাস্তব অঙ্ক দেখুন। “বাজারঘর”-এর পণ্যের পাতায় পণ্য, রিভিউ আর সম্পর্কিত পণ্য, প্রতিটি 200 ms. Parent শেষ হলে child mount হলে, আর child শেষ হলে তার child, তবে মোট 600 ms. একসাথে আনলে 200 ms-এ শেষ, যদি server সামলাতে পারে। এই 400 ms শুধু কোডের গঠনের জন্য নষ্ট।
TanStack Query, SWR বা framework-এর loader এই সমস্যাগুলোই সামলায়: একই key-তে একটিই অনুরোধ, cache, পটভূমিতে নতুন করা, retry আর বাতিল। Next.js-এর Server Component-এ data আগে সার্ভারেই আসে, ফলে সমস্যাটির বড় অংশ শুরুতেই থাকে না।
তবে “effect কখনো নয়” বলাও বাড়াবাড়ি। একটি ছোট admin পাতা, যেখানে একটিমাত্র তালিকা আসে, সেখানে ঠিকমতো cleanup-সহ effect পুরোপুরি চলে, আর নতুন dependency-র খরচ বাঁচে। Library-র নিজের bundle, শেখার সময় আর version বদলের ঝুঁকি আছে। সমাজে মতভেদও আছে: কেউ শুরু থেকেই library, কেউ দরকার পড়লে। শেখার জন্য অন্তত একবার হাতে লেখা ভালো, নইলে library কী সমাধান করছে তা বোঝা যায় না, আর AI-এর ভুলও ধরা যায় না।
মূল উপলব্ধি: প্রশ্নটি “effect ভালো না খারাপ” নয়। প্রশ্ন হলো এই পাতায় cache, SSR আর একাধিক data কতটা লাগে। প্রয়োজন বাড়লে library-র দাম ফেরত আসে।
প্র ০৫“ঘটনা” আর “মিল রাখা”-র এই ভাগ কেন এত গুরুত্বপূর্ণ? useEffectEvent আসতে ২০২২ থেকে ২০২৫ পর্যন্ত সময় লাগল কেন?
Effect-এর ভেতরে অনেক সময় দুই ধরনের যুক্তি মিশে থাকে। সংযোগ করা “মিল রাখা”, যা roomId বদলালে আবার হওয়া উচিত। “সংযোগ হলে theme মেনে বার্তা দেখানো” একটি ঘটনা, যা theme বদলালে আবার হওয়ার কথা নয়, কিন্তু সর্বশেষ theme পড়তে চায়। পুরোনো ভাষায় এর সমাধান ছিল না। Dependency দিলে অদরকারি পুনঃসংযোগ, না দিলে stale closure, আর lint চাপা দিলে পরের বাগের রাস্তা খোলা।
মানুষ তাই নিজের কৌশল বানাত: useRef-এ সর্বশেষ মান রাখা, যাকে অনেকে “latest ref” pattern বলেন। এটি কাজ করত, কিন্তু কোড জটিল হতো আর ভুলের জায়গা থাকত। ২০২২ সালের মে মাসে Dan Abramov useEvent নামে একটি RFC খোলেন, যা event handler-এর জন্য স্থির পরিচয়ের ফাংশন দিতে চেয়েছিল।
প্রস্তাবটি সোজা পথে এগোয়নি। সেই বছরের শেষ দিকে RFC-র আলোচনায় জানানো হয়, এটি নিয়ে আবার ভাবা হবে, আর ডিসেম্বরের দিকে নথিতে নাম বদলে হয় useEffectEvent. RFC-টি বন্ধ হয়ে যায়। চূড়ান্ত রূপের পরিধি অনেক ছোট: শুধু effect থেকে ডাকা যায়, render-এ বা child-এ নয়, সব ধরনের event handler-এর জন্য নয়। এরপর এটি দীর্ঘদিন experimental ছিল। React 19.2 (অক্টোবর ২০২৫) এটি প্রকাশ করে, আর lint plugin-ও শেখে যে এটি dependency-তে যাবে না। নথি আবার সতর্ক করে, এর পরিচয় ইচ্ছা করেই প্রতি render-এ বদলায়। এটি ছোট কিন্তু বুদ্ধিদীপ্ত নকশা: কেউ dependency-তে দিলে effect প্রতি render-এ চলে, ভুলটি সাথে সাথে চোখে পড়ে।
এখান থেকে দুটি কথা শেখার আছে। ভালো API-র জন্য সময় লাগে, আর ছোট পরিধি প্রায়ই বড় প্রতিশ্রুতির চেয়ে ভালো। আর AI যদি পুরোনো ব্লগ থেকে useEvent লেখে, সেটি এখন React-এ নেই; নথি মিলিয়ে দেখা দরকার।
মূল উপলব্ধি: প্রতিটি লাইনে জিজ্ঞেস করুন, এটি মিল রাখছে, না কোনো ঘটনায় সাড়া দিচ্ছে। উত্তরই বলে দেয় কোডটি কোথায় যাবে।
অনুশীলন
হিসাব, ফল অনুমান, হাতে-কলমে পরীক্ষা আর নকশার সমালোচনা, চার ধরনের কাজ।
-
হিসাব: একজন ব্যবহারকারী সার্চ বাক্সে
basmati(৭ অক্ষর) লিখলেন, প্রতি অক্ষরের ব্যবধান 150 ms. প্রতিটি অক্ষরে effect একটি অনুরোধ পাঠায়, উত্তর আসে ঠিক 400 ms পরে। প্রথম অক্ষরের সময় ধরুন 0 ms. (ক) মোট কয়টি অনুরোধ বেরোবে? (খ) AbortController থাকলে কয়টি উত্তর শেষ পর্যন্ত পৌঁছাবে, আর শেষ ফল কখন? (গ) 300 ms-এর debounce দিলে, অর্থাৎ শেষ অক্ষরের 300 ms পরে একটিই অনুরোধ গেলে, ফল কখন আসবে?(ক) ৭টি। শেষ অক্ষরের সময়
6 × 150 = 900 ms.(খ) প্রতিটি অনুরোধ 150 ms পরেই বাতিল হয়, কারণ
150 < 400. তাই ৬টি বাতিল, ১টি উত্তর পৌঁছায়। শেষ ফল900 + 400 = 1300 ms-এ।(গ)
900 + 300 + 400 = 1600 ms, অনুরোধ মাত্র ১টি। Debounce 300 ms দেরি বাড়ায়, কিন্তু server-এর ওপর ৭টির বদলে ১টি অনুরোধ যায়। বাতিল হওয়া অনুরোধ server-এ পৌঁছে থাকতে পারে, তাই abort server-এর চাপ পুরো কমায় না। বাস্তবে দুটিই একসাথে ব্যবহার হয়। -
ফল অনুমান করুন: Component-টি প্রথমে
id = 1নিয়ে mount হলো, তারপর একইid = 1-এ আবার render, তারপরid = 2, তারপর unmount. StrictMode নেই। আউটপুট কী হবে? তারপর StrictMode চালু করে development-এ চালালে কোন অংশ বদলাবে?function Predict({ id }) { useEffect(() => { console.log('setup', id); return () => console.log('cleanup', id); }, [id]); useEffect(() => { console.log('always'); }); return <p>{id}</p>; }সত্যিকারের run-এর আউটপুট (React 19.3.1):
setup 1 always always cleanup 1 setup 2 always cleanup 2দ্বিতীয়
alwaysএসেছে একই id-র render থেকে, কারণ ওই effect-এ dependency array নেই। StrictMode-এ শুধু mount-এর অংশ বদলায়:setup 1,always,cleanup 1,setup 1,always; বাকিটা একই। -
নিজে পরীক্ষা করুন: ৬ নম্বর অংশের
race-condition.htmlblock-এ delay বদলে{ চাল: 100, ডাল: 300 }করে চালান। “ভুল” কোডের শেষ ফল কী হয়? এ থেকে race condition পরীক্ষার ব্যাপারে কী বোঝা যায়? তারপর নিজের project-এnpm i -D eslint-plugin-react-hooksবসিয়ে একটি কোডে dependency মুছেnpx eslintচালান।“ভুল” কোডেও শেষে তালিকায় ডাল-এর ফল থাকে। চাল-এর উত্তর 100 ms-এ, ডাল-এর উত্তর
50 + 300 = 350 ms-এ, অর্থাৎ ক্রম ঠিক থাকে। “ভুল” কোড মাঝে অল্প সময় চাল-এর ফল দেখায়, তবে শেষে ঠিক হয়। “ঠিক” কোড চাল-এর ফল ফেলে দেয়।শিক্ষা: এই বাগ নির্ভর করে উত্তরের ক্রমের ওপর। দ্রুত localhost-এ প্রায়ই সব ঠিক দেখায়, তাই “আমার কম্পিউটারে চলে” এখানে কোনো প্রমাণ নয়। Throttling বা নকল delay দিয়ে পরীক্ষা করতে হয়। ESLint-এর ফল ৩ নম্বর অংশের মতো
missing dependencyসতর্কতা দেবে; আপনার project-এ ফাইলের নাম আর লাইন আলাদা হবে। -
নকশার সমালোচনা: AI “বাজারঘর”-এর checkout-এর জন্য এই কোড দিল। পড়ে অন্তত চারটি সমস্যা বের করুন আর কী বদলাবেন বলুন।
function Checkout({ items, coupon }) { const [total, setTotal] = useState(0); const [paid, setPaid] = useState(false); useEffect(() => { setTotal(calc(items, coupon)); }, [items, coupon]); useEffect(() => { window.addEventListener('beforeunload', warn); }, []); useEffect(() => { if (paid) sendOrder(items); }, [paid]); // eslint-disable-line return <button onClick={() => setPaid(true)}>{total} টাকা দিন</button>; }- Derived state:
totalstate নয়। Render-এconst total = calc(items, coupon)লিখুন। এখন প্রথম render-এ 0 টাকা দেখায়। - Listener-এর cleanup নেই:
removeEventListenerফেরত দিন। না হলে checkout থেকে সরে গেলেও সতর্কতা থেকে যাবে, আর ফিরলে আরেকটি যোগ হবে। - ঘটনার কাজ effect-এ: অর্ডার পাঠানো বোতাম চাপার ফল, তাই
handlePayনামের event handler-এ যাবে। - চাপা দেওয়া lint:
itemsdependency থেকে বাদ, এবং সেটি চাপা দেওয়া হয়েছে। এটি stale closure-এর সরাসরি ঝুঁকি। - নিরাপত্তা: Frontend-এর
totalদামের প্রমাণ নয়। Server নিজে দাম হিসাব করবে, আর দুবার বোতাম চাপলেও একটি অর্ডার হবে, তার জন্য idempotency লাগবে (পর্ব ৪৩, পর্ব ৬৫)। - যেটি ভালো:
beforeunloadসত্যিই বাইরের ঘটনা, তাই effect-এই থাকবে, শুধু cleanup চাই।
- Derived state:
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি component-এর useEffect-এর dependency বদলালে ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Parent নতুন prop পাঠায়
- React নতুন মান নিয়ে component ফাংশন চালায়
- React DOM-এ বদল বসায় (commit)
- আগের effect-এর cleanup পুরোনো মান দিয়ে চলে
- নতুন effect-এর setup নতুন মান দিয়ে চলে
কার্টের পণ্যের দাম যোগ করে মোট দেখাতে হবে। React-এ সবচেয়ে ভালো পথ কোনটি?
useEffect-এ যোগ করে আলাদা state-এ রাখা- প্রতি সেকেন্ডে
setInterval-এ মোট হিসাব করা - Render-এর সময়েই সরাসরি হিসাব করা
useLayoutEffect-এ হিসাব করা
useEffect(setup, [x])-এ x মান 1 থেকে 2 হলো, আর setup একটি cleanup ফেরত দেয়। কোন ক্রমটি সঠিক?
- x = 2 দিয়ে setup, তারপর x = 1 দিয়ে cleanup
- x = 1 দিয়ে cleanup, তারপর x = 2 দিয়ে setup
- x = 2 দিয়ে cleanup, তারপর x = 2 দিয়ে setup
- শুধু x = 2 দিয়ে setup; cleanup চলে শুধু unmount-এ
একটি component <StrictMode>-এর ভেতরে আছে, আর এর একটি effect-এর dependency []. Development-এ আর production build-এ প্রথম mount-এ setup কতবার চলবে?
- দুই জায়গাতেই ১ বার
- দুই জায়গাতেই ২ বার
- Development-এ ১ বার, production-এ ২ বার
- Development-এ ২ বার, production-এ ১ বার
Component-এর ভেতরে const opts = { page: 1 }; লেখা, আর একটি effect-এর dependency [opts]. Effect-এর ভেতরে একটি state বদলায়। কী হবে?
- Effect প্রতি render-এ চলবে, আর একটি চক্র তৈরি হবে
- ভেতরের মান একই, তাই effect একবারই চলবে
- React object-এর প্রতিটি ঘর গভীরে মিলিয়ে দেখবে
- Object রাখা যায় না, তাই compile error হবে
সার্চ বাক্সে দ্রুত লেখার সময় পুরোনো লেখার উত্তর পরে এসে নতুন ফল ঢেকে দিচ্ছে। সবচেয়ে সঠিক সমাধান কোনটি?
- Dependency থেকে সার্চের লেখা বাদ দেওয়া
setTimeout-এ 500 ms অপেক্ষা করে state বসানো- Cleanup-এ
ignore = trueঅথবাcontroller.abort() useEffect-এর বদলেuseLayoutEffectব্যবহার করা
useEffect(() => { const id = setInterval(() => setN(n + 1), 1000); return () => clearInterval(id); }, []); — n শুরু হয় 0 থেকে। কয়েক সেকেন্ড পরে পর্দায় কত থাকবে, আর কেন?
- 0, কারণ interval চলে না
- 1, কারণ ফাংশনটি প্রথম render-এর n = 0 মনে রাখে
- প্রতি সেকেন্ডে এক করে বাড়বে
- React অসীম চক্রের error দেবে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Effect বাইরের জগতের সাথে মিল রাখে। বাইরের সিস্টেম নেই মানে সম্ভবত effect-ও নেই: হিসাব render-এ, ক্লিকের কাজ event handler-এ।
- Dependency আপনি বাছেন না, কোড ঠিক করে দেয়। ESLint মানুন, চাপা দেবেন না। Object আর function প্রতি render-এ নতুন হতে পারে।
- Cleanup setup-এর আয়না, আর এটি পুনরায় চলার আগেও চলে, পুরোনো মান নিয়ে। StrictMode এর অভাব ধরিয়ে দেয়।
- Data আনলে তিন দায়িত্ব: loading, error, আর
ignoreবাAbortControllerদিয়ে race condition ঠেকানো। - Stale closure ঠেকান সৎ dependency, updater function, আর সত্যিকারের ঘটনার জন্য
useEffectEventদিয়ে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৪ · Hook-এর নিয়ম আর custom hook — useRef, useMemo আর নিজের hookপরবর্তী পর্বএই পর্বের কোড একটি নিজের hook-এ তোলা।
- ফুল-স্ট্যাক ওয়েব ফ্রেমওয়ার্ক · কোর্স হোম৯৩টি পর্বপুরো কোর্সের পথ। Data-র গভীরে যেতে পর্ব ১৯.
- পর্ব ০৩ · Rendering-এর মানচিত্রআগের পর্বEffect ব্রাউজারে চলে, তাই CSR-এ data কখন আসে।
- JavaScript · Closureপাশের ট্র্যাকStale closure বোঝার ভিত্তি।
- JavaScript · fetch আর HTTPপাশের ট্র্যাকPromise, status আর JSON.
- JavaScript · Event loopপাশের ট্র্যাকTimer আর Promise কখন চলে।