শিরোনাম: ‘Effects দিয়ে Synchronize করা’
কিছু কিছু React কম্পোনেন্টকে বাইরের কোন সিস্টেমের সঙ্গে কাজ করাতে হয়। যেমন ধরেন, আপনি React স্টেট দিয়ে একটা non-React কম্পোনেন্ট কন্ট্রোল করতে চান, অথবা কোনো সার্ভারে কানেকশন করতে চান, অথবা যখন কোনো কম্পোনেন্ট স্ক্রিনে আসে তখন একটা রিপোর্ট (analytics log) পাঠাতে চান। Effect ব্যবহার করলে আপনি রেন্ডারিং শেষ হওয়ার পর কিছু কোড চালাতে পারেন, যাতে করে React-এর বাইরে যেকোনো সিস্টেমের সাথে কম্পোনেন্ট মিলিয়ে নেওয়া (synchronize) যায়।
এই পর্বে আপনি শিখবেন:
- Effect কী জিনিস
- Event আর Effect এর পার্থক্য
- কিভাবে কম্পোনেন্টে Effect লিখতে হয়
- কিভাবে অপ্রয়োজনীয়ভাবে বারবার Effect চালানো বন্ধ করবেন
- ডেভেলপমেন্টে কেন Effect দুইবার চলে এবং এটা ঠিক করার উপায়
Effect কী? আর Event থেকে এটা আলাদা কেন?
Effect বুঝতে হলে আগে React কম্পোনেন্টে দুই ধরনের কোডের কথা জানতে হবে:
-
Rendering কোড: এই কোডটা কম্পোনেন্টের উপরের দিকে থাকে। এখানে আপনি props আর state ব্যবহার করে যেটা স্ক্রিনে দেখাতে চান সেটা তৈরি করেন। এই কোডটা শুধু হিসাব-নিকাশ (calculation) করে, কোনো কাজ (action) করে না। যেমন গাণিতিক ফর্মুলা।
-
Event handler: এই কোডটা কম্পোনেন্টের ভেতরে থাকে, এবং এটা আসলে কিছু করে। যেমন ইনপুট ফিল্ড আপডেট করা, সার্ভারে ডেটা পাঠানো, বা অন্য পেইজে নেওয়া। এইগুলা user action (যেমন ক্লিক বা টাইপ করা) এর পরে চলে।
কিন্তু সব সময় user action থাকেই না। ধরেন, একটা ChatRoom
কম্পোনেন্ট আছে, যেটা স্ক্রিনে এলেই চ্যাট সার্ভারে কানেক্ট হতে হবে। এটা কোনো হিসাব-নিকাশ না (side effect), আর এটা কোনো ক্লিকেও হয় না—কম্পোনেন্ট স্ক্রিনে এলেই হয়।
এই জায়গাতেই Effect দরকার হয়। মানে, Effect এমন কোড চালায় যেটা রেন্ডারিং এর কারণে হয়, না যে user কিছু করলো বলে। যেমন, মেসেজ পাঠানো মানে user একটা ক্লিক করেছে—এটা event। আর সার্ভারে কানেক্ট হওয়া মানে কম্পোনেন্ট স্ক্রিনে এসেছে—এটা effect।
Effect রেন্ডার শেষ হওয়ার পর চলে। তখনই React-এর কম্পোনেন্টকে বাইরের সিস্টেম (যেমন সার্ভার, থার্ড পার্টি লাইব্রেরি) এর সঙ্গে যুক্ত করা যায়।
এখানে "Effect" মানে React-এর স্পেশাল কোড যেটা রেন্ডারিং শেষ হলে চলে। আর সাধারণ প্রোগ্রামিং-এ যেটাকে side effect বলে, সেটা আলাদা।
নিশ্চিতভাবেই! নিচে বাংলাদেশের মানুষ যেন সহজে বুঝতে পারে, সেভাবে সহজ ভাষায় অনুবাদ করলাম:
সবসময় Effect দরকার হয় না
কম্পোনেন্টে Effect ব্যবহার করার আগে একটু চিন্তা করুন। মনে রাখবেন, Effect সাধারণত তখনই ব্যবহার করা হয় যখন আপনাকে React-এর কোডের বাইরে গিয়ে কোনো বাইরের সিস্টেমের (যেমন: ব্রাউজার API, থার্ড পার্টি টুলস, বা নেটওয়ার্ক) সাথে মিলিয়ে কাজ করতে হয়।
যদি আপনার Effect শুধুমাত্র এক state এর উপর ভিত্তি করে আরেকটা state সেট করে, তাহলে হয়তো Effect দরকারই নেই। বিস্তারিত জানতে এই লিংকটি দেখুন।
কিভাবে একটি Effect লিখবেন
Effect লেখার জন্য এই তিনটি ধাপ অনুসরণ করুন:
-
Effect ডিক্লেয়ার করুন। ডিফল্টভাবে, আপনার Effect প্রতিবার commit হওয়ার পর চলবে।
-
Effect-এর dependency দিন। বেশিরভাগ Effect প্রতিবার রেন্ডার হওয়ার পর চালানো ঠিক না। বরং তখনই চলা উচিত যখন সত্যিই প্রয়োজন। যেমন: কোনো কম্পোনেন্ট স্ক্রিনে আসলে fade-in animation চলবে। আবার চ্যাটরুমে কানেক্ট/ডিসকানেক্ট হওয়া উচিত তখনই, যখন কম্পোনেন্ট স্ক্রিনে আসে বা চলে যায়, অথবা চ্যাটরুম বদলায়। এভাবে কখন Effect চলবে সেটা নিয়ন্ত্রণ করতে dependency দিতে হয়।
-
প্রয়োজন হলে cleanup ফাংশন দিন। কিছু Effect এমন হয় যেগুলা শেষ করার, undo করার বা পরিষ্কার করার নিয়ম থাকা দরকার। যেমন: "connect" এর বিপরীতে "disconnect" থাকা দরকার, "subscribe" এর বিপরীতে "unsubscribe", আর "fetch" এর ক্ষেত্রে হয় "cancel" করতে হবে না হয় "ignore" করতে হবে। এটা করতে হলে Effect এর ভেতরে একটা cleanup function রিটার্ন করতে হয়।
চলুন, এবার এই তিনটা ধাপ একে একে বিস্তারিতভাবে দেখি।
অবশ্যই! নিচে সহজ বাংলায় বাংলাদেশের মানুষের দৃষ্টিভঙ্গি অনুযায়ী অনুবাদ করা হলো:
ধাপ ১: Effect ডিক্লেয়ার করুন
কম্পোনেন্টে Effect ব্যবহার করতে হলে, প্রথমে React থেকে useEffect
হুক ইমপোর্ট করুন:
import { useEffect } from "react";
তারপর কম্পোনেন্টের ভেতরে, একদম ওপরেই useEffect
ব্যবহার করুন এবং এর ভেতরে কিছু কোড লিখুন:
function MyComponent() {
useEffect(() => {
// এই কোডটা *প্রতিবার* রেন্ডার হওয়ার পর চালু হবে
});
return <div />;
}
React যখনই আপনার কম্পোনেন্ট রেন্ডার করে, প্রথমে স্ক্রিন আপডেট করে, তারপর useEffect
এর কোড চালায়। অর্থাৎ, useEffect
মূলত কোড চালানোর কাজটাকে একটু দেরিতে করে, যেন স্ক্রিন আপডেট শেষ হলে তারপর কোডটা চলে।
চলুন দেখি কীভাবে Effect দিয়ে কোনো বাইরের সিস্টেমের সাথে মিলিয়ে কাজ করা যায়। ধরুন, একটা <VideoPlayer>
কম্পোনেন্ট আছে। আপনি চাইছেন isPlaying
নামে একটি prop দিয়ে ভিডিও চালু বা বন্ধ করতে:
<VideoPlayer isPlaying={isPlaying} />
এই VideoPlayer
কম্পোনেন্ট ব্রাউজারের বিল্ট-ইন <video>
ট্যাগ (opens in a new tab) ব্যবহার করে:
function VideoPlayer({ src, isPlaying }) {
// TODO: এখানে isPlaying অনুযায়ী কিছু করতে হবে
return <video src={src} />;
}
কিন্তু <video>
ট্যাগে isPlaying
নামে কোনো prop নেই। ভিডিও চালু বা বন্ধ করতে হলে আপনাকে DOM এলিমেন্টের উপর play()
অথবা pause()
মেথড ব্যবহার করতে হবে।
এখানে আপনাকে isPlaying
prop এর মান অনুযায়ী ভিডিও চালু বা বন্ধ করতে হবে। এজন্য ভিডিও ট্যাগের DOM এ অ্যাক্সেস লাগবে।
প্রথমে ভিডিও এলিমেন্টের রেফারেন্স পেতে useRef
ব্যবহার করতে হবে।
অনেকে ভুল করে play()
বা pause()
সরাসরি রেন্ডারের সময়েই কল করতে চায়, কিন্তু এটা ঠিক না:
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
if (isPlaying) {
ref.current.play(); // রেন্ডারের সময় কল করা ঠিক না
} else {
ref.current.pause(); // এটা করলে error হতে পারে
}
return <video ref={ref} src={src} loop playsInline />;
}
কেন এটা ঠিক না? কারণ রেন্ডারের সময় ভিডিও DOM এখনো তৈরি হয়নি! JSX রিটার্ন করার আগে DOM তৈরি হয় না, তাই ref.current
তখন null
থাকে।
✅ সঠিক সমাধান:
রেন্ডার শেষ হলে কাজ করার জন্য useEffect
ব্যবহার করুন:
import { useEffect, useRef } from "react";
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});
return <video ref={ref} src={src} loop playsInline />;
}
এখানে React আগে DOM তৈরি করে, তারপর useEffect
চলে এবং play()
বা pause()
কল করে।
যখনই VideoPlayer
কম্পোনেন্ট রেন্ডার হয় (প্রথমবার অথবা আপডেট হলে), এই ধাপগুলো হয়:
- React স্ক্রিনে
<video>
দেখায়। - তারপর
useEffect
চলে। isPlaying
অনুযায়ীplay()
বাpause()
হয়।
এভাবে isPlaying
এর মানের সাথে ভিডিওর অবস্থা সঠিকভাবে মিলিয়ে রাখা যায়।
⚠️ একটি গুরুত্বপূর্ণ ব্যাপার (Pitfall):
useEffect
ডিফল্টভাবে প্রতিবার রেন্ডারের পর চলে। নিচের কোডটি একটি অসীম লুপ (infinite loop) তৈরি করবে:
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // এটা বারবার চলবে
});
এখানে setCount()
state পরিবর্তন করে → নতুন রেন্ডার হয় → আবার useEffect()
চলে → আবার setCount()
... এভাবে লুপ চলে।
সাধারণভাবে, useEffect
বাইরের সিস্টেম (যেমন ভিডিও, API, ব্রাউজার API ইত্যাদি) এর সাথে কম্পোনেন্টকে sync করার জন্য ব্যবহার করা হয়। শুধু state পরিবর্তনের জন্য Effect লাগেই না। এখানে বিস্তারিত দেখুন।
এখানে React-এর useEffect
হুকের দ্বিতীয় ধাপ—Effect dependencies নির্দিষ্ট করা—সম্পর্কে একটি সহজ ও স্পষ্ট বাংলা অনুবাদ দেওয়া হলো:
ধাপ ২: Effect dependencies নির্দিষ্ট করা
ডিফল্টভাবে, useEffect
প্রতি রেন্ডারেই চলে। কিন্তু প্রায় সময়, আপনি এটা চাইবেন না, কারণ:
- কখনও এটা ধীর হয়। বাইরের কোন সিস্টেমের সাথে সিঙ্ক করতে সময় লাগে, তাই এটা তখনই করা উচিত যখন সত্যিই প্রয়োজন। উদাহরণস্বরূপ, আপনি প্রতিবার টাইপ করলে চ্যাট সার্ভারে রিকানেক্ট করতে চান না।
- কখনও এটা ভুল হয়। ধরুন আপনি একটি fade-in অ্যানিমেশন চালাতে চান যখন কম্পোনেন্ট প্রথমবার আসে, কিন্তু সেটা প্রতিবার টাইপ করলে চললে সেটা ভুলভাবে কাজ করবে।
এই সমস্যা বোঝাতে নিচে একটি উদাহরণ কোড দেওয়া হয়েছে যেখানে console.log
দিয়ে দেখা যাবে যে প্রতিবার ইনপুটে টাইপ করলে useEffect
আবার চলে:
import { useState, useRef, useEffect } from "react";
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
console.log("Calling video.play()");
ref.current.play();
} else {
console.log("Calling video.pause()");
ref.current.pause();
}
});
return <video ref={ref} src={src} loop playsInline />;
}
export default function App() {
const [isPlaying, setIsPlaying] = useState(false);
const [text, setText] = useState("");
return (
<>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? "Pause" : "Play"}
</button>
<VideoPlayer
isPlaying={isPlaying}
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
/>
</>
);
}
এই মুহূর্তে useEffect
প্রতিবার রেন্ডারে চলে। কিন্তু আপনি চাইলে এটিকে প্রয়োজন ছাড়া চালানো থেকে বিরত রাখতে পারেন—এইজন্য useEffect
-এর দ্বিতীয় আর্গুমেন্ট হিসেবে একটি dependency array দিতে হবে।
শুরুতে আপনি যদি খালি []
দেন:
useEffect(() => {
// ...
}, []);
তাহলে React আপনাকে একটি এরর দেখাবে:
React Hook useEffect has a missing dependency: 'isPlaying'
কারণ useEffect
এর ভিতরের কোড isPlaying
-এর উপর নির্ভর করে, কিন্তু সেটা []
এর মধ্যে উল্লেখ করা হয়নি। সমাধান হিসেবে আপনাকে isPlaying
array-তে দিতে হবে:
useEffect(() => {
if (isPlaying) {
// ...
} else {
// ...
}
}, [isPlaying]); // নির্ভরশীলতা ঘোষণা করা হলো
এখন useEffect
কেবল তখনই চলবে যখন isPlaying
-এর মান আগের রেন্ডারের চেয়ে পরিবর্তিত হয়। টাইপ করলে এখন Effect আর চলবে না, কিন্তু Play/Pause বাটনে ক্লিক করলে চলবে।
React এই dependency গুলো Object.is
পদ্ধতিতে তুলনা করে। আপনি যদি ভুলভাবে dependency বাদ দেন, React lint error দেখিয়ে সতর্ক করবে।
👉 মনে রাখবেন: আপনি নিজের ইচ্ছামতো dependency বেছে নিতে পারবেন না। যদি আপনি চান কোনো dependency বাদ দিতে, তাহলে আপনার Effect কোড এমনভাবে লিখতে হবে যাতে সেই মানটির প্রয়োজন না পড়ে।
গুরুত্বপূর্ণ পার্থক্য
useEffect(() => {
// প্রতিবার রেন্ডারে চলে
});
useEffect(() => {
// কেবল প্রথমবার component আসলে (mount) চলে
}, []);
useEffect(() => {
// প্রথমবার এবং যখন a বা b বদলায় তখন চলে
}, [a, b]);
পরবর্তী ধাপে আমরা “mount” বলতে কী বোঝায় তা বিস্তারিতভাবে দেখবো।
রেফারেন্স (ref) কেন dependency array থেকে বাদ দেওয়া হয়েছে?
এই Effect-এ ref
এবং isPlaying
—দুটোই ব্যবহার করা হয়েছে, কিন্তু শুধুমাত্র isPlaying
-কেই dependency হিসেবে ঘোষণা করা হয়েছে:
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying]);
এর কারণ হলো, ref
অবজেক্টের একটি স্থির পরিচয় (stable identity) থাকে: React নিশ্চিত করে যে, প্রতিবার useRef
কল করলে আপনি একই অবজেক্টই পাবেন। এটি কখনো পরিবর্তিত হয় না, তাই এটি কখনোই একা একা Effect পুনরায় চালানোর কারণ হবে না। সুতরাং আপনি এটি include করেন বা না করেন—দুইভাবেই ঠিক আছে।
চাইলেও আপনি এমনভাবে লিখতে পারেন:
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying, ref]);
useState
থেকে পাওয়া set
ফাংশনগুলো এরও একইভাবে স্থির পরিচয় থাকে, তাই সেগুলোও অনেক সময় dependency array-এ বাদ থাকে। যদি লিন্টার কোনো ত্রুটি না দেখায়, তবে এই বাদ দেওয়া নিরাপদ।
তবে এই ধরনের স্থির dependency বাদ দেওয়া কেবল তখনই কাজ করে, যখন লিন্টার বুঝতে পারে যে অবজেক্টটি আসলেই স্থির। উদাহরণস্বরূপ, যদি ref
প্যারেন্ট কম্পোনেন্ট থেকে পাঠানো হয়, তবে আপনাকে তা dependency array-এ অবশ্যই দিতে হবে। কারণ আপনি নিশ্চিত নন যে প্যারেন্ট কম্পোনেন্ট সবসময় একই ref
পাঠাবে, নাকি শর্ত অনুযায়ী বিভিন্ন ref
পাঠাবে। তাই, এ ক্ষেত্রে আপনার Effect নির্ভরশীল হয়ে পড়ে ঠিক কোন ref
পাঠানো হয়েছে তার ওপর।
নিচে দেওয়া হলো অনুবাদটি সহজ বাংলায়:
ধাপ ৩: প্রয়োজন হলে Cleanup যোগ করুন
চলুন এখন ভিন্ন একটি উদাহরণ দেখি। আপনি একটি ChatRoom
কম্পোনেন্ট তৈরি করছেন, যা স্ক্রিনে আসার সময় চ্যাট সার্ভারের সাথে কানেক্ট করতে হবে। আপনাকে একটি createConnection()
API দেওয়া হয়েছে, যা এমন একটি অবজেক্ট রিটার্ন করে যার মধ্যে connect()
এবং disconnect()
মেথড থাকে। এখন প্রশ্ন হলো: ইউজার যখন স্ক্রিনে এই কম্পোনেন্টটি দেখছে, তখন কীভাবে কানেকশন চালু রাখা যায়?
প্রথমে Effect এর ভিতরে এই কোড লিখুন:
useEffect(() => {
const connection = createConnection();
connection.connect();
});
প্রতিবার রেন্ডার হওয়ার পর নতুন করে কানেক্ট করলে সেটা ধীরগতির হবে, তাই আমরা dependency array যোগ করি:
useEffect(() => {
const connection = createConnection();
connection.connect();
}, []);
এখানে Effect এর ভিতরের কোড কোনো প্রপস বা স্টেট ব্যবহার করছে না, তাই dependency array খালি []
রাখা হয়েছে। এর মানে React কেবল তখনই এই কোড চালাবে যখন কম্পোনেন্ট প্রথমবার স্ক্রিনে দেখাবে (mount হবে)।
চলুন এবার এই কোডটি চালিয়ে দেখি:
import { useEffect } from "react";
import { createConnection } from "./chat.js";
export default function ChatRoom() {
useEffect(() => {
const connection = createConnection();
connection.connect();
}, []);
return <h1>Welcome to the chat!</h1>;
}
export function createConnection() {
// বাস্তব চ্যাট সার্ভারে কানেক্ট করার মতো করে বানানো
return {
connect() {
console.log("✅ Connecting...");
},
disconnect() {
console.log("❌ Disconnected.");
},
};
}
এই Effect কেবল প্রথমবার চলার কথা, তাই আপনি হয়তো আশা করছেন "✅ Connecting..."
একবারই কনসোলে দেখাবে।
কিন্তু আপনি দেখবেন এটা দু'বার কনসোলে দেখাচ্ছে। কেন?
ভাবুন, ChatRoom
কম্পোনেন্টটি বড় একটি অ্যাপের অংশ। ইউজার প্রথমে চ্যাট স্ক্রিনে আসেন, তখন কম্পোনেন্ট মাউন্ট হয় এবং connect()
কল হয়। তারপর ইউজার অন্য পেইজে যান, যেমন Settings
। তখন ChatRoom
আনমাউন্ট হয়ে যায়। পরে ইউজার আবার ব্যাক করে ChatRoom
এ ফেরে—এবার আবার connect()
চলে। কিন্তু আগের কানেকশনটি কখনো বন্ধ করা হয়নি! এতে একাধিক কানেকশন তৈরি হতে থাকে যা অ্যাপের জন্য সমস্যা সৃষ্টি করতে পারে।
এই ধরণের বাগ সহজে ধরা পড়ে না। তাই ডেভেলপমেন্ট মোডে React প্রথমবার মাউন্ট করার পর আবার সঙ্গে সঙ্গে আনমাউন্ট ও রিমাউন্ট করে, যেন আপনি এমন সমস্যা আগে থেকেই ধরে ফেলতে পারেন।
যখন আপনি কনসোলে "✅ Connecting..."
দু’বার দেখেন, তখনই বোঝা যায় আপনি disconnect()
কল করেননি। এটি ঠিক করতে Effect এর ভিতর থেকে একটি cleanup function রিটার্ন করুন:
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect();
};
}, []);
React প্রতিবার Effect চালানোর আগে এবং কম্পোনেন্ট আনমাউন্ট হওয়ার সময় এই cleanup ফাংশন চালায়। এখন দেখা যাক কী হয়:
import { useState, useEffect } from "react";
import { createConnection } from "./chat.js";
export default function ChatRoom() {
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, []);
return <h1>Welcome to the chat!</h1>;
}
export function createConnection() {
return {
connect() {
console.log("✅ Connecting...");
},
disconnect() {
console.log("❌ Disconnected.");
},
};
}
এখন আপনি ডেভেলপমেন্টে কনসোলে তিনটি লগ পাবেন:
"✅ Connecting..."
"❌ Disconnected."
"✅ Connecting..."
এটাই ডেভেলপমেন্টে সঠিক আচরণ। React ইচ্ছাকৃতভাবে কম্পোনেন্টটি রিমাউন্ট করে যেন আপনি নিশ্চিত হতে পারেন cleanup ঠিকমতো কাজ করছে। এটি দেখতে যেমনই হোক, production-এ এটি একবারই চলবে।
Production-এ আপনি কেবল "✅ Connecting..."
একবারই দেখবেন। এই রিমাউন্ট কেবল ডেভেলপমেন্ট মোডে হয় বাগ ধরার জন্য। চাইলে আপনি Strict Mode বন্ধ করতে পারেন, কিন্তু React এটি চালু রাখার পরামর্শ দেয় যাতে এমন বাগ দ্রুত ধরা যায়।
নিচে সহজ ভাষায় বাংলা অনুবাদ দেওয়া হলো:
ডেভেলপমেন্টে Effect দুইবার চললে কীভাবে সামলাবেন?
React ইচ্ছে করেই ডেভেলপমেন্ট মোডে আপনার কম্পোনেন্টকে পুনরায় মাউন্ট করে যাতে আগের মতো বাগ ধরা যায়। ঠিক প্রশ্ন হচ্ছে না "Effect একবার কিভাবে চালাবো?" বরং "Effect যেন রিমাউন্টের পরেও ঠিকঠাক কাজ করে, সেটা কিভাবে নিশ্চিত করবো?"
সাধারণত এর উত্তর হলো cleanup ফাংশন ঠিকমতো লিখা। এই ফাংশনের কাজ হচ্ছে Effect যা করছে সেটা বন্ধ বা উল্টে দেওয়া। একটা সহজ নিয়ম হলো: ইউজার যেন বুঝতে না পারে যে Effect একবারই চালিয়েছে (যেমন প্রোডাকশনে হয়) নাকি চালিয়ে বন্ধ করে আবার চালিয়েছে (যেমন ডেভেলপমেন্টে হয়)।
আপনার বেশিরভাগ Effect নিচের সাধারণ ধাঁচের (pattern) ভেতরে পড়ে।
ref
দিয়ে Effect বন্ধ করার চেষ্টা করবেন না
অনেকেই ডেভেলপমেন্টে Effect দুইবার চলা ঠেকাতে ref
ব্যবহার করে—এটা একটা বড় ভুল।
উদাহরণ হিসেবে ধরুন আপনি নিচের কোড লিখলেন:
const connectionRef = useRef(null);
useEffect(() => {
// 🚩 এইভাবে বাগ ঠিক হবে না!!!
if (!connectionRef.current) {
connectionRef.current = createConnection();
connectionRef.current.connect();
}
}, []);
এভাবে আপনি হয়তো কনসোলে "✅ Connecting..."
একবারই দেখছেন, কিন্তু আসলে বাগটি এখনো ঠিক হয়নি।
যখন ইউজার অন্য পেইজে যায়, তখন কানেকশন বন্ধ হয় না। আর যখন তারা আবার ফিরে আসে, তখন নতুন কানেকশন তৈরি হয়। ফলে আগের মতোই অনেক কানেকশন জমে যায়। এই "fix" আসলে কোনো সমস্যার সমাধান করে না।
বাগ ঠিক করতে শুধু Effect একবার চালানো যথেষ্ট না। Effect যেন রিমাউন্টের পরেও ঠিকঠাক কাজ করে, অর্থাৎ পুরাতন কাজগুলো (যেমন কানেকশন) ঠিকভাবে বন্ধ করা হয়, সেটাই গুরুত্বপূর্ণ।
নিচের উদাহরণগুলোতে এই ধরনের সাধারণ সমস্যার সমাধান দেখানো আছে।
নিচে সহজ ভাষায় বাংলায় অনুবাদ দেওয়া হলো:
React নয় এমন উইজেট কন্ট্রোল করা
কখনো কখনো এমন কিছু UI উইজেট ব্যবহার করতে হয় যেগুলো React দিয়ে বানানো না। ধরুন, আপনি একটা ম্যাপ কম্পোনেন্ট পেজে যোগ করছেন। এতে setZoomLevel()
নামে একটা মেথড আছে, এবং আপনি চান এটা যেন React-এর zoomLevel
state-এর সাথে সবসময় মিল রেখে চলে। তাহলে আপনার useEffect
এমন হবে:
useEffect(() => {
const map = mapRef.current;
map.setZoomLevel(zoomLevel);
}, [zoomLevel]);
এই ক্ষেত্রে কোনো cleanup দরকার নেই। ডেভেলপমেন্টে React useEffect
দুইবার চালাবে, কিন্তু এতে কোনো সমস্যা হবে না কারণ setZoomLevel
যদি একই ভ্যালু দিয়ে দুইবার ডাকা হয়, তাহলে কিছুই পরিবর্তন হয় না। হয়তো একটু ধীর গতির হতে পারে, কিন্তু প্রোডাকশনে এটা একবারই চলবে, তাই সমস্যা নেই।
তবে কিছু API আছে যেগুলো একসাথে দুইবার কল করা যায় না। উদাহরণ: ব্রাউজারের বিল্ট-ইন <dialog>
(opens in a new tab) এলিমেন্টের showModal()
(opens in a new tab) মেথড যদি দুইবার একসাথে কল করেন, তাহলে এরর দেয়। এই অবস্থায় আপনাকে cleanup ফাংশন লিখে dialog.close()
করতে হবে:
useEffect(() => {
const dialog = dialogRef.current;
dialog.showModal();
return () => dialog.close();
}, []);
ডেভেলপমেন্টে showModal()
→ close()
→ showModal()
এইভাবে চলবে। এতে ইউজারের চোখে এটা একবারই খোলার মতো মনে হবে—যেমনটা প্রোডাকশনে হয়।
ইভেন্ট সাবস্ক্রাইব করা
আপনার Effect যদি কোনো ইভেন্টে সাবস্ক্রাইব করে, তাহলে cleanup ফাংশনে আপনাকে আনসাবস্ক্রাইব করতে হবে:
useEffect(() => {
function handleScroll(e) {
console.log(window.scrollX, window.scrollY);
}
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
ডেভেলপমেন্টে addEventListener()
→ removeEventListener()
→ addEventListener()
এইভাবে চলবে। ফলে এক সময় শুধু একটি সাবস্ক্রিপশন অ্যাকটিভ থাকবে। ইউজারের কাছে এটাও একবারই চলার মতোই মনে হবে—যেমনটা প্রোডাকশনে হয়।
অ্যানিমেশন চালানো
আপনার Effect যদি কিছু অ্যানিমেট করে, তাহলে cleanup ফাংশনে সেই অ্যানিমেশনকে শুরু অবস্থায় ফিরিয়ে আনতে হবে:
useEffect(() => {
const node = ref.current;
node.style.opacity = 1; // অ্যানিমেশন চালু করা
return () => {
node.style.opacity = 0; // আবার শুরু অবস্থায় ফেরত আনা
};
}, []);
ডেভেলপমেন্টে opacity হবে 1
, পরে 0
, পরে আবার 1
। এতে ইউজারের চোখে শুধু 1
দেখাবে—যেমনটা প্রোডাকশনে হয়।
যদি আপনি কোনো থার্ড-পার্টি অ্যানিমেশন লাইব্রেরি ব্যবহার করেন যেটাতে টুইনিং (tweening) সাপোর্ট আছে, তাহলে cleanup ফাংশনে অ্যানিমেশনের টাইমলাইন রিসেট করতে হবে।
ডেটা ফেচ করা
যদি আপনার useEffect
কোনো ডেটা ফেচ করে, তাহলে ক্লিনআপ ফাংশনে এই ফেচটা বাতিল করে দেওয়া উচিত বা এর রেজাল্ট ইগনোর করা উচিত:
useEffect(() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
}, [userId]);
আপনি এমন কোনো নেটওয়ার্ক রিকোয়েস্ট "আনডু" করতে পারবেন না যেটা একবার হয়ে গেছে। কিন্তু ক্লিনআপ ফাংশন নিশ্চিত করবে যেন সেই ফেচ রেসপন্স—যেটা এখন আর দরকার নেই—আপনার অ্যাপ্লিকেশনে প্রভাব না ফেলে।
উদাহরণ: যদি userId
'Alice'
থেকে 'Bob'
-এ পরিবর্তন হয়, তাহলে 'Alice'
এর জন্য আসা রেসপন্স setTodos()
এ যাবে না, কারণ আমরা আগেই ignore = true
করে দিয়েছি।
🔹 ডেভেলপমেন্টে আপনি দুটি ফেচ দেখতে পাবেন। এটা স্বাভাবিক। প্রথম Effect
রান হওয়ার পরই ক্লিনআপ হয়ে যায়, তাই ignore = true
হয়ে যায় এবং ওই রেসপন্স ইগনোর হয়ে যায়।
🔹 প্রোডাকশনে শুধু একটি রিকোয়েস্ট হয়। ডেভেলপমেন্টে বারবার ফেচ হওয়াটা বিরক্তিকর মনে হলে আপনি ক্যাশিং এবং রিকোয়েস্ট ডিডুপ্লিকেশন যুক্ত কোনো সলিউশন ব্যবহার করতে পারেন:
function TodoList() {
const todos = useSomeDataLibrary(`/api/user/${userId}/todos`);
// ...
এতে শুধু ডেভেলপমেন্ট এক্সপেরিয়েন্স ভালো হবে না, অ্যাপও দ্রুত অনুভব হবে। যেমন, ইউজার যদি Back বাটনে ক্লিক করে আগের পেজে ফিরে যায়, তাহলে আগের ফেচ করা ডেটাই দেখাবে, নতুন করে অপেক্ষা করতে হবে না।
useEffect
দিয়ে ডেটা ফেচ করার বিকল্প কী?
অনেকেই useEffect
দিয়ে fetch
কল করেন, বিশেষ করে ক্লায়েন্ট-সাইড অ্যাপে। কিন্তু এতে কিছু সমস্যা আছে:
-
✅ Effects সার্ভারে চলে না। ফলে সার্ভার থেকে আসা HTML-এ শুধু “Loading…” দেখায়, ডেটা আসে না। পরে ব্রাউজারে সব JS লোড হয়ে গেলে তবেই ডেটা আসে—যা স্লো এবং ইউজার এক্সপেরিয়েন্স খারাপ করে।
-
✅ নেটওয়ার্ক ওয়াটারফল তৈরি হয়। মানে, প্রথমে প্যারেন্ট কম্পোনেন্ট ডেটা ফেচ করে, তারপর চাইল্ড কম্পোনেন্টগুলো শুরু করে। এতে সময় বেশি লাগে।
-
✅ প্রিলোড বা ক্যাশ হয় না। কম্পোনেন্ট আনমাউন্ট হয়ে আবার মাউন্ট হলে আবার ডেটা ফেচ করতে হয়।
-
✅ বাগ হওয়ার সম্ভাবনা থাকে। যেমন, একাধিক রিকোয়েস্টে রেস কন্ডিশন তৈরি হতে পারে।
তাই সমাধান কী?
- 👉 React Framework ব্যবহার করলে তার ডেটা ফেচিং সিস্টেম ব্যবহার করুন। এগুলো ভালোভাবে অপ্টিমাইজড।
- 👉 না হলে React Query, useSWR, বা React Router 6.4+ ব্যবহার করুন। এগুলো ক্যাশিং, রিকোয়েস্ট ডিডুপ্লিকেশন এবং প্রিলোড সাপোর্ট করে।
- 👉 চাইলে আপনি নিজেও একটি ক্যাশ সিস্টেম তৈরি করতে পারেন। এর ভিতরে
useEffect
থাকবে, কিন্তু স্মার্টভাবে কাজ করবে।
তবে, যদি এগুলোর কোনোটিই না হয়, তখন useEffect
দিয়েই ফেচ করতে পারেন, শুধু উপরের মত সেফলি ব্যবহার করুন।
Here is the translation of your provided text into Bengali (Bangla):
অ্যানালিটিকস পাঠানো
এই কোডটি দেখুন যা একটি পেজ ভিজিটের সময় একটি অ্যানালিটিকস ইভেন্ট পাঠায়:
useEffect(() => {
logVisit(url); // একটি POST অনুরোধ পাঠায়
}, [url]);
ডেভেলপমেন্ট মোডে, logVisit
প্রতিটি URL-এর জন্য দুইবার কল হয়, তাই আপনি হয়তো এটা একবার চালানোর চেষ্টা করতে পারেন। আমরা পরামর্শ দিই এই কোডটি যেমন আছে তেমনই রাখার জন্য। আগের উদাহরণগুলোর মতোই, একবার চালানো আর দুইবার চালানোর মধ্যে ব্যবহারকারীর দৃষ্টিকোণ থেকে কোনো পার্থক্য নেই। ব্যবহারিক দিক থেকে, ডেভেলপমেন্ট মোডে logVisit
কোনো কাজ করা উচিত নয়, কারণ আপনি চান না যাতে ডেভেলপমেন্ট মেশিন থেকে আসা লগগুলো প্রোডাকশন মেট্রিকসকে প্রভাবিত করে। যখনই আপনি কোনো কম্পোনেন্ট ফাইল সেভ করেন, তা আবার রিমাউন্ট হয়, ফলে ডেভেলপমেন্টে অতিরিক্ত ভিজিট লগ হয়।
প্রোডাকশন মোডে কোনো ডুপ্লিকেট ভিজিট লগ হবে না।
আপনি যে অ্যানালিটিকস ইভেন্ট পাঠাচ্ছেন তা ডিবাগ করতে চাইলে, আপনার অ্যাপটি একটি স্টেজিং এনভায়রনমেন্টে ডিপ্লয় করতে পারেন (যেটি প্রোডাকশন মোডে চলে), অথবা অস্থায়ীভাবে Strict Mode এবং এর ডেভেলপমেন্ট-অনলি রিমাউন্টিং চেক বন্ধ রাখতে পারেন। আপনি চাইলে রুট চেঞ্জ ইভেন্ট হ্যান্ডলার থেকেও অ্যানালিটিকস পাঠাতে পারেন Effects-এর বদলে। আরও নির্ভুল অ্যানালিটিকসের জন্য ইন্টারসেকশন অবজারভার (opens in a new tab) ব্যবহার করে দেখতে পারেন, যা ট্র্যাক করতে সাহায্য করে কোন কম্পোনেন্টগুলো ভিউপোর্টে আছে এবং কতক্ষণ ধরে দৃশ্যমান থাকে।
Here’s a simplified explanation of the two sections in easy Bengali:
১. ইফেক্ট নয়: অ্যাপ্লিকেশন শুরু করার সময় ইনিশিয়ালাইজ করা
কিছু কাজ শুধু একবারই করা উচিত, যেমন যখন অ্যাপটি প্রথমবার চালু হয়।
এই ধরনের কাজ আপনি কম্পোনেন্টের বাইরে করবেন। যেমন:
if (typeof window !== "undefined") {
checkAuthToken();
loadDataFromLocalStorage();
}
এখানে দেখা যাচ্ছে — যদি আমরা ব্রাউজারে থাকি, তাহলে checkAuthToken()
এবং loadDataFromLocalStorage()
চালানো হবে।
👉 এটা শুধু একবারই চলে, যখন ব্রাউজার পেজ লোড করে। তাই এটি ইফেক্টের (useEffect) ভেতরে না রাখলেও চলে।
২. ইফেক্ট নয়: একটি পণ্য কেনা
ধরুন আপনি কোনো পণ্য কিনতে একটি API কল দেন:
useEffect(() => {
fetch("/api/buy", { method: "POST" });
}, []);
এটা ভুল। কেন?
👉 ডেভেলপমেন্টে useEffect
দুইবার চলে। ফলে আপনি দুইবার পণ্য কিনে ফেলতে পারেন।
👉 এমনকি কেউ যদি Back বাটনে চাপ দেয়, তাহলেও আবার API কল চলে যেতে পারে।
কেনা হচ্ছে একটি কাজ, যেটা ব্যবহারকারী ক্লিক করলে হওয়া উচিত, শুধু পেজ ভিজিট করলেই নয়।
সঠিক উপায় হলো — ইফেক্ট বাদ দিয়ে এই কোডটি ক্লিক ইভেন্টের মধ্যে রাখা:
function handleClick() {
fetch("/api/buy", { method: "POST" });
}
👉 এখন, ব্যবহারকারী যখন Buy বাটনে ক্লিক করবে, তখনই পণ্য কেনা হবে।
মোট কথা: যদি রিমাউন্ট (component নতুন করে লোড হওয়া) হলে আপনার অ্যাপের লজিক ভেঙে পড়ে, তাহলে বুঝবেন কোডে আগে থেকেই সমস্যা ছিল। React এই ধরনের সমস্যা ধরার জন্যই ডেভেলপমেন্টে কম্পোনেন্ট দুইবার চালায়।
এখানে আপনার দেওয়া টেক্সটের সহজ ও স্পষ্ট বাংলায় অনুবাদ দেওয়া হলো:
সবকিছু একসাথে দেখা (Putting it all together)
এই প্লেগ্রাউন্ডটি আপনাকে প্র্যাকটিক্যালি বুঝতে সাহায্য করবে যে Effect কিভাবে কাজ করে।
এই উদাহরণে setTimeout
(opens in a new tab) ব্যবহার করা হয়েছে। এটি Effect চালু হওয়ার তিন সেকেন্ড পর ইনপুট করা টেক্সট কনসোলে লগ করবে। কিন্তু ক্লিনআপ ফাংশন ব্যবহার করে এই টাইমআউটটি বাতিল করা যায়। শুরু করতে “Mount the component” বাটনে ক্লিক করুন।
কোডের ব্যাখ্যা:
useEffect(() => {
function onTimeout() {
console.log("⏰ " + text);
}
console.log('🔵 Schedule "' + text + '" log');
const timeoutId = setTimeout(onTimeout, 3000);
return () => {
console.log('🟡 Cancel "' + text + '" log');
clearTimeout(timeoutId);
};
}, [text]);
🔹 এই কোড text
ভ্যালু পরিবর্তিত হলেই Effect চালায়।
🔹 প্রথমে “Schedule” লগ হয় এবং তারপর ৩ সেকেন্ড পর console.log
হয়।
🔹 তবে যদি text
পরিবর্তন হয়, আগের টাইমআউট Cancel হয় এবং নতুনটি Schedule হয়।
কীভাবে কাজ করে:
প্রথমবার আপনি তিনটি লগ দেখবেন:
🔵 Schedule "a" log
🟡 Cancel "a" log
🔵 Schedule "a" log
৩ সেকেন্ড পর দেখা যাবে:
⏰ a
👉 কেন দুটি Schedule ও একটি Cancel হলো? React ডেভেলপমেন্ট মোডে কম্পোনেন্ট একবার Unmount করে আবার Mount করে। এতে আপনি বুঝতে পারেন আপনার ক্লিনআপ ঠিকভাবে কাজ করছে কিনা।
এখন ইনপুটে abc
টাইপ করুন:
যদি দ্রুত টাইপ করেন, আপনি দেখবেন:
🔵 Schedule "ab" log
🟡 Cancel "ab" log
🔵 Schedule "abc" log
👉 React আগের Effect ক্লিন করে নতুনটা চালায়। তাই একই সময়ে একটাই setTimeout
অ্যাক্টিভ থাকে।
এখন কিছু টাইপ করে সাথে সাথে "Unmount the component" চাপুন:
👉 দেখবেন, Unmount হওয়ার সময় শেষ Effect-এর ক্লিনআপ হয়ে যায়। মানে টাইমআউট ফায়ার হওয়ার আগেই বাতিল হয়ে যায়।
এবার ক্লিনআপ ফাংশনটি কমেন্ট করে দিন:
// return () => {
// console.log('🟡 Cancel "' + text + '" log');
// clearTimeout(timeoutId);
// };
তারপর দ্রুত টাইপ করুন abcde
।
প্রশ্ন: ৩ সেকেন্ড পর কী হবে?
আপনি যদি ভাবেন console.log(text)
পাঁচবার abcde
দেখাবে, তাহলে ভুল। আসলে আপনি দেখবেন:
⏰ a
⏰ ab
⏰ abc
⏰ abcd
⏰ abcde
👉 কারণ, প্রত্যেকটি Effect তার সময়ের text
ভ্যালু "বন্দি" করে রাখে (এই ধারণাকে বলে Closure)।
👉 তাই text
বদলালেও পুরনো Effect তার পুরনো মান দেখায়।
সারাংশ:
- Effect চালু হলে এটি নিজস্ব তথ্য নিয়ে কাজ করে।
- পুরনো Effect মুছে ফেলা হয় যখন নতুন Effect চলে।
- Cleanup ফাংশন না থাকলে পুরনো কাজগুলো একসাথে চালু হয়ে যেতে পারে।
- Closure-এর জন্য প্রতিটি Effect আলাদা আলাদাভাবে চলে, একে অপরকে প্রভাবিত করে না।