जवळपास कोणताही React codebase उघडला की तुम्हाला तोच स्वभाव दिसून येईल. डेव्हलपरला एखादी व्हॅल्यू ट्रॅक करायची असते, म्हणून ते useState वापरतात. काउंटर हवा आहे? useState. तात्पुरती इनपुट व्हॅल्यू? useState. मॉडेल फ्लिप करण्यासाठी बुलियन? useState. लवकरच, एकाच कंपोनंटमध्ये डझनभर वेगळे hooks असतात, जे डेटाच्या एका लहान भागाचे व्यवस्थापन करतात, ज्याला रेंडरिंग दरम्यान टिकून राहण्याची गरज असू शकते किंवा नसू शकते. परिणामी कोड गोंधळलेला (noisy) होतो, अतिरिक्त re-renders होतात आणि state कंपोनंटमध्ये विखुरलेला असतो.
ही सवय समजण्यासारखी आहे. useState हा आपल्यापैकी बहुतेकांनी शिकलेला पहिला hook आहे आणि तो काम करतो. पण 'काम करणे' म्हणजे 'योग्य प्रकारे बसणे' (fitting) असे नाही. प्रत्येक डेटाला reactive state मानल्यामुळे अशा समस्या निर्माण होतात ज्या कंपोनंट मोठा झाल्यावरच दिसून येतात.
फक्त बदलतो याचा अर्थ असा नाही की त्याला State ची गरज आहे
वेळेनुसार बदलणाऱ्या प्रत्येक व्हेरिएबलला useState मध्ये असण्याची गरज नसते. काही व्हॅल्यूज केवळ तुमच्याकडे आधीच असलेल्या गोष्टींचा परिणाम असतात. जर तुम्ही केवळ firstName आणि lastName एकत्र (concatenate) केल्यामुळे वापरकर्त्याचे पूर्ण नाव state मध्ये साठवत असाल, तर तुमच्याकडे आता सत्याचे दोन स्रोत (two sources of truth) आहेत. जेव्हा firstName अपडेट होतो, तेव्हा तुमचे fullName state जोपर्यंत तुम्ही ते सिंक करण्यासाठी दुसरा effect रन करत नाही तोपर्यंत जुनेच (stale) राहते. तुम्हाला sync effect ची गरज नाही. तुम्हाला 'derived value' ची गरज आहे.
const fullName = `${firstName} ${lastName}`;
रेंडरिंग दरम्यान त्याची गणना करा. जर ती गणना खर्चिक (expensive) असेल, तर ती memoize करा. पण जोपर्यंत वापरकर्ता त्या पूर्ण नावामध्ये स्वतंत्रपणे बदल करू शकत नाही, तोपर्यंत त्याला स्वतःचा useState hook देऊ नका.
हाच नियम फिल्टर केलेल्या लिस्ट्सनाही लागू होतो. जर तुम्ही allItems आणि filteredItems दोन्ही state मध्ये ठेवले, तर तुम्ही देखभालीचा (maintenance) भार दुप्पट केला आहे. रेंडरिंग दरम्यान फिल्टर करा. मूळ array आणि फिल्टर टेक्स्ट state मध्ये ठेवा, आणि त्यानंतर दिसणारी लिस्ट derive करा. यामुळे फिल्टर केलेली लिस्ट मूळ लिस्टशी कधीही विसंगत (out of sync) होणार नाही याची खात्री मिळते.
काही व्हॅल्यूजमुळे कधीही Re-renders ट्रिगर होऊ नयेत
useState हे विशेषतः React ला सांगण्यासाठी अस्तित्वात आहे की काहीतरी बदलले आहे आणि DOM ला अपडेट करण्याची गरज असू शकते. जर एखादी व्हॅल्यू बदलते पण UI चा कोणताही भाग त्या बदलाची पर्वा करत नसेल, तर useRef हे अधिक चांगले साधन आहे.
Timers आणि intervals हे याचे उत्तम उदाहरण आहेत. setInterval IDs state मध्ये साठवल्यामुळे प्रत्येक वेळी तुम्ही टाइमर सुरू किंवा थांबवता तेव्हा re-render होतो, जरी वापरकर्त्याला तो interval ID दिसत नसला तरीही. एक ref ही व्हॅल्यू React ला सूचित न करता साठवून ठेवते. हेच तर्क मागील props ट्रॅक करण्यासाठी, पेंटिंगपूर्वी DOM nodes मोजण्यासाठी किंवा custom hook साठी लेटेस्ट callback साठवण्यासाठी देखील लागू होते. स्वतःला विचारा: या व्हॅल्यूला स्क्रीनवर दिसण्याची गरज आहे का? जर उत्तर 'नाही' असेल, तर बहुधा त्याला useState ची गरज नाही.
DOM nodes स्वतः देखील refs मध्ये असावेत. जरी तुम्ही DOM element state मध्ये साठवू शकत असलात, तरी असे केल्याने ref callback चालल्यानंतर re-render ट्रिगर होतो. बहुतेक प्रकरणांमध्ये, तुम्हाला नोडची गरज केवळ एखाद्या imperative method साठी किंवा मोजमापासाठी असते, त्याला वेगळ्या पद्धतीने रेंडर करण्यासाठी नाही.
बुलियनचा सापळा
जेव्हा प्रत्येक फ्लॅगला स्वतःचा hook मिळतो, तेव्हा संबंधित UI गोष्टी विखुरल्या जातात. तुम्ही असे कंपोनंट्स पाहता जिथे isLoading, isError, आणि isSuccess हे तीन वेगळे booleans म्हणून परिभाषित केलेले असतात. समस्या अशी आहे की या तीन states स्वतंत्र नाहीत. जर isLoading आणि isSuccess दोन्ही true असतील, तर तुमचे UI एका अशक्य स्थितीत असते, तरीही TypeScript आणि React तुम्हाला ते रेंडर करू देतात.
संबंधित state एकत्र केल्यामुळे ही अवैध (invalid) संयोजने टाळता येतात. तीन booleans ऐवजी, एक सिंगल status string ट्रॅक करा: 'idle', 'loading', 'success', किंवा 'error'. एका वेळी फक्त एकच सक्रिय असू शकते, ज्यामुळे type level वर अशक्य स्थिती निर्माण होत नाहीत. जर डेटा अधिक जटिल असेल, तर discriminated union असलेला एक object गोष्टी अधिक सुटसुटीत करतो. जेव्हा तुम्हाला एकाच event handler मध्ये अनेक useState calls अपडेट करताना स्वतःला आढळता, तेव्हा तो संकेत आहे की त्या व्हॅल्यूज एकत्र असाव्यात.
आणखी एक useState वापरण्याऐवजी useReducer वापरा
एक अशी वेळ येते जेव्हा state updates हे 'whack-a-mole' खेळासारखे होतात. तुम्ही एकाच फंक्शनमध्ये setA, मग setB, आणि नंतर कंडिशनल setC कॉल करता. तो कोड वाचणाऱ्या पुढच्या डेव्हलपरला कंपोनंट नक्की काय करतो हे समजून घेण्यासाठी त्या संपूर्ण क्रमाचा मागोवा घ्यावा लागतो.
useReducer येथे उत्तम काम करते. ते useState ची जागा अधिक प्रगत असल्यामुळे घेत नाही; तर ते useState ची जागा घेते कारण लॉजिकची तशी गरज असते. एक reducer state कसे बदलते याचे केंद्रीकरण करतो. event handlers मध्ये अनेक imperative गोष्टी पसरवण्याऐवजी, तुम्ही एक उद्देश (intention) dispatch करता: dispatch({ type: 'submitted' }). पुढची state कशी असेल हे reducer ठरवतो. यामुळे टेस्टिंग सोपे होते, कारण तुमचा state logic हा एक pure function असतो. यामुळे debugging देखील सोपे होते, कारण प्रत्येक बदल एक मागोवा घेण्यायोग्य (traceable) action सोडतो.
reducer वापरण्याचे समर्थन करण्यासाठी तुम्हाला Redux ची गरज नाही. जर तुमच्याकडे तीन किंवा अधिक state variables असतील जे एकत्र अपडेट होतात, किंवा जर तुमची पुढची state पूर्णपणे मागील state वर अवलंबून असेल, तर reducer मुळे component खूप सोपा होतो.
State प्रत्यक्षात कुठे असते
कधीकधी समस्या state कशी साठवायची यात नसते, तर ती कुठे साठवायची यात असते. एका सामान्य चुकीमध्ये, state इतरत्र आवश्यक असू शकते म्हणून ती केवळ parent मध्ये hoist केली जाते. जर एखादा state चा भाग फक्त एकाच leaf component मध्ये वापरला जात असेल, तर तो तिथेच ठेवा. याला colocation म्हणतात, आणि यामुळे बदलांचा परिणाम (blast radius) कमी होतो. एखाद्या child ने काही उघडल्यामुळे parent ला re-render करू नका.
