সরাসরি পর্বে যান
পর্ব ১২ · ৯৩-এর মধ্যে · মডিউল ২

Form — controlled input, submit আর ব্যবহারকারীর ইনপুট

Forms in React — Controlled Inputs, Submit and User Input

Taught in Bangla, this lesson explains who holds a form value, controlled versus uncontrolled inputs, the full path of one keystroke with value and onChange, five input types and why numbers arrive as strings, many fields with one handler, submit, validation, error messages and accessibility, FormData and useRef, React 19 form actions with useActionState and useFormStatus, the Bazarghar checkout form, and Bangla input with IME, digits and character counts.

এই পর্বে যা শিখবেন

  • Controlled আর uncontrolled input-এর পার্থক্য, আর একটি keystroke-এর পুরো পথ
  • Text, number, select, checkbox ও radio একটি handler দিয়ে সামলানো
  • Submit, preventDefault, validation আর accessible error দেখানো
  • FormData, useRef, React 19-এর form action, useActionState, useFormStatus
  • “বাজারঘর”-এর checkout ঠিকানা form, বাংলাদেশি ফোন যাচাই আর বাংলা IME সামলানো

১ · মান কে মনে রাখে — controlled আর uncontrolled

একটি ব্যাংকের কাউন্টার ভাবুন। প্রথম কাউন্টারে আপনি প্রতিটি শব্দ বলছেন, আর কর্মকর্তা সঙ্গে সঙ্গে কম্পিউটারে তুলছেন। ভুল বললে তিনি তখনই থামাতে পারেন। দ্বিতীয় কাউন্টারে আপনি নিজে একটি জমার স্লিপ পূরণ করেন, শেষে স্লিপটি জমা দেন। কর্মকর্তা তখনই প্রথম পড়েন।

React-এর form-এ এই দুটি পথই আছে। প্রথমটি controlled input: প্রতিটি অক্ষর React state-এ যায়, আর render সেই মানই input-এ বসায়। দ্বিতীয়টি uncontrolled input: মান থাকে browser-এর DOM-এ, আর React পড়ে শুধু submit-এর সময়।

মানের মালিক দুই জায়গায়

Controlled: state owns the value · Uncontrolled: the DOM owns the value

Controlled input একটি চক্র, uncontrolled input একবার পড়া Controlled: প্রতিটি অক্ষরে চক্র React state name = 'রহিম' <input> পর্দায় রহিম value onChange প্রতিটি অক্ষরে render তাই সঙ্গে সঙ্গে যাচাই ও preview সম্ভব Uncontrolled: submit-এ একবার পড়া <input> মান DOM-এ submit handler FormData পড়ে submit টাইপ করার সময় কোনো render নেই তবে মাঝপথে মান জানতে হলে পড়তে হয়
ছবি ১২.১ — ওপরের পথে React সব সময় মান জানে। নিচের পথে React জানে শুধু পড়ার মুহূর্তে।

HTML form-এর ভিত্তি এখানে আবার শেখানো হবে না। HTML-এর form-এর শুরু, form control আর browser-এর validation, আর সাধারণ JavaScript-এ form যাচাই আলাদা পর্বে আছে। এখানে দেখা হবে React এই জগতের সঙ্গে কীভাবে মেলে।

এই পর্বের লক্ষ্য একটি বাস্তব জিনিস বানানো: বাজারঘরের checkout ঠিকানা form. ক্রেতা এখানে নাম, ফোন, শহর দেবেন, পেমেন্ট বেছে নেবেন, তারপর অর্ডার করবেন। একটি অনলাইন দোকানে এই পাতাটিই টাকা আসার শেষ দরজা, তাই ছোট একটি ভুল মানে হারানো অর্ডার। পথে প্রতিটি ধারণা একে একে আসবে, শেষে সব জোড়া লাগবে।

মূল ধারণা

একটি input-এর মালিক একজনই হবেন, React অথবা DOM. value দিলে React মালিক, defaultValue দিলে DOM মালিক। একই input জীবনের মাঝখানে মালিক বদলাতে পারে না।

২ · value আর onChange — একটি keystroke-এর পুরো পথ

Controlled input-এর নিয়ম দুটি। value দিয়ে state থেকে মান বসান, আর onChange দিয়ে নতুন মান state-এ ফেরত রাখুন। useState আর event handler এসেছে পর্ব ১০-এ (State আর event). এখানে দুটিকে একসাথে দেখুন। React-এর onChange নামটি একটু বিভ্রান্তিকর, কারণ এটি browser-এর change-এর মতো field ছাড়ার পরে নয়, প্রতিটি input event-এ চলে।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
function Preview() {
  const [name, setName] = useState('');
  const [qty, setQty] = useState('2');
  console.log('render', JSON.stringify({ name, qty }));
  const total = 120 * Number(qty);
  return (
    <div>
      <p><label>নাম <input value={name}
        onChange={e => setName(e.target.value)} /></label></p>
      <p><label>কেজি <input type="number" min="1" value={qty}
        onChange={e => setQty(e.target.value)} /></label></p>
      <p>{name || 'ক্রেতা'}: {total} টাকা</p>
      <p>typeof qty = {typeof qty}, qty + 1 = {qty + 1}</p>
    </div>
  );
}
createRoot(document.getElementById('root')).render(<Preview />);
</script>

চালানো মাত্র একটি লাইন আসে: render {"name":"","qty":"2"}. এরপর নাম ঘরে “রহিম” লিখলে Edge-এ চারটি নতুন লাইন এলো, শেষটি render {"name":"রহিম","qty":"2"}. চোখে তিন অক্ষর, কিন্তু Unicode-এ চারটি (র, হ, ি, ম), তাই চারটি input event আর ৪ বার render. কেজি ৩ করলে পর্দায় আসে রহিম: 360 টাকা আর typeof qty = string, qty + 1 = 31. গুণে JavaScript string-কে সংখ্যায় বদলেছে, কিন্তু যোগে জোড়া লাগিয়েছে।

এবার ধীর গতিতে দেখুন, “রহি”-এর পরে “ম” টাইপ করলে ঠিক কী ঘটে। খেয়াল করুন, প্রথম কাজটি React করে না, করে browser.

1function NameBox() {2  const [name, setName] = useState('রহি');3  return (4    <input value={name}5      onChange={e => setName(e.target.value)} />6  );7}
এই মুহূর্তের মান
state name
DOM value
e.target.value
  1. শান্ত অবস্থা। State আর DOM দুটিই রহি. Input-টি শেষ render-এর ফল।
  2. Browser নিজে অক্ষর বসায়। “ম” চাপতেই browser DOM-এ রহিম লিখে ফেলে। এক মুহূর্তের জন্য DOM আর state আলাদা। State এখনো রহি.
  3. Event পৌঁছায়। Browser input event ছাড়ে। React সেটি ধরে আপনার onChange চালায়। e.target.value হলো DOM-এর নতুন মান, রহিম.
  4. setName কাজ জমা দেয়। setName('রহিম') সঙ্গে সঙ্গে চলতি name বদলায় না। React একটি নতুন render-এর কাজ তালিকায় তোলে। এটি পর্ব ১০-এর snapshot-এর নিয়ম, মানে চলতি render-এর মান স্থির থাকে।
  5. আবার render. NameBox আবার চলে, এবার name হলো রহিম. নতুন JSX বলছে value="রহিম".
  6. Commit. React দেখে DOM-এ আগে থেকেই রহিম আছে, তাই আর লিখতে হয় না। State আর DOM আবার এক। পুরো কাজ পরের অক্ষরের আগেই শেষ।

ধাপ ৪-এ setName না ডাকলে কী হয়? DOM-এ অক্ষর বসেছিল, কিন্তু state বদলায়নি। তখন React DOM-কে আগের মানে ফিরিয়ে দেয়। Edge-এ পরীক্ষায় onChange ঠিকই আমx দেখল, অথচ input থেকে গেল আম. ব্যবহারকারীর চোখে মনে হয় কিবোর্ড নষ্ট। AI-এর লেখা কোডে এই ভুল প্রায়ই পাওয়া যায়, যখন state আর input-এর নাম মেলে না।

Development build-এ React এসব ভুল নিয়ে সতর্ক করে। পেজের লাইভ বাক্সে React-এর production build চলে, তাই সেখানে এই বার্তা দেখা যাবে না। নিচেরটি আসল development build থেকে নেওয়া।

সত্যিকারের run · লেখার সময়ে React 19.3.1, Node 24, jsdom · ~/bazarghar

$ node warn.mjs
You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.
A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. …

দ্বিতীয় বার্তাটির সাধারণ কারণ: useState() খালি রাখা, তাই শুরুতে value হলো undefined. React undefined-কে “মান দেওয়া হয়নি” ধরে, অর্থাৎ uncontrolled. পরে string এলে মালিক বদলে যায়। সমাধান: শুরুর মান সব সময় '', আর API থেকে null এলে value={v ?? ''}.

৩ · পাঁচ রকম input, আর “সংখ্যা” যখন string

সব input একভাবে কাজ করে না। কোনটিতে কোন prop, আর event থেকে কী পড়বেন, তা নিচের ছকে আছে।

InputState-এর মানJSX propHandler-এ পড়বেন
Text, tel, email, textareastringvaluee.target.value
Numberstring (গণনায় বদলান)valuee.target.value
Selectstring<select value>e.target.value
Checkboxbooleancheckede.target.checked
Radiostring, দলের একটি মানchecked={pay === 'cod'}e.target.value

HTML-এ <option selected> লেখা হয়, React-এ পুরো <select>-এ value দিতে হয়। Radio-র ক্ষেত্রে দলের সব input-এর name এক থাকে, আর checked হিসাব করা হয় তুলনা দিয়ে। <textarea>-এও লেখা ট্যাগের ভেতরে নয়, value prop-এ যায়। চা-স্টলের খাতার মতো ভাবুন: নগদ না বাকি, এটি হ্যাঁ-না, তাই checkbox. চা, কফি না দুধ চা, একটিই বেছে নিতে হয়, তাই radio বা select.

Number input আসলে string দেয়

বাজারঘরের কার্টে ২ কেজি ডাল, প্রতি কেজি ১২০ টাকা, ডেলিভারি ৬০ টাকা। মোট হওয়ার কথা ২৪০ + ৬০ = ৩০০ টাকা। পরিমাণ আসে input থেকে, ডেলিভারি আসে select থেকে, দুটিই string. এবার দেখুন কী হয়:

const qty = '2';                  // input থেকে
const delivery = '60';            // select থেকে
const subtotal = 120 * qty;       // 240, গুণ string-কে সংখ্যা বানায়
const wrong = subtotal + delivery;          // '24060', যোগ জোড়া লাগায়
const right = subtotal + Number(delivery);  // 300

রূপান্তরের ফলগুলো মনে রাখার মতো, কারণ প্রতিটি আলাদা ফাঁদ। নিচের মানগুলো Edge-এ চালিয়ে পাওয়া।

কোডফলমানে
'2' + 1'21'যোগ string জোড়া লাগায়
Number('')0খালি ঘর শূন্য হয়ে যায়, যাচাই ফাঁকি পড়ে
Number('abc')NaNসংখ্যা নয়
parseInt('12abc')12আবর্জনা চুপচাপ বাদ দেয়

State-এ number রাখার লোভ এড়ানো ভালো। ব্যবহারকারী ঘর খালি করতে চাইলে Number('') তাকে 0 বানিয়ে ফেলে, আর সে 0 মুছতে পারে না। react.dev-এর উদাহরণেও মান string হিসেবেই রাখা হয়েছে, গণনার সময় Number(). ফোন নম্বর, OTP, পোস্ট কোডে কখনোই type="number" নয়, কারণ শুরুর 0 সংখ্যায় থাকে না: Number('01712345678') হয় 1712345678.

৪ · এক handler-এ অনেক field, আর submit

দশটি field-এর জন্য দশটি useState আর দশটি handler লেখা ক্লান্তিকর। সহজ পথ: সব মান একটি object-এ, আর input-এর name হবে object-এর key. তারপর JavaScript-এর computed key [name] সঠিক ঘরটি বদলায়।

function change(e) {
  const { name, type, value, checked } = e.target;
  setForm(old => ({ ...old, [name]: type === 'checkbox' ? checked : value }));
}

এখানে {...old} জরুরি, কারণ state object সরাসরি বদলানো যায় না, নতুন object দিতে হয়। আর old => রূপটি নিশ্চিত করে সবচেয়ে নতুন state-এর ওপর কাজ হচ্ছে।

Submit আর preventDefault

Browser-এর পুরোনো নিয়ম হলো, form submit হলে পুরো পাতা নতুন request পাঠায় এবং নতুন পাতা লোড করে। তাতে আপনার সব state হারায়। তাই React-এর onSubmit-এ প্রথম লাইনে থাকে e.preventDefault(). এটি browser-কে বলে, “নিজের নিয়মে পাতা বদলাবেন না, এখন থেকে কাজ আমার।”

onClick-এ নয়, <form onSubmit>-এ কাজ ধরুন। তাহলে input-এ Enter চাপলেও submit হয়, আর button-এ type="submit" ডিফল্ট থাকে। Form-এর ভেতরের অন্য button-এ type="button" লিখুন, নইলে সেটিও submit করে ফেলবে। TypeScript-এ handler-এর type SubmitEvent<HTMLFormElement>. বর্তমান @types/react 19.3-এ পুরোনো FormEvent deprecated.

৫ · Validation, error দেখানো আর accessibility

Validation মানে ডেটা পাঠানোর আগে যাচাই। ব্যবহারকারীর দিক থেকে এর উদ্দেশ্য একটাই: ভুলটি দ্রুত জানানো, যাতে server পর্যন্ত যাওয়ার সময় নষ্ট না হয়। নিচের form-এ নাম, ফোন আর একটি checkbox একটি handler দিয়ে চলছে। খালি অবস্থায় “অর্ডার” চাপুন, তারপর ফোনে 0171 লিখে আবার চাপুন।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
function validate(f) {
  const e = {};
  if (!f.name.trim()) e.name = 'নাম লিখুন';
  if (!/^01[3-9]\d{8}$/.test(f.phone)) e.phone = '১১ সংখ্যার মোবাইল নম্বর দিন';
  return e;
}
function Form() {
  const [f, setF] = useState({ name: '', phone: '', gift: false });
  const [errors, setErrors] = useState({});
  function change(e) {
    const { name, type, value, checked } = e.target;
    setF(old => ({ ...old, [name]: type === 'checkbox' ? checked : value }));
  }
  function submit(e) {
    e.preventDefault();
    const found = validate(f);
    setErrors(found);
    console.log(Object.keys(found).length ? 'আটকে গেল: ' + Object.keys(found) : 'যাবে: ' + JSON.stringify(f));
  }
  const box = (n, t) => <p><label>{t} <input name={n} value={f[n]} onChange={change} aria-invalid={!!errors[n]} aria-describedby={errors[n] ? n + '-e' : undefined} /></label> <b id={n + '-e'}>{errors[n]}</b></p>;
  return <form onSubmit={submit} noValidate>{box('name', 'নাম')}{box('phone', 'ফোন')}
    <p><label><input type="checkbox" name="gift" checked={f.gift} onChange={change} /> উপহার মোড়ক</label></p>
    <button>অর্ডার</button></form>;
}
createRoot(document.getElementById('root')).render(<Form />);
</script>

চালানো মাত্র কোনো আউটপুট আসে না, কারণ console.log আছে শুধু submit-এ। Edge-এ খালি form submit করলে এলো আটকে গেল: name,phone. নাম “রহিমা” আর ফোন 0171 দিলে আটকে গেল: phone. ফোন 01712345678 করে checkbox চিহ্নিত করলে যাবে: {"name":"রহিমা","phone":"01712345678","gift":true}. noValidate browser-এর নিজের popup বন্ধ রাখে, যাতে আপনার লেখা বাংলা বার্তা দেখা যায়।

এবার নিচের ধাপগুলোতে ব্যর্থ submit-এর ভেতরটা দেখুন। উদাহরণের ডেটা: নাম রহিমা, ফোন 0171.

1function submit(e) {2  e.preventDefault();3  const found = validate(form);4  flushSync(() => setErrors(found));5  if (found.phone) phoneRef.current.focus();6  else send(form);7}
এই মুহূর্তের মান
পাতা লোড
found
errors state
aria-invalid
focus
  1. Submit event. ব্যবহারকারী “অর্ডার” চাপলেন। Browser submit event ছাড়ল। কিছু না করলে পাতা নতুন করে লোড হবে।
  2. Browser-কে থামানো। preventDefault পাতা লোড বন্ধ করল। এখন state নিরাপদ।
  3. যাচাই। 0171-এ ৪টি অঙ্ক, দরকার ১১টি। তাই found-এ শুধু phone-এর ভুল। নামে ভুল নেই। Errors state এখনো খালি।
  4. Error পর্দায়। flushSync এই বদলটি সঙ্গে সঙ্গে DOM-এ পৌঁছায়। Error লেখা বসল, aria-invalid="true" আর aria-describedby জুড়ল।
  5. Focus সরানো। Cursor ভুল ঘরে গেল। Screen reader এখন label-এর সঙ্গে error-ও পড়ে। Focus আগে দিলে error তখনো DOM-এ থাকত না, তাই flushSync আগে।
  6. পাঠানো হলো না। send চলেনি। কোনো request যায়নি, server-এর ওপর কোনো চাপ পড়েনি, আর ব্যবহারকারীর লেখা সব অক্ষত।

Error কখন দেখাবেন, তাও একটি সিদ্ধান্ত। দর্জি প্রথম ফিতা ধরতেই “মাপ ভুল” বলেন না, মাপ শেষ হলে বলেন। Form-এও প্রথমবার ঘর ছাড়ার সময় যাচাই করা ভদ্র। এর জন্য onBlur আর একটি touched object যথেষ্ট:

const [touched, setTouched] = useState({});
const errors = validate(form);                // প্রতি render-এ হিসাব, আলাদা state নয়

function blur(e) {
  setTouched(old => ({ ...old, [e.target.name]: true }));
}
// দেখানোর শর্ত: ঘর ছুঁয়েছেন এবং ভুল আছে
const showPhone = touched.phone && errors.phone;

এখানে errors আলাদা state নয়, form থেকে হিসাব করা মান। যে তথ্য অন্য state থেকে বের করা যায়, তা আলাদা state-এ রাখলে দুটি একসময় আলাদা হয়ে যায়। বিশাল হিসাব না হলে render-এর সময় হিসাব করাই সরল পথ।

Error সবার জন্য: label, aria-describedby

লাল রং একা যথেষ্ট নয়। যিনি screen reader ব্যবহার করেন, তিনি রং দেখেন না। নিচের নিয়মগুলো কয়েক লাইনের কাজ, কিন্তু এগুলোই form-কে সবার ব্যবহারযোগ্য করে। বিস্তারিত আছে HTML-এর accessibility পর্বে.

  • প্রতিটি input-এ <label>: htmlFor দিয়ে (JSX-এ for নয়), অথবা input-কে label-এর ভেতরে রেখে। Placeholder label-এর বিকল্প নয়, কারণ টাইপ শুরু করলেই সেটি হারিয়ে যায়।
  • Error যুক্ত করুন: error-এর id aria-describedby-তে দিন, যাতে focus পড়লে সেটিও পড়া হয়। ARIA attribute-গুলো দেখা যায় না, শুধু সহায়ক প্রযুক্তিকে তথ্য দেয়।
  • রাজ্য জানান: aria-invalid বলে দেয় ঘরটিতে ভুল আছে। Radio-র দলকে <fieldset> আর <legend> দিয়ে ঘিরুন।
  • স্থায়ী id: reuse হওয়া component-এ হাতে লেখা id ঠোকাঠুকি করে। useId() প্রতিটি component-এর জন্য আলাদা id বানায়।

৬ · Uncontrolled input, FormData আর useRef

সব form-এ প্রতিটি অক্ষর জানার দরকার নেই। একটি সার্চ বাক্স বা ছোট যোগাযোগ form-এ submit-এর সময় মান পেলেই চলে। তখন defaultValue দিয়ে শুধু শুরুর মান দেওয়া হয়, বাকিটা DOM সামলায়। type="file" সব সময় uncontrolled, কারণ নিরাপত্তার কারণে কোড দিয়ে তার মান বসানো যায় না।

FormData হলো browser-এর API, যা form-এর সব নামওয়ালা field একবারে তুলে আনে। আর useRef একটি বাক্স দেয়, যাতে একটি DOM node ধরে রাখা যায়। এখানে শুধু focus-এর জন্য ব্যবহার। useRef-এর পুরো গল্প পর্ব ১৪-এ (Hook-এর নিয়ম আর custom hook).

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
function Contact() {
  const nameRef = useRef(null);
  const [count, setCount] = useState(0);
  console.log('render', count);
  function submit(e) {
    e.preventDefault();
    const data = Object.fromEntries(new FormData(e.currentTarget));
    console.log('FormData:', JSON.stringify(data));
    setCount(count + 1);
    e.currentTarget.reset();
    nameRef.current.focus();
  }
  return (
    <form onSubmit={submit}>
      <p><label>নাম <input ref={nameRef} name="name" defaultValue="রহিম" /></label></p>
      <p><label>শহর <select name="city" defaultValue="ঢাকা">
        <option>ঢাকা</option><option>রাজশাহী</option></select></label></p>
      <p><label><input type="checkbox" name="gift" /> উপহার</label></p>
      <button>পাঠান</button> মোট পাঠানো: {count}
    </form>
  );
}
createRoot(document.getElementById('root')).render(<Contact />);
</script>

চালালে আসে render 0. নাম ঘরে যতই টাইপ করুন, নতুন লাইন আসে না, কারণ state বদলাচ্ছে না। এটিই uncontrolled-এর লাভ। “পাঠান” চাপলে Edge-এ এলো FormData: {"name":"রহিম","city":"ঢাকা"}, তারপর render 1. খেয়াল করুন gift নেই: চিহ্ন না দেওয়া checkbox FormData-তে একেবারেই থাকে না। চিহ্ন দিলে আসে "gift":"on", মানে এটি boolean নয়, string. disabled field-ও FormData-তে যায় না।

useRef দিয়ে controlled input-এর মান পড়া বা লেখা ঠিক নয়। মান তো state-এই আছে। Ref রাখুন সেই কাজের জন্য, যা state দিয়ে করা যায় না: focus দেওয়া, লেখা select করা, স্ক্রল করা। আর e.currentTarget.reset() শুধু uncontrolled ঘর মোছে, controlled ঘর মুছতে state-কে শুরুর মানে ফেরাতে হয়। await-এর পরে e.currentTarget আর পাওয়া যায় না, তাই দরকার হলে আগেই একটি variable-এ রাখুন।

কোনটি কখন, আর একটি হিসাব

প্রয়োজনControlledUncontrolled
টাইপ করতে করতে preview বা যাচাইসহজকঠিন
এক field অন্যটির ওপর নির্ভরসহজকঠিন
JavaScript ছাড়াও form চলা (server-এর সাহায্যে)কঠিনসহজ
প্রতিটি অক্ষরে renderহয়হয় না
ফাইল বাছাইসম্ভব নয়একমাত্র পথ

Controlled input কি ধীর? নিজে নয়, ধীর হয় বড় component. ধরে নিন একটি form-এ ৪০টি field একটি component-এর state-এ, প্রতিটি field render হতে ২ ms লাগে, আর ব্যবহারকারী সেকেন্ডে ৫টি অক্ষর টাইপ করেন। এই সংখ্যাগুলো হিসাবের জন্য ধরে নেওয়া, মাপা নয়।

প্রতি keystroke: ৪০ × ২ ms = ৮০ ms
প্রতি সেকেন্ডে: ৮০ × ৫ = ৪০০ ms, অর্থাৎ main thread-এর ৪০%
State নিচে নামালে: ১ × ২ × ৫ = ১০ ms, অর্থাৎ ১%

৬০ Hz পর্দায় একটি frame-এর বাজেট প্রায় ১৬.৭ ms, তাই ৮০ ms মানে কয়েকটি frame নষ্ট, আর INP খারাপ হওয়ার ঝুঁকি। সমাধান সাধারণত তিনটি: state ছোট component-এ নামানো, ভারী অংশ আলাদা করা, অথবা বড় form-এ uncontrolled-ভিত্তিক library. শেষেরটিই পর্ব ২১-এর React Hook Form-এর মূল ভাবনা। আগে থেকে ভয় পাবেন না, ১০টি field-এর সাধারণ form-এ controlled প্রায় সব সময়ই যথেষ্ট।

৭ · React 19 — form action, useActionState, useFormStatus

এতক্ষণ pending, error আর পাঠানোর কাজ হাতে লিখতে হয়েছে। ডিসেম্বর ২০২৪-এ প্রকাশিত React 19 এই নকশাটিকেই framework-এর অংশ করেছে। ভাবনাটি হলো Action: Transition-এর ভেতরে চলা async function. ডাকঘরের কাউন্টারের মতো ভাবুন। আপনি শুধু চিঠি জমা দেন। রসিদ, লাইন আর “কাজ চলছে” সাইনবোর্ড কাউন্টারের দায়িত্ব।

APIকাজযা মনে রাখবেন
<form action={fn}>Submit-এ fn(formData) চালায়preventDefault লাগে না; সফল হলে uncontrolled field reset হয়
useActionState[state, action, isPending] ফেরত দেয়Function পায় (prevState, formData), ক্রম উল্টো নয়
useFormStatusবাইরের form-এর pending, data, method, actionreact-dom থেকে আসে; form-এর ভেতরের component-এ ডাকতে হয়
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const wait = ms => new Promise(r => setTimeout(r, ms));
async function save(prev, formData) {
  const phone = formData.get('phone').trim();
  console.log('action শুরু:', phone);
  await wait(400);                 // নকল server
  console.log('action শেষ');
  if (!/^01[3-9]\d{8}$/.test(phone)) return { phone, error: 'নম্বরটি ঠিক নয়' };
  return { phone: '', error: null, done: true };
}
function Submit() {
  const { pending } = ReactDOM.useFormStatus();
  console.log('Submit: pending =', pending);
  return <button disabled={pending}>{pending ? 'পাঠানো হচ্ছে…' : 'অর্ডার'}</button>;
}
function Order() {
  const [state, action, isPending] = useActionState(save, { phone: '', error: null });
  console.log('Order: isPending =', isPending);
  return <form action={action}>
    <label>ফোন <input name="phone" defaultValue={state.phone} /></label> <Submit />
    <p role="alert">{state.error}{state.done && 'অর্ডার জমা হয়েছে'}</p>
  </form>;
}
createRoot(document.getElementById('root')).render(<Order />);
</script>

চালালে দুটি লাইন: Order: isPending = false আর Submit: pending = false. ফোনে 0171 লিখে “অর্ডার” চাপলে Edge-এ ক্রমটি হলো action শুরু: 0171, Order: isPending = true, Submit: pending = true, action শেষ, তারপর দুটিই false. আসল project-এ import { useFormStatus } from 'react-dom' লিখবেন। এই লাইভ বাক্সে React-এর export আগে থেকেই আছে, কিন্তু এই hook React DOM-এর, তাই ReactDOM.useFormStatus.

এখন lifecycle-টি ধাপে ধাপে দেখুন। ডেটা একই: ফোন 0171, যা server বাতিল করবে।

1async function save(prev, formData) {2  const phone = formData.get('phone');3  await wait(400);4  return { phone, error: 'ঠিক নয়' };5}6const [state, action, isPending] =7  useActionState(save, initial);8<form action={action}>9  <input name="phone" defaultValue={state.phone} />
এই মুহূর্তের মান
isPending
form pending
state.error
input পর্দায়
  1. অপেক্ষা। ব্যবহারকারী 0171 লিখেছেন। মান DOM-এ আছে, React তা জানে না।
  2. Submit. React নিজে preventDefault করে, FormData বানায়, একটি Transition খোলে আর save ডাকে। লগ অনুযায়ী action শুরু আগে আসে, pending render পরে।
  3. Pending render. isPending আর useFormStatus().pending দুটিই true. Button নিষ্ক্রিয়। এই সময় data.get('phone') থেকে পাঠানো মানও পড়া যায়।
  4. অপেক্ষার সময়। await চলছে। পাতা জমে যায় না, ব্যবহারকারী স্ক্রল করতে পারেন। Transition খোলা আছে।
  5. ফল এলো। save object ফেরত দিল। React একই commit-এ state বসায় আর দুই pending-ই false করে। Error এখন পর্দায়।
  6. Reset. Function error ছোড়েনি, তাই React একে সফল ধরে uncontrolled input reset করে। defaultValue এখন 0171, তাই লেখা থেকে গেল। defaultValue না থাকলে ঘরটি খালি হতো।

শেষ ধাপটিই সবচেয়ে বড় ফাঁদ। Validation error ফেরত দিলেও React তা সফল ধরে। Edge-এ defaultValue ছাড়া form-এ ব্যর্থ action-এর পরে ঘরটি হলো '', মানে ব্যবহারকারীর লেখা হারিয়ে গেল। defaultValue={state.phone} দিলে থাকল '123'. Controlled input-এ এই reset লাগে না; একই পরীক্ষায় controlled ঘরটি অক্ষত, আর পাশের uncontrolled ঘর খালি। আর function-এর ভেতর থেকে error ছুড়লে নিকটতম error boundary ধরে, তাই প্রত্যাশিত ভুল ফেরত দিন, ছুড়বেন না।

আরেকটি বাস্তব পরীক্ষা। তিনবার দ্রুত ক্লিক করলে কী হয়? disabled ছাড়া সাধারণ onSubmit-এ Edge-এ POST 1, POST 2, POST 3 একসাথে গেল। useActionState-এ তিনটি action এক লাইনে পরপর চলল, প্রতিটি আগেরটির ফল নিয়ে। দুই ক্ষেত্রেই অর্ডার তিনটি! তাই disabled={pending} জরুরি। Button নিষ্ক্রিয় থাকলে input-এ Enter চাপলেও submit হয়নি, HTML-এর নিয়মেই। তবু এটি শুধু সামনের পাহারা। আসল ডবল অর্ডার ঠেকায় server, যা পর্ব ৩৩-এর idempotency-তে দেখবেন।

Disabled, readOnly আর pending-এর বার্তা

disabled আর readOnly একই জিনিস নয়। readOnly ঘর বদলানো যায় না, কিন্তু focus পায় আর FormData-তে যায়। disabled ঘর focus পায় না, FormData-তেও যায় না। বাজারঘরে ঠিকানার সাথে একটি অপরিবর্তনীয় অর্ডার নম্বর পাঠাতে হলে readOnly বা <input type="hidden"> নিন, disabled নয়। তবে মনে রাখুন, hidden ঘরও ব্যবহারকারী বদলাতে পারেন, তাই সেটিও server যাচাই করবে।

Pending-এর সময় শুধু button ধূসর করা যথেষ্ট নয়। লেখাও বদলান, যেমন “পাঠানো হচ্ছে…”, আর ফলাফল role="status" বা role="alert" ঘরে দিন, যাতে screen reader জানায়। ব্যর্থ হলে ব্যবহারকারীর লেখা অক্ষত রেখে আবার চেষ্টার সুযোগ দিন। ধীর নেটওয়ার্কে এই কয়েক সেকেন্ডই ঠিক করে দেয় মানুষ আবার ক্লিক করবেন, না দোকান ছেড়ে যাবেন।

তিন পথের কোনটি বাছবেন

পথযেখানে মানায়দুর্বল দিক
Controlled + onSubmitলাইভ preview, নির্ভরশীল ঘর, ছোট থেকে মাঝারি formPending আর error হাতে লিখতে হয়
Uncontrolled + actionServer-এ যাচাই, সরল form, পর্ব ৫৪-এর Server Actionটাইপের সময় মান জানা যায় না; ব্যর্থতায় মান রাখতে হয়
Form libraryঅনেক ঘর, বারবার আসা ঘর, জটিল নিয়ম (পর্ব ২১)নতুন dependency, শেখার খরচ

দুটি ছোট সুবিধাও জেনে রাখুন। একটি form-এ দুই কাজের জন্য button-এ formAction={fn} দেওয়া যায়, যেমন “খসড়া রাখুন” আর “অর্ডার করুন”। আর পুরো form নতুন করে শুরু করতে <form key={id}> বদলানো সবচেয়ে সরল পথ, কারণ key বদলালে React পুরোনো component ফেলে নতুনটি বসায়। পর্ব ১১-এ (List, key আর শর্তসাপেক্ষ UI) key-এর এই ভূমিকা দেখেছেন।

৮ · বাজারঘরের checkout form — ফোন যাচাই থেকে server পর্যন্ত

এবার সব একসাথে। বাজারঘরের checkout-এ ঠিকানা নেওয়া হবে। আগে ফোন নম্বরের নিয়ম। বাংলাদেশের মোবাইল নম্বর সাধারণত ১১ অঙ্কের, শুরু 01 দিয়ে, আর লেখার সময়ে তৃতীয় অঙ্কটি সাধারণত ৩ থেকে ৯-এর মধ্যে। নতুন prefix যোগ হতে পারে, তাই regex এক জায়গায় রাখা ভালো। ব্যবহারকারী বাংলায়, +88 দিয়ে, বা ড্যাশ দিয়েও লিখতে পারেন। তাই যাচাইয়ের আগে মান “স্বাভাবিক” করে নিতে হয়।

const BN = '০১২৩৪৫৬৭৮৯';

// বাংলা অঙ্ক → ASCII, ফাঁকা ও ড্যাশ বাদ, দেশের কোড বাদ
export function normalizePhone(raw: string): string {
  const ascii = raw.replace(/[০-৯]/g, d => String(BN.indexOf(d)));
  return ascii.replace(/[\s-]/g, '').replace(/^\+?88(?=01)/, '');
}

export const isBdMobile = (p: string) => /^01[3-9]\d{8}$/.test(p);

সত্যিকারের run · Node 24 · ~/bazarghar

$ node phone-check.mts
01712345678      -> 01712345678  ok
০১৭১২৩৪৫৬৭৮      -> 01712345678  ok
+8801712345678   -> 01712345678  ok
017-1234-5678    -> 01712345678  ok
8801912345678    -> 01912345678  ok
01212345678      -> 01212345678  reject
0171234567       -> 0171234567   reject

এবার form-এর মূল অংশ। Project বানানোর ধাপ পর্ব ০৫-এ, আর type-এর ব্যাখ্যা পর্ব ০৬-এ আছে। পুরো ফাইলটি strict TypeScript-এ (TypeScript 7.0.2) npx tsc পার হয়েছে। React 19.3.1 দিয়ে Edge-এ চালিয়ে দেখা হয়েছে।

type Address = {
  name: string; phone: string; city: string;
  payment: 'cod' | 'mobile'; saveAddress: boolean;
};
type Errors = Partial<Record<keyof Address, string>>;
type FieldProps = ComponentProps<'input'> & { label: string; error?: string };

function Field({ label, error, ...input }: FieldProps) {
  const id = useId();
  const errId = `${id}-err`;
  return (
    <p>
      <label htmlFor={id}>{label}</label>
      <input id={id} aria-invalid={error ? true : undefined}
        aria-describedby={error ? errId : undefined} {...input} />
      {error && <span id={errId} className="err">{error}</span>}
    </p>
  );
}

// CheckoutForm-এর ভেতরে
async function submit(e: SubmitEvent<HTMLFormElement>) {
  e.preventDefault();
  if (status === 'saving') return;           // দ্বিতীয় পাঠানো ঠেকায়
  const found = validate(form);
  flushSync(() => setErrors(found));         // error আগে DOM-এ
  const first = Object.keys(found)[0];
  if (first) {
    formRef.current?.querySelector<HTMLElement>(`[name="${first}"]`)?.focus();
    return;
  }
  setStatus('saving');
  try {
    await onSave({ ...form, phone: normalizePhone(form.phone) });
    setStatus('saved');
  } catch {
    setStatus('failed');                     // লেখা মোছে না, আবার চেষ্টা করা যায়
  }
}

// JSX-এর অংশ
<Field label="মোবাইল নম্বর" name="phone" type="tel" autoComplete="tel"
  value={form.phone} onChange={change} error={errors.phone} />
<button disabled={status === 'saving'}>
  {status === 'saving' ? 'পাঠানো হচ্ছে…' : 'অর্ডার নিশ্চিত করুন'}
</button>
<p role="status">{message}</p>

Edge-এ পরীক্ষা: খালি form submit করলে তিনটি error এলো, focus গেল name-এ, আর সেখানে aria-invalid="true". ফোনে ০১৭১২-৩৪৫৬৭৮ লিখলে ফোনের error চলে গেল। সব পূরণ করলে onSave পেল "phone":"01712345678", অপেক্ষার সময় button নিষ্ক্রিয় ছিল, শেষে “অর্ডার জমা হয়েছে।” এখানে type="tel" মোবাইলে সংখ্যার কিবোর্ড আনে, আর autoComplete browser-কে আগের ঠিকানা ভরতে সাহায্য করে।

Browser-এর যাচাই নিরাপত্তা নয়

এখন সবচেয়ে গুরুত্বপূর্ণ কথা। এই সব যাচাই চলছে ব্যবহারকারীর নিজের কম্পিউটারে। আপনার form-কে পাশ কাটিয়ে যে কেউ সরাসরি server-এ request পাঠাতে পারেন। নিচে একটি ইচ্ছে করে সরল server, যা যাচাই ছাড়াই সব নেয়।

সত্যিকারের run · Node 24, 127.0.0.1 · ~/bazarghar

$ curl -i -X POST http://127.0.0.1:3000/orders \
    -H 'content-type: application/json' \
    -d '{"phone":"abc","total":-500}'
HTTP/1.1 201 Created
content-type: application/json
…
{"ok":true}

# server-এর log
saved: {"phone":"abc","total":-500}

ফোন abc, মোট −৫০০ টাকা, আর server বলল 201 Created. কোনো form, কোনো React এই পথে আসেনি। তাই নিয়ম দুই জায়গায় থাকবে। Browser-এ যাচাই ব্যবহারকারীর সুবিধার জন্য, server-এ যাচাই নিরাপত্তার জন্য। দাম কখনো browser থেকে বিশ্বাস করা যায় না, server নিজে হিসাব করবে। পর্ব ২৭-এ Zod দিয়ে server-এর যাচাই, আর পর্ব ২১-এ একই schema দুই দিকে ব্যবহার দেখবেন। আক্রমণের দিকটি আছে Cybersecurity কোর্সের injection পর্বে. আর React-এর {value} লেখা escape করে, যা XSS থেকে সুরক্ষার একটি অংশ।

৯ · বাংলা ইনপুট — IME, অঙ্ক আর অক্ষর গোনা

ইংরেজি ধরে লেখা form বাংলায় প্রায়ই ভাঙে। একটি IME (Input Method Editor) হলো সেই সফটওয়্যার, যা কয়েকটি keystroke মিলিয়ে একটি অক্ষর বানায়। ফোনেটিক কিবোর্ডে k, a মিলে “কা” হয়। মাঝের এই সময়টিকে বলে composition. সব বাংলা কিবোর্ড একভাবে কাজ করে না, তাই নিচের ফল Edge-এর composition-এর একটি পরীক্ষা, সব কিবোর্ডের নিয়ম নয়।

সত্যিকারের run · Edge, DevTools Protocol দিয়ে composition · React 19.3.1

# সাধারণ controlled input
compositionstart
onChange "k" isComposing true
onChange "ka" isComposing true
onChange "কা" isComposing true
compositionend কা
চূড়ান্ত মান: কা

# handler-এ setV(value.toUpperCase()) করলে
compositionstart
onChange "k" isComposing true
compositionstart
onChange "Kka" isComposing true
compositionstart
onChange "KKAকা" isComposing true
compositionend কা
চূড়ান্ত মান: KKAকা

প্রথম অংশ দেখায়, composition চলার সময়েও onChange চলে, আর মান রাখলে কোনো সমস্যা হয় না। দ্বিতীয় অংশে handler মান বদলে দিয়েছে। প্রতিবার composition ভেঙে নতুন করে শুরু হয়েছে, আর ফল হয়েছে আবর্জনা। শিক্ষা পরিষ্কার: টাইপ করার সময় মান বদলাবেন না। যেমন আছে তেমন state-এ রাখুন, রূপান্তর করুন onBlur-এ বা submit-এ, যেমন normalizePhone করে।

function onKeyDown(e) {
  // IME-র Enter অক্ষর বাছাইয়ের জন্য, পাঠানোর জন্য নয়
  if (e.key === 'Enter' && !e.nativeEvent.isComposing) send();
}

এবার অঙ্ক আর অক্ষর। নিচের সব ফল Edge-এ চালিয়ে পাওয়া, en-US আর bn-BD দুই locale-এ একই।

পরীক্ষাফলকরণীয়
Number('১২'), parseInt('১২')NaNআগে ASCII অঙ্কে বদলান
/\d/.test('১')false\p{Nd} ও u flag অথবা [০-৯]
type="number"-এ ১২ লেখামান ''বাংলা অঙ্ক নিতে হলে inputMode="numeric" সহ text
type="number"-এ 12eমান '', badInput trueখালি মান মানেই খালি ঘর নয়
'কা'.length2 (চোখে এক অক্ষর)Intl.Segmenter দিয়ে গুনুন
'রহিম'.length4, grapheme 3maxLength-ও code unit গোনে
'য়' === 'য়' (দুই রকম “য়”)falseদুটিই NFC-তে এক হয়; server-এ normalize('NFC')
new Intl.NumberFormat('bn-BD') দিয়ে 1250১,২৫০দেখানোর সময় বাংলা অঙ্ক, হিসাবে সংখ্যা

একই নীতি মনে রাখুন: ভেতরে এক রূপ, বাইরে যেকোনো রূপ। ইনপুট নিন উদারভাবে, রাখার আগে স্বাভাবিক করুন, দেখানোর সময় বাংলায় সাজান। তারিখ, সংখ্যা আর locale-এর পুরো কাজ আসবে পর্ব ৫৯-এ (বাংলা সাইট).

আরও গভীরে: React input-এর value property-র ওপর একটি tracker বসায়, যা মনে রাখে শেষ জানা মান। Event এলে React তুলনা করে সত্যিই মান বদলেছে কি না, তাই একই বদলে দুবার onChange চলে না। Handler শেষ হলে state মানের সঙ্গে না মিললে React DOM-কে ফিরিয়ে দেয়, এই ধাপটিই controlled input-কে “controlled” রাখে। আর el.value = 'x' কোড দিয়ে লিখলে input event ছাড়ে না, তাই onChange-ও চলে না। কিছু বাইরের script এই কারণেই controlled input-এ কাজ করে না।

পরীক্ষা ও ইন্টারভিউতে:

  • Controlled আর uncontrolled-এর পার্থক্য? Controlled-এ মান React state-এ, uncontrolled-এ DOM-এ।
  • useFormStatus কেন false-ই দিচ্ছে? Form যে component render করে, তার ভেতরেই ডাকা হয়েছে। এটি form-এর ভেতরের child-এ ডাকতে হয়।
  • value আছে, onChange নেই, কী হয়? ঘরটি read-only হয়, development-এ warning আসে।
  • Form action-এর পরে ঘর খালি হলো কেন? সফল action-এর পরে React uncontrolled field reset করে। Error ফেরত দিলেও সেটি সফল।
  • Client-side validation থাকলে server-এ লাগে কি? লাগে। Request সরাসরি পাঠানো যায়।

১০ · AI-এর যুগে form

AI খুব দ্রুত form লিখে দেয়, আর দেখতেও সুন্দর হয়। সমস্যা হলো form-এর ভুলগুলো প্রথম চেষ্টায় চোখে পড়ে না। ইংরেজিতে সহজ ইনপুট দিয়ে পরীক্ষা করলে সব ঠিক লাগে। ধরা পড়ে বাংলা অঙ্কে, screen reader-এ, ধীর নেটওয়ার্কে দুবার ক্লিকে, আর curl-এ।

AI-এর ভুলকেন সমস্যাযাচাই
useFormState ব্যবহারReact 19-এ নাম বদলে useActionState; পুরোনোটি deprecated, 19.3.1-এ এখনো export আছে তাই “চলে”react.dev-এ নিজের version মিলিয়ে দেখা
FormEvent type@types/react 19.3-এ deprecatedEditor-এ কাটা দাগ দেখা; SubmitEvent ব্যবহার
ফোনে type="number"শুরুর 0 হারায়, বাংলা অঙ্ক নেয় না০১৭… লিখে দেখা
Placeholder-কে label বানানোScreen reader আর স্মৃতি দুটোই ক্ষতিগ্রস্তকিবোর্ড আর screen reader দিয়ে চলা
শুধু client validationRequest সরাসরি পাঠানো যায়curl দিয়ে ভুল ডেটা পাঠানো
অকারণে বড় libraryদুই field-এর form-এ নতুন dependencyআগে সাধারণ state দিয়ে ভাবা

AI-কে form লিখতে বললে শর্তগুলো আগেই লিখে দিন। নিচের prompt-এ version, accessibility, বাংলা আর নিরাপত্তা, চারটিই আছে।

React 19 আর TypeScript-এ একটি checkout ঠিকানা form লেখো। ক্ষেত্র: নাম, মোবাইল নম্বর, শহর (select), পেমেন্ট (radio), ঠিকানা মনে রাখা (checkbox). শর্ত: ১. controlled input আর একটি handler, শুধু React ছাড়া নতুন package নয়। ২. প্রতিটি input-এ label; error aria-describedby দিয়ে যুক্ত; প্রথম ভুল ঘরে focus. ৩. ফোনে বাংলা অঙ্ক, +88 আর ড্যাশ চলবে; টাইপ করার সময় মান বদলাবে না, submit-এ স্বাভাবিক করবে। ৪. পাঠানোর সময় button নিষ্ক্রিয়, ব্যর্থ হলে লেখা অক্ষত। ৫. deprecated API ব্যবহার করবে না। কোন API কোন version-এ এসেছে, react.dev-এর কোন পাতা দেখব, তা বলো। ৬. শেষে বলো server-এ কোন যাচাই আবার করতে হবে।

AI-এর লেখা form হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১AngularJS বা Vue-তে v-model এক শব্দেই input জুড়ে দেয়। React কেন দুই লাইন লিখিয়ে নেয়?

২০১০-এর দিকে AngularJS জনপ্রিয় করেছিল two-way binding: ng-model লিখলেই মডেল আর input একে অপরকে বদলায়। ছোট form-এ এটি জাদুর মতো। সমস্যা দেখা দেয় বড় অ্যাপে। একটি মান কয়েকটি জায়গা থেকে বদলাতে পারে, তাই “এই মান কে বদলাল” প্রশ্নের উত্তর খুঁজে পাওয়া কঠিন হয়। AngularJS-এর digest cycle একটি বদলের পরে আবার সব পাহারা যাচাই করত, তাই খরচও বাড়ত।

২০১৩-তে React উল্টো পথ নিল: ডেটা এক দিকে যায়। State থেকে UI, আর UI থেকে শুধু event. পরে Flux নকশা এই ভাবনাকে অ্যাপ জুড়ে ছড়াল। Controlled input এই নিয়মেরই ছোট রূপ। দুই লাইন বাড়তি লেখার দামে আপনি একটি নিশ্চিত জায়গা পান, যেখানে প্রতিটি বদল ঘটে।

  • লাভ: বদল আটকানো, রূপ বদলানো, লগ রাখা বা ফেরানো, সব onChange-এ এক জায়গায়। Test করাও সহজ।
  • খরচ: বেশি কোড, আর বড় component-এ প্রতিটি অক্ষরে render.

তবে বিষয়টি সরল নয়। Vue-র v-model আসলে compile হয়ে value আর input event-ই হয়, অর্থাৎ ভেতরে একই একমুখী প্রবাহ, শুধু লেখা ছোট। Vue আর Svelte-এর দর্শন হলো “সাধারণ কাজ ছোট হোক”, React-এর দর্শন হলো “যা ঘটছে তা স্পষ্ট থাকুক”। আধুনিক React-এও আপনি এক handler দিয়ে পুনরাবৃত্তি কমাতে পারেন। পর্ব ৬৬-এ Vue-র তুলনা আসবে।

মূল উপলব্ধি: বাড়তি লাইনটি অদক্ষতা নয়, নকশার সিদ্ধান্ত। কম লেখার সুবিধা আর ডেটার পথ স্পষ্ট রাখার সুবিধা, এই দুইয়ের মধ্যে একটি বেছে নিতে হয়, আর উত্তর নির্ভর করে অ্যাপের আকারের ওপর।

প্র ০২React দশ বছর ধরে controlled input শিখিয়ে React 19-এ আবার FormData আর <form action>-এ ফিরল কেন? এটি কি উল্টো হাঁটা?

ওয়েবের শুরুতে form-ই ছিল সব। PHP-র যুগে <form method="post"> server-এ যেত, server নতুন পাতা ফেরত দিত। JavaScript ছাড়াই সব চলত। তারপর SPA যুগে যুক্তি browser-এ চলে এলো, আর প্রতিটি দল নিজের মতো করে isLoading, error, try/catch, ডবল ক্লিক ঠেকানো লিখতে লাগল। একই ৩০ লাইন হাজারো প্রজেক্টে।

এরপর দুটি জিনিস মানুষকে আবার ভাবাল। প্রথম, ধীর নেটওয়ার্ক আর দুর্বল ফোনে JavaScript লোড হওয়ার আগে form অকেজো থাকে। আমাদের দেশে মাঝারি দামের ফোনে এটি বাস্তব সমস্যা। দ্বিতীয়, Remix-এর মতো framework দেখাল, HTML form-কেই ভিত্তি করে তার ওপর React বসালে কোড কমে আর পাতা JavaScript ছাড়াও কাজ করতে পারে। এই ধারণাকে বলে progressive enhancement.

React 19-এর উত্তর তাই পিছিয়ে যাওয়া নয়, বরং দুই জগৎ জোড়া দেওয়া। Client-এ <form action={fn}> একটি function চালায়, pending সামলায়, আর এরই ওপর পর্ব ৫৪-এর Server Action দাঁড়ায়, যেখানে function চলে server-এ। useActionState-এর তৃতীয় argument permalink ঠিক এই ক্ষেত্রেই লাগে: JavaScript লোড হওয়ার আগে submit হলে browser কোন URL-এ যাবে।

তবে সব কিছু action-এ নেওয়া ঠিক নয়। ফিল্ড-ধরে সঙ্গে সঙ্গে যাচাই, এক ঘরের মানে আরেক ঘর বদলানো, লাইভ দাম, এসব controlled-এই সহজ। সম্প্রদায়ে এ নিয়ে এখনো মতভেদ আছে। অনেকে React Hook Form-এর মতো library-তেই থাকেন, অনেকে সম্পূর্ণ action-এ সরে গেছেন।

মূল উপলব্ধি: প্রযুক্তি প্রায়ই চক্রে ফেরে, কিন্তু একই জায়গায় নয়। ফিরে আসা ধারণাটি আগের অভিজ্ঞতা সঙ্গে নিয়ে আসে। Web platform-এর ক্ষমতা জানা তাই framework জানার মতোই জরুরি।

প্র ০৩Form-এ শুধু তিনটি ঘর আছে। তাহলে কেউ চতুর্থ একটি ঘর পাঠালে বিপদ কোথায়? GitHub-এর ২০১২-র ঘটনা কী শেখায়?

অনেকে ভাবেন, form যা পাঠায়, server শুধু তাই পায়। আসলে server পায় যে কোনো HTTP request. আপনার React কোড সেখানে উপস্থিতই নয়। এই পর্বের curl-এর ফলাফলে সেটিই দেখা গেছে: ফোন abc, মোট ঋণাত্মক, তবু 201.

বিখ্যাত উদাহরণ ২০১২-র মার্চের। প্রকাশিত প্রতিবেদন অনুযায়ী, Egor Homakov নামের একজন ডেভেলপার Ruby on Rails-এর mass assignment সমস্যা নিয়ে সতর্ক করছিলেন। Rails-এ তখন form-এর সব parameter একসাথে model-এ বসিয়ে দেওয়া খুব সহজ ছিল। তিনি GitHub-এর public key আপডেট করার form-এ বাড়তি একটি parameter পাঠিয়ে key-টির মালিক বদলে দেন, এবং Rails-এর repository-তে একটি নিরীহ commit করে নিজের কথা প্রমাণ করেন। GitHub দ্রুত ঠিক করে। পরে Rails-ও ডিফল্ট নিরাপদ পথে সরে যায়, যেখানে অনুমোদিত ঘরের তালিকা লিখতে হয়।

আমাদের checkout-এ একই ঝুঁকি হাতের কাছেই। ধরুন server লেখে db.order.create({ data: req.body }). কেউ পাঠালেন {"total": 1, "status": "paid"}. Form-এ এই ঘর নেই, কিন্তু server বিশ্বাস করে বসালে পণ্য প্রায় বিনামূল্যে।

  • নিয়ম ১: Server-এ অনুমোদিত ঘরের তালিকা (schema) রাখুন, বাকি বাদ দিন।
  • নিয়ম ২: দাম, ছাড়, অবস্থা, মালিক কখনো client থেকে নয়। Server নিজে বের করে।
  • নিয়ম ৩: Browser-এর নিয়ম আর server-এর নিয়ম এক উৎস থেকে নিন, যাতে দুটি আলাদা হয়ে না যায়। পর্ব ২১ আর ২৭ এটিই দেখায়।

মূল উপলব্ধি: UI একটি পরামর্শ, চুক্তি নয়। Form কী দেখায় তা দিয়ে নিরাপত্তা ঠিক হয় না, server কী মেনে নেয় তা দিয়ে হয়।

প্র ০৪বাংলা লেখা তো অনেক বছর ধরে কম্পিউটারে চলছে। তাহলে আজও form-এ এত সমস্যা কেন?

ইতিহাস এখানে অনেকটা ব্যাখ্যা দেয়। আশির দশকের শেষে মোস্তফা জব্বারের বিজয় কিবোর্ড বাংলা টাইপিংকে ব্যাপক করে, কিন্তু সে যুগের বেশিরভাগ বাংলা ফন্ট ছিল ASCII-তে বসানো। অর্থাৎ ফাইলে আসলে ইংরেজি অক্ষরের কোড থাকত, ফন্ট সেটিকে বাংলা আঁকত। এমন লেখা form-এ পেস্ট করলে আজও অর্থহীন ইংরেজি অক্ষর দেখা যায়। পরে Unicode-এ বাংলার নিজস্ব স্থান এলো, আর ২০০০-এর দশকে মেহদী হাসান খানের অভ্র ফোনেটিক টাইপিং বিনামূল্যে সবার হাতে দিল।

Unicode সমস্যা কমালেও কয়েকটি জটিলতা রেখে গেছে। একটি চোখে দেখা অক্ষর অনেক সময় কয়েকটি code point. “কা”-তে দুটি, যুক্তাক্ষরে আরও বেশি। তাই .length, maxLength আর ইংরেজি ধরে লেখা বাক্স ভুল গোনে। “য়”-এর মতো কিছু অক্ষর দুই রকমে লেখা যায়। এই পর্বের পরীক্ষায় 'য়' === 'য়' হয়েছে false, যদিও চোখে একই। ফলে একই নাম database-এ দুই রকম থাকতে পারে, আর সার্চে মেলে না।

এরপর IME. প্রতিটি কিবোর্ড সফটওয়্যার একটু আলাদা আচরণ করে। কোনোটি composition event পাঠায়, কোনোটি অক্ষর মুছে আবার বসায়। Windows-এ, Android-এ, আর browser ভেদে ফল আলাদা হতে পারে। তাই “আমার মেশিনে ঠিক আছে” এখানে খুব দুর্বল প্রমাণ।

  • ডেভেলপারের করণীয়: টাইপের সময় মান বদলাবেন না; server-এ NFC করুন; অঙ্ক ASCII-তে রাখুন; অন্তত একটি ফোন আর একটি ডেস্কটপ বাংলা কিবোর্ডে পরীক্ষা করুন।
  • পণ্যের সিদ্ধান্ত: নাম বাংলা বা ইংরেজি দুটিই নিন, নাম-যাচাইয়ে অতিরিক্ত কড়া regex লিখবেন না।

মূল উপলব্ধি: ভাষা হলো ডেটা। ইংরেজি ধরে তৈরি অনুমান, যেমন “এক অক্ষর এক ঘর”, বাংলায় ভাঙে। নিজের ভাষার জন্য ঠিক সফটওয়্যার বানানো একজন বাংলাদেশি ডেভেলপারের একটি বাস্তব সুবিধা।

প্র ০৫Error কখন দেখাবেন: প্রতিটি অক্ষরে, ঘর ছাড়ার সময়, না submit-এ? আর ভুল থাকলে button নিষ্ক্রিয় রাখা কি ভালো?

এটি স্বাদের প্রশ্ন মনে হলেও গবেষণা আছে। ২০০৯ সালে Luke Wroblewski A List Apart-এ “Inline Validation in Web Forms” নামে একটি পরীক্ষার ফল লেখেন। ছোট দলে করা পরীক্ষায় inline যাচাই পাওয়া ব্যবহারকারীরা দ্রুত শেষ করেন, ভুল কম করেন, সন্তুষ্টও বেশি হন। তিনি এটিও লেখেন, নিজের নাম ধরনের সহজ প্রশ্নে সবুজ টিকের তেমন মূল্য নেই, লাভ বেশি যেখানে উত্তর নিয়ে মানুষ অনিশ্চিত, যেমন password.

প্রতিটি অক্ষরে error দেখানোর ক্ষতি পরিষ্কার। 0 লিখতেই “নম্বর ভুল” দেখানো যেন দর্জির প্রথম ফিতা ধরতেই বলা, “মাপ ঠিক হয়নি।” তাই একটি প্রচলিত মধ্যপথ আছে:

  • প্রথমবার: ঘর ছাড়ার সময় (onBlur) বা submit-এ যাচাই।
  • Error দেখানোর পরে: টাইপের সাথে সাথে যাচাই, যাতে ঠিক হওয়া মাত্র error চলে যায়।
  • Submit-এ: সব ঘর একসাথে যাচাই, প্রথম ভুল ঘরে focus.

“ভুল থাকলে submit button disabled” নকশাটি দেখতে গোছানো, কিন্তু অনেক accessibility বিশেষজ্ঞ এর বিরোধী। নিষ্ক্রিয় button কেন নিষ্ক্রিয়, তা ব্যবহারকারী জানেন না; কিবোর্ডে সেখানে focus-ও পৌঁছায় না; তিনি শুধু দেখেন একটি ধূসর বোতাম। Button চালু রেখে চাপলে কী ভুল আছে দেখানো সাধারণত বেশি সহায়ক। এ জন্যই এই পর্বে disabled শুধু pending-এর জন্য। সম্প্রদায়ে দ্বিমত আছে: খুব ছোট form-এ, যেমন OTP-তে, অনেক দল disabled পছন্দ করে।

WCAG নির্দেশিকা স্পষ্টভাবে চায়, ভুল লেখায় জানাতে হবে, শুধু রঙে নয়, আর ঘরের label থাকতে হবে। এটি কোনো সাজের বিষয় নয়, সাইট ব্যবহারের অধিকার।

মূল উপলব্ধি: Validation শুধু যুক্তির কাজ নয়, কথোপকথনের কাজ। ঠিক সময়ে, ঠিক জায়গায়, ভদ্র ভাষায় বলা একটি error একটি অর্ডার বাঁচাতে পারে।

অনুশীলন

হিসাব, command, আউটপুট অনুমান আর নকশা সমালোচনা, চার রকম কাজ।

  1. হিসাব: একটি form-এ ২৫টি field একই component-এর state-এ। প্রতিটি field render হতে ১.৫ ms লাগে (ধরে নেওয়া), ব্যবহারকারী সেকেন্ডে ৬টি অক্ষর লেখেন। প্রতি keystroke-এ কত ms? প্রতি সেকেন্ডে main thread-এর কত শতাংশ? State প্রতিটি field-এ নামালে কত?

    প্রতি keystroke: ২৫ × ১.৫ = ৩৭.৫ ms, যা ১৬.৭ ms-এর frame বাজেটের দ্বিগুণের বেশি। প্রতি সেকেন্ডে: ৩৭.৫ × ৬ = ২২৫ ms, অর্থাৎ ২২.৫%. State নামালে প্রতি keystroke-এ শুধু একটি field: ১.৫ × ৬ = ৯ ms প্রতি সেকেন্ডে, অর্থাৎ ০.৯%. লাভ ২৫ গুণ, কারণ render হওয়া field-এর সংখ্যা ২৫ থেকে ১ হয়েছে।

  2. আউটপুট অনুমান করুন: একটি form-এ আছে <input name="city" defaultValue="ঢাকা">, চিহ্ন না দেওয়া <input type="checkbox" name="gift">, আর <input name="coupon" defaultValue="EID" disabled>. Submit-এ JSON.stringify(Object.fromEntries(new FormData(form))) কী ছাপবে? তারপর '3' + 1 আর '3' * 2 কী হবে?

    ছাপবে {"city":"ঢাকা"}. চিহ্ন না দেওয়া checkbox আর disabled ঘর FormData-তে যায় না। কোনো ঘর দেখাতে চান কিন্তু বদলাতে দিতে না চাইলে readOnly ব্যবহার করুন, সেটি পাঠানো হয়। '3' + 1 হয় '31', '3' * 2 হয় 6.

  3. Command: node:http দিয়ে একটি server বানান, যা সব POST মেনে 201 দেয় (127.0.0.1-এ)। তারপর curl দিয়ে {"phone":"abc"} পাঠান। এবার server-এ isBdMobile যোগ করে ভুল হলে 400 দিন। দুই ক্ষেত্রে curl -i-এর প্রথম লাইন কী হবে?

    প্রথমে HTTP/1.1 201 Created, যোগ করার পরে HTTP/1.1 400 Bad Request. Server body string হিসেবে পায়, তাই আগে JSON.parse লাগে, আর ভাঙা JSON-এও 400 দিতে হয়। লক্ষ করুন, browser-এর কোনো কোড এখানে বদলাতে হয়নি। এটাই দুই পাশের যাচাইয়ের অর্থ। পূর্ণ পথ পর্ব ২৭-এ।

  4. নকশা সমালোচনা: AI এই form দিল: <input type="number" placeholder="ফোন" value={phone} onChange={e => setPhone(Number(e.target.value))} />, পাশে <button onClick={send}>পাঠান</button>. অন্তত পাঁচটি সমস্যা বলুন, আর ঠিক করার পথ দিন।
    • শুরুর 0 হারায়: Number আর type="number" দুটোই ভুল। type="tel" আর string রাখুন।
    • বাংলা অঙ্ক নেয় না: Edge-এ type="number" ১২ নেয়নি। Text ধরনের ঘরে নিয়ে normalizePhone চালান।
    • Label নেই: Placeholder টাইপ শুরুতেই চলে যায়। <label> দিন।
    • Form নেই: Enter কাজ করে না। <form onSubmit> বা action ব্যবহার করুন।
    • যাচাই ও error নেই: aria-describedby সহ error দেখান, আর server-এও যাচাই রাখুন।
    • Pending নেই: দুবার ক্লিকে দুটি request. পাঠানোর সময় button নিষ্ক্রিয় রাখুন।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমControlled input-এ একটি অক্ষর টাইপ করলে যা ঘটে, সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Browser DOM-এ অক্ষরটি বসায়
  2. Browser input event ছাড়ে, React onChange চালায়
  3. Handler setName(e.target.value) ডাকে
  4. Component নতুন state নিয়ে আবার render হয়
  5. Commit-এ React দেখে DOM আর নতুন মান মিলেছে

<input type="number">-এ ব্যবহারকারী 5 লিখলে React-এর e.target.value-এর type কী?

  • number
  • string
  • bigint
  • undefined

React component-এ <input value={v} onChange={e => console.log(e.target.value)} /> লেখা। v হলো 'আম', আর setV কখনো ডাকা হয় না। ব্যবহারকারী x টাইপ করলে কী হবে?

  • React error ছুড়ে পাতা বন্ধ করবে
  • পর্দায় আমx থেকে যাবে
  • Console-এ আমx ছাপা হবে, কিন্তু পর্দায় আম-ই থাকবে
  • onChange চলবেই না

React 19-এ একটি component useFormStatus() ডাকে, আর সেই component-ই নিজের return-এ <form action={save}> render করে। পাঠানোর সময় pending কী হবে?

  • সব সময় false থাকবে
  • পাঠানোর সময় true হবে
  • Error ছুড়বে
  • undefined হবে

useActionState(fn, initial)-কে <form action>-এ দিলে fn কোন argument পায়?

  • শুধু formData
  • (formData, prevState)
  • (event, formData)
  • (prevState, formData)

JavaScript-এ ব্যবহারকারী বাংলা অঙ্কে ১২ লিখেছেন। Number('১২')-এর ফল কী?

  • 12
  • NaN
  • 0
  • '১২'

একটি checkout form-এ browser-এ সব যাচাই আছে, ভুল থাকলে button-ও কাজ করে না। Server-এ আবার যাচাই কেন দরকার?

  • Server-এর যাচাই browser-এর চেয়ে দ্রুত
  • React কখনো কখনো validation বাদ দেয়
  • যে কেউ form ছাড়াই সরাসরি server-এ request পাঠাতে পারেন
  • HTTP পথে ডেটা নিজে থেকে বদলে যায়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. একটি input-এর মালিক একজন: value + onChange মানে React, defaultValue মানে DOM.
  2. ফর্মের মান string. গণনায় Number(), আর ফোনে কখনো type="number" নয়।
  3. Error লেখায় দেখান, aria-describedby দিয়ে জুড়ুন, প্রথম ভুল ঘরে focus দিন।
  4. React 19-এ useActionState আর useFormStatus pending সামলায়; ব্যর্থতায় লেখা রাখতে defaultValue দিন।
  5. টাইপের সময় মান বদলাবেন না, আর server-এ যাচাই সব সময়।

পরের পর্বে: Form এখন কাজ করে, কিন্তু ডেটা বাইরে থেকে আনতে বা পাতার বাইরের জগতের সঙ্গে কথা বলতে হলে কী করবেন? useEffect, cleanup আর dependency-র গল্প।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান