మీరు దాదాపు ఏ React codebase ని తెరిచినా ఒకే రకమైన అలవాటును గమనించవచ్చు. ఒక డెవలపర్ ఒక విలువను ట్రాక్ చేయాలనుకున్నప్పుడు, వెంటనే useState వైపు మొగ్గు చూపుతారు. కౌంటర్ కావాలా? useState. తాత్కాలిక ఇన్పుట్ విలువ కావాలా? useState. మోడల్ను ఫ్లిప్ చేయడానికి బూలియన్ కావాలా? useState. కొద్దిసేపటికే, ఒకే కాంపోనెంట్లో డజన్ల కొద్దీ విడివిడి హుక్స్ ఉంటాయి, ప్రతి ఒక్కటి రెండర్ల మధ్య నిలవాల్సిన అవసరం లేని చిన్న చిన్న డేటాను నిర్వహిస్తుంటాయి. దీని ఫలితంగా కోడ్ గందరగోళంగా మారుతుంది, అదనపు re-renders జరుగుతాయి మరియు స్టేట్ కాంపోనెంట్ అంతటా చెల్లాచెదురుగా ఉంటుంది.
ఈ అలవాటు అర్థం చేసుకోదగినదే. మనలో చాలామంది నేర్చుకునే మొదటి హుక్ useState, మరియు అది పనిచేస్తుంది. కానీ పనిచేయడం అంటే సరిగ్గా సరిపోవడం (fitting) అని కాదు. ప్రతి డేటా ముక్కను reactive state గా పరిగణించడం వల్ల కాంపోనెంట్ పెరిగే కొద్దీ సమస్యలు తలెత్తుతాయి.
అది మారుతున్నంత మాత్రాన దానికి స్టేట్ అవసరమని కాదు
కాలక్రమేణా మారే ప్రతి వేరియబుల్ useState లో ఉండాల్సిన అవసరం లేదు. కొన్ని విలువలు మీరు ఇప్పటికే కలిగి ఉన్న వేరే ఏదైనా దాని ఫలితాలు మాత్రమే. మీరు కేవలం firstName మరియు lastNameలను కలిపి ఒక యూజర్ యొక్క పూర్తి పేరును స్టేట్లో నిల్వ చేస్తే, ఇప్పుడు మీకు రెండు sources of truth ఉంటాయి. పేరెంట్ re-render అయినప్పుడు firstName అప్డేట్ అయితే, మీరు దానిని సింక్ చేయడానికి మరొక effect రన్ చేసే వరకు మీ fullName స్టేట్ పాతదే (stale) గా ఉంటుంది. మీకు సింక్ effect అవసరం లేదు. మీకు ఒక derived value అవసరం.
const fullName = `${firstName} ${lastName}`;
దానిని render సమయంలోనే లెక్కించండి (compute). ఒకవేళ ఆ డెరివేషన్ ఖరీదైనది (expensive) అయితే, దానిని memoize చేయండి. కానీ యూజర్ ఆ పూర్తి పేరును విడివిడి భాగాల నుండి స్వతంత్రంగా ఎడిట్ చేయగలిగితే తప్ప, దానికి ప్రత్యేకంగా useState హుక్ను ఇవ్వకండి.
ఇదే నియమం filtered lists కి కూడా వర్తిస్తుంది. మీరు allItems మరియు filteredItems రెండింటినీ స్టేట్లో ఉంచితే, మీరు మెయింటెనెన్స్ భారాన్ని రెట్టింపు చేసినట్లు అవుతుంది. Render సమయంలోనే ఫిల్టర్ చేయండి. సోర్స్ అర్రే (source array) మరియు ఫిల్టర్ టెక్స్ట్ను స్టేట్లో ఉంచండి, ఆపై కనిపించే లిస్ట్ను డెలివ్ చేయండి. దీనివల్ల ఫిల్టర్ చేసిన లిస్ట్ ఎప్పుడూ సోర్స్తో సింక్ లోనే ఉంటుంది.
కొన్ని విలువలు ఎప్పుడూ Re-renders ని ట్రిగ్గర్ చేయకూడదు
ఏదో మారింది మరియు DOM అప్డేట్ అవ్వాలని React కి చెప్పడానికి ప్రత్యేకంగా useState ఉంటుంది. ఒక విలువ మారినప్పటికీ, UI లోని ఏ భాగం ఆ మార్పును పట్టించుకోకపోతే, useRef అనేది మెరుగైన సాధనం.
టైమర్లు మరియు ఇంటర్వల్స్ దీనికి క్లాసిక్ ఉదాహరణ. setInterval IDsలను స్టేట్లో నిల్వ చేయడం వల్ల, మీరు ప్రతిసారీ టైమర్ను ప్రారంభించినా లేదా ఆపినా re-render జరుగుతుంది, అయినప్పటికీ యూజర్ ఆ ఇంటర్వల్ IDని చూడలేరు. ఒక ref ఆ విలువను React కి తెలియజేయకుండానే ఉంచుతుంది. మునుపటి propsలను ట్రాక్ చేయడం, పెయింట్ (paint) చేయకముందే DOM నోడ్స్ను కొలవడం లేదా కస్టమ్ హుక్ కోసం లేటెస్ట్ callbackను నిల్వ చేయడం వంటి వాటికి కూడా ఇదే లాజిక్ వర్తిస్తుంది. మిమ్మల్ని మీరు ప్రశ్నించుకోండి: ఈ విలువ స్క్రీన్పై కనిపించాలా? సమాధానం 'కాదు' అయితే, దానికి బహుశా useState అవసరం లేదు.
DOM నోడ్స్ కూడా refs లలోనే ఉండాలి. మీరు ఒక DOM ఎలిమెంట్ను స్టేట్లో నిల్వ చేయవచ్చు, కానీ అలా చేయడం వల్ల ref callback రన్ అయిన తర్వాత re-render ట్రిగ్గర్ అవుతుంది. చాలా సందర్భాలలో, మీకు ఆ నోడ్ ఒక imperative method లేదా కొలత (measurement) కోసం మాత్రమే అవసరమవుతుంది, దానిని భిన్నంగా రెండర్ చేయడానికి కాదు.
బూలియన్ ట్రాప్ (The Boolean Trap)
ప్రతి ఫ్లాగ్కు దాని స్వంత హుక్ ఉన్నప్పుడు, సంబంధిత UI అంశాలు విస్తరిస్తాయి. isLoading, isError, మరియు isSuccess అనే మూడు విడివిడి బూలియన్లుగా నిర్వచించబడిన కాంపోనెంట్లను మీరు చూస్తారు. సమస్య ఏమిటంటే, ఈ మూడు స్టేట్లు స్వతంత్రమైనవి కావు. ఒకవేళ isLoading మరియు isSuccess రెండూ true అయితే, మీ UI అసాధ్యమైన స్థితిలో ఉంటుంది, అయినప్పటికీ TypeScript మరియు React దానిని రెండర్ చేయడానికి అనుమతిస్తాయి.
సంబంధిత స్టేట్ను గ్రూప్ చేయడం వల్ల ఇటువంటి అసాధ్యమైన కాంబినేషన్లను నివారించవచ్చు. మూడు బూలియన్లకు బదులుగా, ఒకే ఒక status string ని ట్రాక్ చేయండి: 'idle', 'loading', 'success', లేదా 'error'. ఒక సమయంలో ఒకటి మాత్రమే యాక్టివ్గా ఉండగలదు, ఇది టైప్ లెవల్లోనే అసాధ్యమైన స్టేట్లను తొలగిస్తుంది. డేటా మరింత సంక్లిష్టంగా ఉంటే, ఒక discriminated union ఉన్న ఆబ్జెక్ట్ విషయాలను మరింత సులభతరం చేస్తుంది. ఒకే ఈవెంట్ హ్యాండ్లర్ లోపల మీరు అనేక useState కాల్స్ను అప్డేట్ చేస్తున్నట్లు మీకు అనిపిస్తే, ఆ విలువలు కలిసి ఉండాలని అర్థం.
మరొక useState కి బదులుగా useReducer ని ఎంచుకోండి
స్టేట్ అప్డేట్లు ఒక 'whack-a-mole' ఆటలా మారే స్థాయి ఉంటుంది. మీరు ఒకే ఫంక్షన్ లోపల setA, తర్వాత setB, ఆపై కండిషనల్గా setC అని పిలుస్తారు. ఆ కోడ్ను చదివే తదుపరి డెవలపర్, కాంపోనెంట్ నిజంగా ఏమి చేస్తుందో అర్థం చేసుకోవడానికి ఆ క్రమాన్ని (sequence) ట్రాస్ చేయాల్సి ఉంటుంది.
ఇక్కడ useReducer అద్భుతంగా పనిచేస్తుంది. ఇది useState కంటే అధునాతనమైనది కాబట్టి దానిని రీప్లేస్ చేయదు; స్టేట్ లాజిక్ అవసరమైనప్పుడు దానిని రీప్లేస్ చేస్తుంది. ఒక reducer స్టేట్ ఎలా మారుతుందో కేంద్రీకరిస్తుంది. ఈవెంట్ హ్యాండ్లర్ల అంతటా ఇంపరేటివ్స్ను చల్లేయడానికి బదులుగా, మీరు ఒక ఉద్దేశ్యాన్ని (intention) డిస్పాచ్ చేస్తారు: dispatch({ type: 'submitted' }). తదుపరి స్టేట్ ఎలా ఉండాలో reducer నిర్ణయిస్తుంది. దీనివల్ల టెస్టింగ్ చాలా సులభం అవుతుంది, ఎందుకంటే మీ స్టేట్ లాజిక్ ఒక pure function. ఇది డీబగ్గింగ్ను కూడా సులభతరం చేస్తుంది, ఎందుకంటే ప్రతి మార్పు ఒక ట్రేసబుల్ (traceable) యాక్షన్ను వదిలి వెళ్తుంది.
ఒక reducer ఉపయోగించడానికి మీకు Redux అవసరం లేదు. ఒకవేళ మూడు లేదా అంతకంటే ఎక్కువ state variables ఒకేసారి అప్డేట్ కావాల్సి ఉన్నా, లేదా మీ తదుపరి state మునుపటి దానిపై ఎక్కువగా ఆధారపడి ఉన్నా, reducer ఆ componentను గణనీయంగా సరళతరం చేస్తుంది.
State నిజంగా ఎక్కడ ఉంటుంది
కొన్నిసార్లు సమస్య stateని ఎలా నిల్వ చేయాలనేది కాదు, ఎక్కడ నిల్వ చేయాలనేది. వేరే చోట అవసరమవుతుందేమోనని stateని కేవలం ఒక parent componentకి మార్చడం (hoisting) అనేది ఒక సాధారణ తప్పు. ఒకవేళ ఒకే ఒక leaf component ఒక stateని ఉపయోగిస్తుంటే, దానిని అక్కడే ఉంచండి. దీనినే colocation అంటారు, ఇది మార్పుల వల్ల కలిగే ప్రభావాన్ని (blast radius) తగ్గిస్తుంది. ఒక child component ఓపెన్ అయినందున parent componentని re-render చేయించకండి.
