क्या आपने कभी ऐसा टूलटिप बनाया है जो टॉप-लेफ्ट कॉर्नर से अचानक अपनी सही जगह पर पहुँच जाता है? या कोई ऐसा मोडल जो अपनी सही साइज़ में सेट होने से पहले गलत साइज़ में चमकता (flash) है? वह पल भर की गड़बड़ी एक layout flicker है। यह तब होता है जब React DOM को पढ़ता है, सुधार की गणना करता है, और state को अपडेट करता है, लेकिन ब्राउज़र पहले ही स्क्रीन पर पिक्सेल दिखाना शुरू कर चुका होता है। सामान्य समाधान useEffect को useLayoutEffect से बदलना है। यह बदलाव काम तो करता है, लेकिन तभी जब आप समझते हैं कि ब्राउज़र पाइपलाइन के अंदर प्रत्येक हुक (hook) वास्तव में कब चलता है।

ब्राउज़र पाइपलाइन: Render, Commit, Paint

React एक कंपोनेंट को तीन अलग-अलग चरणों में अपडेट करता है। render चरण में, React Virtual DOM बनाता है—या फिर से बनाता है—और diff की गणना करता है। अभी तक कोई वास्तविक पिक्सेल परिवर्तन नहीं हुआ है; यह केवल मेमोरी में होने वाली गणना है। इसके बाद commit चरण आता है, जहाँ React उन परिवर्तनों को वास्तविक DOM नोड्स पर लागू करता है। स्टाइल्स अपडेट होते हैं, नोड्स डाले या हटाए जाते हैं, और टेक्स्ट बदलता है।

फिर ब्राउज़र कमान संभालता है। paint चरण में, ब्राउज़र का रेंडरिंग इंजन लेआउट ज्योमेट्री की गणना करता है और स्क्रीन पर पिक्सेल बनाता है। यह क्रम निश्चित है। ब्राउज़र को पेंट करने से पहले लेआउट पूरा करना होगा, और उपयोगकर्ता को कुछ भी नया दिखने से पहले उसे पेंटिंग पूरी करनी होगी। commit और paint के बीच का अंतर मिलीसेकंड में मापा जाता है, लेकिन यह वास्तविक है, और यही वह विंडो है जहाँ useEffect और useLayoutEffect अलग हो जाते हैं।

useEffect फ्लिकर (flicker) का कारण क्यों बनता है

useEffect एसिंक्रोनस (asynchronously) रूप से चलता है, जिसे ब्राउज़र द्वारा स्क्रीन पेंट करने के बाद चलाने के लिए शेड्यूल किया जाता है। DOM अपडेट हो जाता है, पिक्सेल ड्रा हो जाते हैं, और फिर React आपके इफेक्ट को चलाने के लिए वापस आता है।

कल्पना कीजिए कि आप एक बटन के नीचे ड्रॉपडाउन मेनू रेंडर करते हैं। useEffect के अंदर, आप buttonRef.current.getBoundingClientRect() कॉल करते हैं, सही top और left कोऑर्डिनेट्स की गणना करते हैं, और उन्हें state में स्टोर करते हैं। क्योंकि useEffect पेंट के बाद चलता है, ब्राउज़र पहले ही ड्रॉपडाउन को अपनी डिफ़ॉल्ट स्थिति में, शायद top: 0, left: 0 पर ड्रा कर चुका होता है। उस पेंट के बाद ही आपका इफेक्ट state को अपडेट करता है। React सुधारे गए कोऑर्डिनेट्स को commit करता है, और ब्राउज़र फिर से पेंट करता है। उपयोगकर्ता दो फ्रेम देखता है: पहले गलत स्थिति, फिर सही वाली। वह विजुअल 'snap' ही वह फ्लिकर है जिससे हर कोई बचना चाहता है।

डेटा फेचिंग, API कॉल्स, एनालिटिक्स ट्रैकिंग, या इवेंट लिसनर्स सेट करने के लिए, यह देरी मायने नहीं रखती। उपयोगकर्ता को इससे फर्क नहीं पड़ता कि एनालिटिक्स बीकन पेंट के कुछ मिलीसेकंड बाद चलता है। वास्तव में, नॉन-विजुअल काम को पेंट के बाद तक टालना शुरुआती रेंडर को रिस्पॉन्सिव बनाए रखता है। लेकिन लेआउट-निर्भर सुधारों (layout-dependent corrections) के लिए, useEffect बहुत देर से आता है।

useLayoutEffect पेंट को कैसे रोकता है

useLayoutEffect सिंक्रोनस (synchronously) रूप से चलता है, React द्वारा DOM में बदलाव करने के तुरंत बाद, लेकिन ब्राउज़र को लेआउट की गणना करने या पिक्सेल पेंट करने का मौका मिलने से पहले। यह पेंट पाइपलाइन को पूरी तरह से ब्लॉक कर देता है।

यदि आप useLayoutEffect के अंदर वही ड्रॉपडाउन माप (measurement) करते हैं, तो क्रम बदल जाता है। React शुरुआती DOM अपडेट को commit करता है, आपका लेआउट इफेक्ट चलाता है, और आपका state अपडेट एक सिंक्रोनस री-रेंडर को ट्रिगर करता है। React सुधारे गए कोऑर्डिनेट्स को commit करता है, और उसके बाद ही ब्राउज़र पेंट करता है। उपयोगकर्ता को एक ही फ्रेम दिखता है, जो पहले से ही सही होता है।

वह ब्लॉकिंग व्यवहार ही इसकी विशेषता भी है और जोखिम भी। क्योंकि useLayoutEffect ब्राउज़र को तब तक पेंट करने से रोकता है जब तक वह पूरा न हो जाए, इसके अंदर की कोई भी भारी गणना (heavy computation) UI को फ्रीज कर सकती है। कुछ दर्जन मिलीसेकंड का ब्लॉक किया गया पेंट भी उपयोगकर्ता को jank जैसा महसूस होता है। यही कारण है कि React डॉक्स स्पष्ट रूप से आपको useEffect से शुरू करने और केवल useLayoutEffect पर तभी जाने की सलाह देते हैं जब आप वास्तव में ऐसा फ्लिकर देखें जिसे आप बर्दाश्त नहीं कर सकते।

प्रत्येक हुक का उपयोग कब करें

आपका अधिकांश लॉजिक useEffect में होना चाहिए। इसका उपयोग इनके लिए करें:

  • API से डेटा फेच करना
  • सब्सक्रिप्शन या इवेंट लिसनर्स सेट करना
  • एनालिटिक्स इवेंट्स भेजना
  • कोई भी साइड इफेक्ट जो तुरंत लेआउट को नहीं पढ़ता या बदलता है

useLayoutEffect को उन ऑपरेशन्स के लिए सुरक्षित रखें जिन्हें उपयोगकर्ता के फ्रेम देखने से पहले DOM को पढ़ना और वापस लिखना ही होगा:

  • एलिमेंट के आयाम (dimensions) मापना, जैसे चौड़ाई (width), ऊंचाई (height), या स्क्रॉल पोजीशन
  • टूलटिप्स, पॉपओवर्स, या कॉन्टेक्स्ट मेनू के लिए कोऑर्डिनेट्स की गणना करना
  • विजुअल लेआउट शिफ्ट को रोकना जब विजुअल स्थिति रेंडर की गई ज्योमेट्री पर निर्भर हो

यदि आप अनिश्चित हैं कि किसे चुनें, तो डिफ़ॉल्ट रूप से useEffect का उपयोग करें। useLayoutEffect पर केवल तभी जाएँ जब आप विजुअल अस्थिरता (visual instability) महसूस करें। केवल यह नियम ही अधिकांश React एप्लिकेशन को सुचारू रूप से चलाने में मदद करेगा।

सर्वर-साइड रेंडरिंग (Server-Side Rendering) की समस्या

यदि आप Next.js, Remix, या किसी भी ऐसे framework का उपयोग करते हैं जो server पर React को render करता है, तो आपको useLayoutEffect के साथ एक warning मिलेगी। क्योंकि server के पास DOM नहीं होता है, इसलिए hook के पास मापने के लिए कुछ नहीं होता है। React आपको चेतावनी देता है कि उसने browser environment की अपेक्षा की थी लेकिन उसे ऐसा नहीं मिला। Hydration के दौरान, यह mismatch सूक्ष्म bugs का कारण भी बन सकता है क्योंकि server-rendered markup और client का पहला intended render अलग हो सकते हैं।

इसका मानक समाधान एक isomorphic hook है जो environment के आधार पर सही effect चुनता है:

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

इस wrapper का उपयोग किसी भी ऐसे component में करें जिसे DOM nodes को मापना आवश्यक हो लेकिन जो server rendering के दौरान execute हो सकता हो। यह warning को शांत करता है और आपके server output को consistent रखता है।

Performance और Best Practices

चूंकि useLayoutEffect painting को रोकता है, इसलिए hook के body को जितना हो सके उतना हल्का रखें। Layout value पढ़ें, correction की गणना करें, और उसे वापस लिखें। इसके अंदर data fetch न करें, बड़े objects को parse न करें, या भारी algorithms न चलाएं। यहाँ भारी code main thread को रोक देगा और आपके interface को frozen महसूस कराएगा।

जब आप elements को मापते हैं, तो document.getElementById के बजाय React refs का उपयोग करें। Refs आपके component instance से जुड़े होते हैं, query tricks के बिना re-renders में बने रहते हैं, और portals या conditional rendering के साथ विश्वसनीय रूप से काम करते हैं। Global ID lookups component encapsulation को तोड़ते हैं और ठीक उसी समय null return कर सकते हैं जब आपको उनकी आवश्यकता हो।

लगभग हर side effect के लिए useEffect सही default है। यह browser को बिना किसी रुकावट के paint करने देता है और data, events, और external synchronization को सफाई से संभालता है। useLayoutEffect एक विशिष्ट समस्या के लिए एक specialized tool है: paint होने से पहले layout को पढ़ना और उसे वापस लिखना। इनके बीच के timing अंतर में महारत हासिल करें, और आप flickers का पीछा करना बंद कर देंगे और उन्हें रोकना शुरू कर देंगे।

असली सीख: हर चीज़ के लिए useEffect से शुरुआत करें। जिस क्षण आप किसी tooltip या modal को खुद को ठीक करने से पहले गलत जगह पर blink होते हुए देखते हैं, वही आपका संकेत है। useLayoutEffect पर स्विच करें, DOM को मापें, अपने layout को adjust करें, और browser को एक बार—सही तरीके से—paint करने दें।