আপনি কি কখনো এমন কোনো টুলটিপ (tooltip) তৈরি করেছেন যা টপ-লেফট কর্নার থেকে হঠাৎ লাফিয়ে তার সঠিক জায়গায় চলে আসে? অথবা এমন কোনো মোডাল (modal) যা সঠিক সাইজে সেট হওয়ার আগে এক মুহূর্তের জন্য ভুল সাইজে ফ্ল্যাশ করে? এই মুহূর্তের ছোটখাটো সমস্যাটিই হলো লেআউট ফ্লিকার (layout flicker)। এটি তখন ঘটে যখন React DOM পড়ে, একটি সংশোধন গণনা করে এবং স্টেট আপডেট করে, কিন্তু তার আগেই ব্রাউজার স্ক্রিনে পিক্সেল দেখানো শুরু করে দেয়। এর সাধারণ সমাধান হলো useEffect-এর পরিবর্তে useLayoutEffect ব্যবহার করা। এই পরিবর্তনটি কাজ করে, তবে কেবল তখনই যখন আপনি ব্রাউজার পাইপলাইনের ভেতরে প্রতিটি হুক ঠিক কখন কাজ করে তা বুঝতে পারেন।

ব্রাউজার পাইপলাইন: রেন্ডার, কমিট, পেইন্ট

React তিনটি আলাদা ধাপে একটি কম্পোনেন্ট আপডেট করে। render ফেজে, React Virtual DOM তৈরি করে—বা পুনরায় তৈরি করে—এবং diff গণনা করে। এখানে এখনও কোনো প্রকৃত পিক্সেল পরিবর্তন হয় না; এটি মেমরিতে হওয়া নিছক একটি গণনা। এরপর আসে commit ফেজ, যেখানে React সেই পরিবর্তনগুলো প্রকৃত DOM নোডগুলোতে প্রয়োগ করে। স্টাইল আপডেট হয়, নোড যুক্ত বা রিমুভ করা হয় এবং টেক্সট পরিবর্তন হয়।

এরপর ব্রাউজার দায়িত্ব নেয়। paint ফেজে, ব্রাউজারের রেন্ডারিং ইঞ্জিন লেআউট জিওমেট্রি গণনা করে এবং স্ক্রিনে পিক্সেল আঁকে। এই সিকোয়েন্সটি অপরিবর্তনীয়। পেইন্ট করার আগে ব্রাউজারকে অবশ্যই লেআউট সম্পন্ন করতে হবে, এবং ব্যবহারকারী নতুন কিছু দেখার আগে পেইন্টিং শেষ করতে হবে। কমিট এবং পেইন্টের মধ্যবর্তী ব্যবধান মিলিসেকেন্ডে মাপা হয়, কিন্তু এটি বাস্তব, এবং এখানেই useEffect এবং useLayoutEffect-এর মধ্যে পার্থক্য তৈরি হয়।

কেন useEffect ফ্লিকার তৈরি করে

useEffect অ্যাসিনক্রোনাসলি (asynchronously) চলে, যা ব্রাউজার স্ক্রিন পেইন্ট করার পরে কাজ করার জন্য নির্ধারিত থাকে। DOM আপডেট হয়, পিক্সেল আঁকা হয় এবং তারপর React আপনার ইফেক্টটি চালানোর জন্য ফিরে আসে।

কল্পনা করুন আপনি একটি বাটনের নিচে একটি ড্রপডাউন মেনু রেন্ডার করছেন। useEffect-এর ভেতরে, আপনি buttonRef.current.getBoundingClientRect() কল করেন, সঠিক top এবং left কোঅর্ডিনেট গণনা করেন এবং সেগুলো স্টেটে সংরক্ষণ করেন। যেহেতু useEffect পেইন্টের পরে চলে, তাই ব্রাউজার ইতিমধ্যে ড্রপডাউনটিকে তার ডিফল্ট অবস্থানে, সম্ভবত top: 0, left: 0-এ এঁকে ফেলেছে। সেই পেইন্টের পরেই আপনার ইফেক্টটি স্টেট আপডেট করে। React সংশোধিত কোঅর্ডিনেটগুলো কমিট করে এবং ব্রাউজার আবার পেইন্ট করে। ব্যবহারকারী দুটি ফ্রেম দেখেন: প্রথমে ভুল অবস্থান, তারপর সঠিকটি। এই ভিজ্যুয়াল লাফ বা স্ন্যাপ-টিই হলো ফ্লিকার যা সবাই এড়াতে চায়।

ডেটা ফেচিং, API কল, অ্যানালিটিক্স ট্র্যাকিং বা ইভেন্ট লিসেনার সেটআপ করার জন্য এই বিলম্ব কোনো সমস্যা নয়। পেইন্টের কয়েক মিলিসেকেন্ড পরে একটি অ্যানালিটিক্স বিকন (analytics beacon) ফায়ার হচ্ছে কি না, তা ব্যবহারকারীর কাছে গুরুত্বপূর্ণ নয়। প্রকৃতপক্ষে, নন-ভিজ্যুয়াল কাজগুলোকে পেইন্টের পরে রাখা প্রাথমিক রেন্ডারকে রেসপন্সিভ রাখতে সাহায্য করে। কিন্তু লেআউট-নির্ভর সংশোধনের জন্য useEffect অনেক দেরি করে ফেলে।

কীভাবে useLayoutEffect পেইন্টকে ব্লক করে

useLayoutEffect সিনক্রোনাসলি (synchronously) চলে, React যখন DOM পরিবর্তন করে ঠিক তার পরেই, কিন্তু ব্রাউজার লেআউট গণনা করার বা পিক্সেল পেইন্ট করার সুযোগ পাওয়ার আগেই। এটি পেইন্ট পাইপলাইনকে পুরোপুরি ব্লক করে দেয়।

আপনি যদি useLayoutEffect-এর ভেতরে একই ড্রপডাউন মেজারমেন্ট করেন, তবে সিকোয়েন্সটি বদলে যাবে। React প্রাথমিক DOM আপডেট কমিট করে, আপনার লেআউট ইফেক্টটি চালায় এবং আপনার স্টেট আপডেট একটি সিনক্রোনাস রেন্ডার ট্রিগার করে। React সংশোধিত কোঅর্ডিনেটগুলো কমিট করে এবং কেবল তখনই ব্রাউজার পেইন্ট করে। ব্যবহারকারী একটি মাত্র ফ্রেম দেখেন, যা আগে থেকেই সঠিক।

এই ব্লকিং আচরণটি যেমন একটি সুবিধা, তেমনি এটি একটি ঝুঁকিও। যেহেতু useLayoutEffect শেষ না হওয়া পর্যন্ত ব্রাউজারকে পেইন্ট করতে বাধা দেয়, তাই এর ভেতরে যেকোনো ভারী গণনা (heavy computation) UI-কে ফ্রিজ করে দিতে পারে। এমনকি কয়েক ডজন মিলিসেকেন্ডের ব্লকিং পেইন্টও ব্যবহারকারীর কাছে jank বা ঝাকুনি মনে হতে পারে। এই কারণেই React ডকুমেন্টেশন স্পষ্টভাবে বলে যে, প্রথমে useEffect দিয়ে শুরু করুন এবং কেবল তখনই useLayoutEffect-এ যান যখন আপনি এমন কোনো ফ্লিকার লক্ষ্য করেন যা আপনি সহ্য করতে পারছেন না।

কখন কোন হুক ব্যবহার করবেন

আপনার বেশিরভাগ লজিকই useEffect-এ থাকা উচিত। এটি ব্যবহার করুন:

  • API থেকে ডেটা ফেচ করার জন্য
  • সাবস্ক্রিপশন বা ইভেন্ট লিসেনার সেটআপ করার জন্য
  • অ্যানালিটিক্স ইভেন্ট পাঠানোর জন্য
  • যেকোনো সাইড ইফেক্ট যা তাৎক্ষণিকভাবে লেআউট পড়ে না বা পরিবর্তন করে না

useLayoutEffect সেইসব অপারেশনের জন্য রাখুন যা ব্যবহারকারী ফ্রেমটি দেখার আগেই DOM পড়া এবং পুনরায় লেখা প্রয়োজন:

  • এলিমেন্টের ডাইমেনশন পরিমাপ করা, যেমন প্রস্থ (width), উচ্চতা (height), বা স্ক্রল পজিশন
  • টুলটিপ, পপওভার বা কনটেক্সট মেনুর জন্য কোঅর্ডিনেট গণনা করা
  • ভিজ্যুয়াল পজিশন যদি রেন্ডার করা জিওমেট্রির ওপর নির্ভর করে, তবে দৃশ্যমান লেআউট শিফট রোধ করা

আপনি যদি নিশ্চিত না হন কোনটি বেছে নেবেন, তবে ডিফল্টভাবে useEffect ব্যবহার করুন। কেবল তখনই useLayoutEffect-এ যান যখন আপনি ভিজ্যুয়াল অস্থিরতা (visual instability) লক্ষ্য করবেন। এই একটি নিয়ম মেনে চললে অধিকাংশ React অ্যাপ্লিকেশনই মসৃণভাবে চলতে থাকবে।

সার্ভার-সাইড রেন্ডারিং-এর সমস্যা (The Server-Side Rendering Gotcha)

আপনি যদি Next.js, Remix, বা এমন কোনো ফ্রেমওয়ার্ক ব্যবহার করেন যা সার্ভারে React রেন্ডার করে, তবে আপনি useLayoutEffect-এর ক্ষেত্রে একটি ওয়ার্নিং দেখতে পাবেন। যেহেতু সার্ভারে কোনো DOM নেই, তাই এই হুকটির (hook) পরিমাপ করার মতো কিছু থাকে না। React আপনাকে সতর্ক করে যে এটি একটি ব্রাউজার এনভায়রনমেন্ট আশা করেছিল কিন্তু তা পায়নি। Hydration-এর সময়, এই অমিলটি সূক্ষ্ম বাগ (bug) তৈরি করতে পারে কারণ সার্ভার-রেন্ডার করা মার্কআপ এবং ক্লায়েন্টের প্রথম পরিকল্পিত রেন্ডারের মধ্যে পার্থক্য থাকতে পারে।

এর আদর্শ সমাধান হলো একটি isomorphic hook যা এনভায়রনমেন্টের ওপর ভিত্তি করে সঠিক ইফেক্টটি নির্বাচন করে:

const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

যে কোনো কম্পোনেন্টে এই wrapper-টি ব্যবহার করুন যেখানে DOM নোড পরিমাপ করা প্রয়োজন কিন্তু যা সার্ভার রেন্ডারিংয়ের সময় চলতে পারে। এটি ওয়ার্নিংটি বন্ধ করে দেয় এবং আপনার সার্ভার আউটপুটকে সামঞ্জস্যপূর্ণ রাখে।

পারফরম্যান্স এবং সেরা অনুশীলনসমূহ

যেহেতু useLayoutEffect পেইন্টিং (painting) আটকে দেয়, তাই হুকের বডি যতটা সম্ভব হালকা রাখুন। লেআউট ভ্যালু পড়ুন, সংশোধনটি গণনা করুন এবং সেটি পুনরায় লিখে ফেলুন। এর ভেতরে ডেটা ফেচ (fetch) করবেন না, বড় অবজেক্ট পার্স (parse) করবেন না বা কোনো ব্যয়বহুল অ্যালগরিদম চালাবেন না। এখানে ভারী কোড থাকলে মেইন থ্রেডটি আটকে যাবে এবং আপনার ইন্টারফেসটি ফ্রিজ (frozen) হয়ে আছে বলে মনে হবে।

এলিমেন্ট পরিমাপ করার সময় document.getElementById-এর পরিবর্তে React refs ব্যবহার করুন। Refs আপনার কম্পোনেন্ট ইনস্ট্যান্সের সাথে যুক্ত থাকে, কোনো কুয়েরি ট্রিক ছাড়াই রেন্ডার হওয়ার পর টিকে থাকে এবং পোর্টাল (portals) বা কন্ডিশনাল রেন্ডারিংয়ের ক্ষেত্রে নির্ভরযোগ্যভাবে কাজ করে। গ্লোবাল ID লুকআপ কম্পোনেন্টের এনক্যাপসুলেশন (encapsulation) নষ্ট করে এবং ঠিক যখন আপনার এটি প্রয়োজন, তখন এটি null রিটার্ন করতে পারে।

প্রায় প্রতিটি সাইড ইফেক্টের (side effect) জন্য useEffect-ই হলো সঠিক ডিফল্ট। এটি ব্রাউজারকে কোনো বাধা ছাড়াই পেইন্ট করতে দেয় এবং ডেটা, ইভেন্ট এবং এক্সটার্নাল সিনক্রোনাইজেশন (external synchronization) সুন্দরভাবে পরিচালনা করে। useLayoutEffect একটি নির্দিষ্ট সমস্যার জন্য বিশেষায়িত টুল: পেইন্টিংয়ের আগে লেআউট পড়া এবং সেটি পুনরায় লিখে দেওয়া। এগুলোর মধ্যে সময়ের পার্থক্যটি আয়ত্ত করতে পারলে, আপনি ফ্লিকার (flicker) বা ঝিলিক দেওয়া সমস্যা নিয়ে আর হিমশিম খাবেন না, বরং তা প্রতিরোধ করতে পারবেন।

মূল কথা: সবকিছুর জন্য useEffect দিয়ে শুরু করুন। যে মুহূর্তে আপনি দেখবেন একটি টুলটিপ (tooltip) বা মোডাল (modal) নিজেকে ঠিক করার আগে ভুল জায়গায় ঝিলিক দিচ্ছে, সেটিই হলো আপনার সংকেত। তখন useLayoutEffect-এ সুইচ করুন, DOM পরিমাপ করুন, আপনার লেআউট সমন্বয় করুন এবং ব্রাউজারকে একবার সঠিকভাবে পেইন্ট করতে দিন।