প্রায় যেকোনো React codebase খুললেই আপনি একই প্রবণতা দেখতে পাবেন। একজন ডেভেলপার একটি ভ্যালু ট্র্যাক করতে চান, তাই তিনি useState ব্যবহার করেন। একটি কাউন্টার প্রয়োজন? useState। একটি সাময়িক ইনপুট ভ্যালু? useState। একটি মোডাল ফ্লিপ করার জন্য একটি বুলিয়ান? useState। শীঘ্রই, একটি মাত্র কম্পোনেন্টে এক ড dozen আলাদা হুক থাকে, যার প্রতিটি ডেটার একটি ক্ষুদ্র অংশ পরিচালনা করে যা রেন্ডারের মাধ্যমে স্থায়ী হওয়ার প্রয়োজন হতে পারে বা নাও হতে পারে। এর ফলে কোডটি অগোছালো হয়ে যায়, অতিরিক্ত re-renders ঘটে এবং কম্পোনেন্টের মধ্যে স্টেটগুলো ছড়িয়ে ছিটিয়ে থাকে।

এই অভ্যাসটি বোঝা সম্ভব। useState হলো প্রথম হুক যা আমাদের মধ্যে বেশিরভাগ মানুষ শেখে, এবং এটি কাজও করে। কিন্তু কাজ করা মানেই সঠিক উপায়ে মানিয়ে নেওয়া নয়। প্রতিটি ডেটাকে reactive state হিসেবে বিবেচনা করা এমন সব সমস্যা তৈরি করে যা কম্পোনেন্ট বড় হওয়ার সাথে সাথে প্রকাশ পেতে শুরু করে।

পরিবর্তন হলেই যে সেটির State প্রয়োজন হবে এমন নয়

সময়ের সাথে সাথে পরিবর্তন হওয়া প্রতিটি ভেরিয়েবল useState-এ থাকার প্রয়োজন নেই। কিছু ভ্যালু কেবল আপনার কাছে ইতিমধ্যে থাকা অন্য কিছুর ফলাফল মাত্র। আপনি যদি কেবল firstName এবং lastName কনক্যাটিনেট (concatenate) করার জন্য ইউজারের পুরো নাম স্টেট হিসেবে সংরক্ষণ করেন, তবে এখন আপনার কাছে তথ্যের দুটি উৎস (two sources of truth) রয়েছে। যখন প্যারেন্ট রেন্ডার হওয়ার কারণে firstName আপডেট হয়, তখন আপনার fullName স্টেটটি ততক্ষণ পর্যন্ত stale বা পুরনো থাকে যতক্ষণ না আপনি এটি সিঙ্ক করার জন্য অন্য কোনো effect চালান। আপনার কোনো sync effect প্রয়োজন নেই। আপনার প্রয়োজন একটি derived value।

const fullName = `${firstName} ${lastName}`;

রেন্ডারের সময় এটি গণনা করুন। যদি এই গণনাটি ব্যয়বহুল (expensive) হয়, তবে এটি memoize করুন। কিন্তু যতক্ষণ না ব্যবহারকারী সেই পুরো নামটি তার অংশগুলোর থেকে স্বাধীনভাবে এডিট করতে পারছেন, ততক্ষণ এটিকে নিজস্ব useState হুক দেবেন না।

একই নিয়ম ফিল্টার করা লিস্টের ক্ষেত্রেও প্রযোজ্য। আপনি যদি স্টেট হিসেবে allItems এবং filteredItems উভয়ই রাখেন, তবে আপনি আপনার রক্ষণাবেক্ষণের ক্ষেত্র (maintenance surface) দ্বিগুণ করে ফেলছেন। রেন্ডারের সময় ফিল্টার করুন। সোর্স অ্যারে (source array) এবং ফিল্টার টেক্সট স্টেট হিসেবে রাখুন, তারপর দৃশ্যমান লিস্টটি derive করুন। এটি নিশ্চিত করে যে ফিল্টার করা লিস্টটি সোর্সের সাথে কখনোই অসামঞ্জস্যপূর্ণ (out of sync) হবে না।

কিছু ভ্যালুর কখনোই Re-renders ট্রিগার করা উচিত নয়

useState বিশেষভাবে তৈরি করা হয়েছে React-কে এটি জানাতে যে কিছু পরিবর্তন হয়েছে এবং DOM-এর আপডেট করা প্রয়োজন হতে পারে। যদি একটি ভ্যালু পরিবর্তিত হয় কিন্তু UI-এর কোনো অংশ সেই পরিবর্তনের কথা না জানে, তবে useRef একটি উন্নত টুল।

টাইমার এবং ইন্টারভাল হলো এর ক্লাসিক উদাহরণ। স্টেট হিসেবে setInterval ID সংরক্ষণ করলে প্রতিবার টাইমার শুরু বা বন্ধ করার সময় একটি re-render ঘটে, যদিও ব্যবহারকারী ইন্টারভাল ID দেখতে পান না। একটি ref সেই ভ্যালুটিকে React-কে না জানিয়ে ধরে রাখে। একই যুক্তি আগের props ট্র্যাক করা, পেইন্টের (paint) আগে DOM নোড পরিমাপ করা, বা একটি কাস্টম হুকের জন্য সর্বশেষ callback সংরক্ষণ করার ক্ষেত্রেও প্রযোজ্য। নিজেকে প্রশ্ন করুন: এই ভ্যালুটির কি স্ক্রিনে প্রদর্শিত হওয়া প্রয়োজন? যদি উত্তর 'না' হয়, তবে সম্ভবত এর জন্য useState প্রয়োজন নেই।

DOM নোডগুলোকেও refs-এ রাখা উচিত। যদিও আপনি স্টেট হিসেবে একটি DOM এলিমেন্ট সংরক্ষণ করতে পারেন, তবে তা করলে ref callback চলার পরে একটি re-render ট্রিগার হয়। বেশিরভাগ ক্ষেত্রে, আপনার নোডটি কেবল একটি imperative method বা পরিমাপের (measurement) জন্য প্রয়োজন হয়, এটিকে ভিন্নভাবে রেন্ডার করার জন্য নয়।

The Boolean Trap

যখন প্রতিটি ফ্ল্যাগ (flag) তার নিজস্ব হুক পায়, তখন সম্পর্কিত UI বিষয়গুলো অগোছালো হয়ে পড়ে। আপনি এমন কম্পোনেন্ট দেখতে পাবেন যেখানে isLoading, isError, এবং isSuccess তিনটি আলাদা বুলিয়ান হিসেবে সংজ্ঞায়িত করা হয়েছে। সমস্যা হলো এই তিনটি স্টেট স্বাধীন নয়। যদি isLoading এবং isSuccess উভয়ই true হয়, তবে আপনার UI একটি অসম্ভব অবস্থায় থাকে, তবুও TypeScript এবং React আপনাকে এটি রেন্ডার করতে দেবে।

সম্পর্কিত স্টেটগুলোকে গ্রুপ করলে এই ধরনের অবৈধ কম্বিনেশন এড়ানো যায়। তিনটি বুলিয়ানের পরিবর্তে, একটি একক স্ট্যাটাস স্ট্রিং ট্র্যাক করুন: 'idle', 'loading', 'success', অথবা 'error'। এক সময়ে কেবল একটিই সক্রিয় থাকতে পারে, যা টাইপ লেভেলে অসম্ভব স্টেটগুলোকে দূর করে। যদি ডেটা আরও জটিল হয়, তবে একটি discriminated union সহ একটি অবজেক্ট বিষয়টিকে আরও পরিষ্কার করে তোলে। যখন আপনি দেখবেন যে একই ইভেন্ট হ্যান্ডলারের ভেতরে আপনি একাধিক useState কল আপডেট করছেন, তখন বুঝবেন যে সেই ভ্যালুগুলো একসাথে থাকা উচিত।

আরেকটি useState নয়, useReducer ব্যবহার করুন

এমন একটি পর্যায় আসে যখন স্টেট আপডেটগুলো 'whack-a-mole' খেলার মতো হয়ে যায়। আপনি একটি ফাংশনের ভেতরেই setA, তারপর setB, এবং তারপর শর্তসাপেক্ষে setC কল করছেন। পরবর্তী ডেভেলপারকে কোডটি বুঝতে হলে কম্পোনেন্টটি আসলে কী করে তা বোঝার জন্য পুরো সিকোয়েন্সটি অনুসরণ করতে হবে।

এখানে useReducer দারুণ কাজ করে। এটি useState-এর বিকল্প নয় কারণ এটি আরও উন্নত; এটি useState-এর বিকল্প কারণ লজিকটি এটি দাবি করে। একটি reducer স্টেট কীভাবে পরিবর্তিত হবে তা কেন্দ্রীয়ভাবে পরিচালনা করে। ইভেন্ট হ্যান্ডলারগুলোতে বিভিন্ন imperative কল ছড়িয়ে দেওয়ার পরিবর্তে, আপনি একটি উদ্দেশ্য (intention) dispatch করেন: dispatch({ type: 'submitted' })। পরবর্তী স্টেট কেমন হবে তা reducer নির্ধারণ করে। এটি টেস্টিং সহজ করে তোলে, কারণ আপনার স্টেট লজিক একটি pure function। এটি ডিবাগিংকেও সহজ করে তোলে, কারণ প্রতিটি পরিবর্তন একটি অনুসরণযোগ্য (traceable) action রেখে যায়।

একটি reducer ব্যবহারের জন্য আপনার Redux-এর প্রয়োজন নেই। যদি আপনার তিনটি বা তার বেশি state variable থাকে যা একসাথে আপডেট হয়, অথবা যদি আপনার পরবর্তী state আগেরটির ওপর ব্যাপকভাবে নির্ভর করে, তবে একটি reducer কম্পোনেন্টকে নাটকীয়ভাবে সহজ করে তোলে।

স্টেট আসলে কোথায় থাকে

মাঝে মাঝে সমস্যাটি স্টেট কীভাবে সংরক্ষণ করছেন তাতে নয়, বরং কোথায় করছেন তাতে। একটি সাধারণ ভুল হলো স্টেটকে কোনো parent কম্পোনেন্টে hoisting করা শুধুমাত্র এই কারণে যে এটি অন্য কোথাও প্রয়োজন হতে পারে। যদি কেবল একটি leaf component একটি নির্দিষ্ট স্টেট ব্যবহার করে, তবে সেটি সেখানেই রাখুন। এটি হলো colocation, যা পরিবর্তনের প্রভাব (blast radius) কমিয়ে দেয়। একটি child ওপেন করার কারণে parent-কে re-render করাবেন না