এই পর্বে যা শিখবেন
- বড় form কেন ধীর ও এলোমেলো হয়, আর সেটি মাপবেন কীভাবে
- React Hook Form:
register,handleSubmit,formState,Controller,useFieldArray - Zod: schema,
safeParse, error-এর path,z.infer,@hookform/resolvers - এক schema client ও server-এ, async যাচাই, আর server-এর ভুল সঠিক field-এ ফেরানো
- “বাজারঘর”-এর তিন ধাপের checkout, accessible error আর file input
১ · বড় form কেন ধীর আর এলোমেলো হয়
একটি ব্যাংকের হিসাব খোলার ফরম ভাবুন। চার পাতা, চল্লিশটি ঘর, কিছু ঘর অন্য ঘরের ওপর নির্ভর করে, আর শেষে একজন কর্মকর্তা সব মিলিয়ে দেখেন। এখন ভাবুন, প্রতিটি অক্ষর লেখার পর কর্মকর্তা পুরো চার পাতা আবার পড়ছেন। কাজ হবে, কিন্তু লাইন আর এগোবে না।
পর্ব ১২-এ (Form — controlled input, submit আর ব্যবহারকারীর ইনপুট) আপনি controlled input, FormData আর accessible error শিখেছেন। ছোট form-এ সেটিই যথেষ্ট, আর এখনো সেটিই প্রথম পছন্দ। সমস্যা শুরু হয় বাজারঘরের checkout-এর মতো form-এ, যেখানে তিনটি ধাপ, পণ্যের সারি যোগ-বাদ, শর্তসাপেক্ষ ঘর, server-এর উত্তর, সব একসাথে।
তখন হাতে লেখা কোডে তিন রকম ঝামেলা জমে:
- গতি: সব মান এক state-এ থাকলে প্রতিটি অক্ষরে পুরো form আবার render হয়।
- হিসাব: কোন ঘর ছোঁয়া হয়েছে, কোনটি বদলেছে, কোনটিতে ভুল, submit চলছে কি না, এগুলো আলাদা state হয়ে ছড়িয়ে পড়ে।
- নিয়মের কপি: ফোনের একই regex থাকে form-এ, API-তে, আবার TypeScript type-এ। একটি বদলালে বাকি দুটি পুরোনো থেকে যায়।
গতির কথাটি অনুমানে নয়, মেপে দেখা যাক। নিচের পাতায় ২০ ঘরের দুটি form. ওপরের ঘরে মান state-এ, নিচের ঘরে মান DOM-এ, আর React পায় শুধু ref. দুটির প্রথম ঘরে একই সংখ্যক অক্ষর লিখে “গণনা দেখুন” চাপুন।
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const names = Array.from({ length: 20 }, (_, i) => 'f' + (i + 1));
const count = { controlled: 0, uncontrolled: 0 };
function Box({ kind, ...rest }) {
count[kind]++;
return <input size="2" aria-label={kind + ' ' + rest.name} {...rest} />;
}
function Controlled() {
const [v, setV] = useState({});
return <form><h4>Controlled</h4>{names.map(n => <Box key={n} kind="controlled" name={n}
value={v[n] ?? ''} onChange={e => setV(o => ({ ...o, [n]: e.target.value }))} />)}</form>;
}
function Uncontrolled() {
const refs = useRef({});
const show = () => console.log('f1 =', refs.current.f1.value, JSON.stringify(count));
return <form><h4>Uncontrolled</h4>{names.map(n => <Box key={n} kind="uncontrolled" name={n}
ref={el => { refs.current[n] = el; }} defaultValue="" />)}
<button type="button" onClick={show}>গণনা দেখুন</button></form>;
}
function App() {
useEffect(() => console.log('শুরুতে', JSON.stringify(count)), []);
return <><Controlled /><Uncontrolled /></>;
}
createRoot(document.getElementById('root')).render(<App />);
</script>চালানো মাত্র আসে শুরুতে {"controlled":20,"uncontrolled":20}. Edge-এ দুটি form-এর প্রথম ঘরে 01712345678 টাইপ করে বোতাম চাপলে এলো f1 = 01712345678 {"controlled":240,"uncontrolled":20}. Controlled-এ ১১টি অক্ষর × ২০টি ঘর = ২২০টি বাড়তি render. Uncontrolled-এ ০টি, অথচ মান ঠিকই পাওয়া গেছে।
এই ভাবনাটিকেই একটি পূর্ণ library বানিয়েছে React Hook Form. ঘরের মান DOM-এ থাকে, library ref দিয়ে সব ঘরের হিসাব রাখে, আর আপনি শুধু দরকারি অংশে সাড়া দেন। সত্যিকারের Vite production build-এ Edge দিয়ে একই পরীক্ষা করা হলো: ২০ ঘরের form-এর সপ্তম ঘরে ১১টি keystroke.
সত্যিকারের run · লেখার সময়ে React 19.3.1, react-hook-form 7.91.0, Vite 8.3.5, Edge · একটি স্থানীয় run
| ধরন | Mount-এ form render | টাইপে form render | টাইপে field render |
|---|---|---|---|
| Controlled, এক state | ১ | ১১ | ২২০ |
| Controlled + memo + useCallback | ১ | ১১ | ১১ |
| React Hook Form | ২ | ০ | ০ |
RHF, render-এ isDirty পড়া | ২ | ১ | ২০ |
RHF, render-এ watch() | ২ | ১১ | ২২০ |
শেষ সারিটি সবচেয়ে শিক্ষণীয়। Library বসালেই গতি আসে না। ওপরের স্তরে watch() ডাকলে আপনি controlled form-এর খরচেই ফিরে গেলেন। Mount-এ RHF একবার বাড়তি render করায়, এটি এককালীন খরচ, প্রতি keystroke-এর নয়।
Controlled: ১২০ × ২০ = ২,৪০০ render, প্রতি অক্ষরে ২০ ms
৬০ Hz-এর frame বাজেট ১৬.৭ ms, তাই বাজেট ছাড়িয়ে যায়
Memo: ১২০ × ১ = ১২০ render · RHF: টাইপে ০
Controlled input খারাপ নয়। memo দিয়েও field render ১১-তে নামল। ১০টি ঘরের সাধারণ form-এ এই পার্থক্য চোখে পড়বে না। React Hook Form নেওয়ার ভালো কারণ শুধু গতি নয়: touched, dirty, error, focus আর সারি যোগ-বাদের হিসাব হাতে লিখতে হয় না। মাপা নিয়ে বিস্তারিত আসবে পর্ব ২৩-এ (React performance).
২ · React Hook Form — register, handleSubmit, formState
React Hook Form-কে দর্জির মাপের খাতার মতো ভাবুন। দর্জি প্রতিবার ফিতা ধরেই পুরো খাতা নতুন করে লেখেন না। প্রতিটি মাপ নিজের ঘরে থাকে, আর কাপড় কাটার সময় তিনি একবার সব মিলিয়ে দেখেন। register মানে ঘরটি খাতায় তোলা, আর handleSubmit মানে কাটার আগের শেষ যাচাই।
import { useForm } from 'react-hook-form';
export function AddressForm() {
const {
register, handleSubmit,
formState: { errors, isSubmitting },
} = useForm({
defaultValues: { name: '', phone: '' },
mode: 'onTouched', // প্রথমবার ঘর ছাড়ার পর যাচাই
});
return (
<form noValidate onSubmit={handleSubmit(data => console.log(data))}>
<input {...register('name', { required: 'নাম লিখুন' })} />
<p>{errors.name?.message}</p>
<input {...register('phone')} />
<button disabled={isSubmitting}>অর্ডার</button>
</form>
);
}প্রতিটি অংশের কাজ আলাদা:
register('name')চারটি জিনিস ফেরত দেয়:name,ref,onChange,onBlur. তাই{...register()}স্প্রেড করলে ঘরটি জোড়া লেগে যায়। এখানেvalueনেই, অর্থাৎ ঘরটি uncontrolled.handleSubmit(fn)নিজেই একটি submit handler বানায়। সেটিpreventDefaultকরে, যাচাই চালায়, সব ঠিক থাকলে তবেই আপনারfnডাকে। দ্বিতীয় argument দিলে ব্যর্থতার সময় সেটি চলে।- formState
errors,isDirty,isSubmitting,isValid,touchedFields,submitCountরাখে। মান দরকার হলে render-এর সময়েই পড়ুন, কারণ library দেখে আপনি কোনটি পড়েছেন, আর শুধু সেটির বদলে render করায়। ওপরের ছকেisDirty-এর সারিতে render মাত্র ১ বার, প্রথম অক্ষরে, কারণfalseথেকেtrueএকবারই বদলায়। defaultValuesপ্রতিটি ঘরের জন্য দিন, এমনকি''.isDirtyএই মানের সঙ্গেই তুলনা করে।
কখন যাচাই হবে: mode
mode | প্রথম যাচাই কখন | কখন মানায় |
|---|---|---|
onSubmit (ডিফল্ট) | Submit-এ | কম render, ছোট form |
onBlur | ঘর ছাড়লে | ঘর ছাড়ার পর জানানো |
onTouched | প্রথম blur, পরে প্রতি বদলে | Checkout-এর মতো form, ভদ্র আচরণ |
onChange | প্রতি অক্ষরে | লাইভ বার্তা দরকার হলে, render বেশি |
Submit-এর পরে ভুল ঘরগুলো আলাদা নিয়মে চলে: reValidateMode, যার ডিফল্ট onChange. অর্থাৎ একবার ভুল দেখালে ঠিক হওয়ামাত্র বার্তা সরে যায়।
watch() যেখানে ডাকেন, সেখানেই render হয়। একটি ঘরের ওপর নির্ভরশীল ছোট অংশ থাকলে সেটিকে আলাদা component বানিয়ে useWatch ব্যবহার করুন, তাহলে শুধু সেই অংশ render হবে। Submit-এর সময় মান দরকার হলে getValues(), যা render করায় না।
৩ · Zod — নিয়ম একটি object-এ
এতক্ষণ required-এর মতো নিয়ম JSX-এর ভেতরে ছিল। তাতে নিয়ম আটকে যায় একটি component-এ, server তা ব্যবহার করতে পারে না। Zod নিয়মগুলোকে UI থেকে আলাদা করে একটি সাধারণ JavaScript object-এ রাখে, যাকে বলে schema. ডাকঘরের বাছাইয়ের ছকের মতো: চিঠি যে-ই আনুক, ছক একটাই। Zod আগে পরিচিত হয়েছে পর্ব ০৬-এ, এখানে form-এর দিক থেকে দেখা যাক।
Live বাক্সে npm package আসে না, তাই আগে Zod-এর ভাবনাটি ছোট আকারে নিজে লিখে দেখি। এটি শেখার মডেল, আসল Zod নয়। খেয়াল করুন তিনটি জিনিস: ভুল পেলে থেমে যায় না, প্রতিটি ভুলের সঙ্গে path থাকে, আর ফল হিসেবে আসে পরিষ্কার নতুন object.
const str = (rules = []) => ({
min: (n, msg) => str([...rules, v => v.trim().length >= n || msg]),
regex: (r, msg) => str([...rules, v => r.test(v) || msg]),
run(v, path, issues) {
if (typeof v !== 'string') return void issues.push({ path, message: 'লেখা দরকার' });
rules.map(rule => rule(v)).filter(r => r !== true)
.forEach(message => issues.push({ path, message }));
return v.trim();
},
});
const int = (lo, hi, msg) => ({ run(v, path, issues) {
if (!Number.isInteger(v) || v < lo || v > hi) issues.push({ path, message: msg });
return v; } });
const arr = (inner) => ({ run: (v, path, issues) => Array.isArray(v)
? v.map((x, i) => inner.run(x, [...path, i], issues))
: void issues.push({ path, message: 'তালিকা দরকার' }) });
const obj = (shape) => ({
run(v, path, issues) {
const out = {}; // অজানা key এখানেই বাদ পড়ে
for (const k in shape) out[k] = shape[k].run(Object(v)[k], [...path, k], issues);
return out;
},
safeParse(v) { const issues = [], data = this.run(v, [], issues);
return issues.length ? { success: false, issues } : { success: true, data };
},
});
const checkout = obj({
name: str().min(2, 'পুরো নাম লিখুন'),
phone: str().regex(/^01[3-9]\d{8}$/, '১১ সংখ্যার মোবাইল নম্বর দিন'),
items: arr(obj({ sku: str().min(1, 'পণ্য বাছুন'), qty: int(1, 10, '১ থেকে ১০') })),
});
const r = checkout.safeParse({ name: 'র', phone: '0171', items: [{ sku: 'dal', qty: 0 }] });
r.issues.forEach(i => console.log(i.path.join('.'), '→', i.message));
const ok = { name: ' রহিমা ', phone: '01712345678', isAdmin: true, items: [{ sku: 'dal', qty: 2 }] };
console.log(JSON.stringify(checkout.safeParse(ok)));আউটপুটের প্রথম তিন লাইন name → পুরো নাম লিখুন, phone → ১১ সংখ্যার মোবাইল নম্বর দিন, items.0.qty → ১ থেকে ১০. প্রথম ভুলে থেমে গেলে ক্রেতা একটি করে ভুল ঠিক করতেন, তাই সব ভুল একসাথে আসে। শেষ লাইনে {"success":true,"data":{"name":"রহিমা","phone":"01712345678","items":[{"sku":"dal","qty":2}]}}. নামের দুই পাশের ফাঁকা গেছে, আর isAdmin নেই, কারণ schema শুধু চেনা key নতুন object-এ তোলে।
এবার আসল Zod. বাজারঘরের schema থাকে একটি আলাদা ফাইলে, React ছাড়াই। Vite project-এ ইনস্টল করার কমান্ড:
npm create vite@latest bazarghar -- --template react-ts
cd bazarghar
npm install react-hook-form zod @hookform/resolversimport { z } from 'zod';
const item = z.object({
sku: z.string().min(1, 'পণ্য বাছুন'),
qty: z.number().int('পূর্ণ সংখ্যা দিন')
.min(1, 'অন্তত ১টি').max(10, 'একবারে সর্বোচ্চ ১০টি'),
});
export const checkoutSchema = z.object({
name: z.string().trim().min(2, 'পুরো নাম লিখুন'),
phone: z.string().regex(/^01[3-9]\d{8}$/, '১১ সংখ্যার মোবাইল নম্বর দিন'),
city: z.enum(['dhaka', 'rajshahi', 'khulna', 'sylhet'], { error: 'শহর বাছুন' }),
address: z.string().trim().min(10, 'ঠিকানা আরও বিস্তারিত লিখুন'),
items: z.array(item).min(1, 'অন্তত একটি পণ্য রাখুন').max(20, 'সর্বোচ্চ ২০টি লাইন'),
pay: z.enum(['cod', 'bkash'], { error: 'পেমেন্ট বাছুন' }),
trxId: z.string().trim().optional(),
}).refine((d) => d.pay !== 'bkash' || (d.trxId ?? '').length >= 8, {
path: ['trxId'],
error: 'Transaction ID দিন',
});
export type Checkout = z.infer<typeof checkoutSchema>;.parse() ভুল পেলে exception ছোড়ে। Form আর API-তে ভুল কোনো ব্যতিক্রম নয়, স্বাভাবিক ঘটনা, তাই safeParse ব্যবহার করুন। এটি একটি object ফেরত দেয়: { success: true, data } অথবা { success: false, error }. refine নিজের নিয়ম যোগ করে, আর path বলে দেয় ভুলটি কোন ঘরে দেখাবে।
সত্যিকারের run · zod 4.6.7, tsx, Node 24 · ~/bazarghar · ছাঁটা আউটপুট
$ npx tsx zod-demo.mts
…
B success: false
issue count: 7
name too_small পুরো নাম লিখুন
phone invalid_format ১১ সংখ্যার মোবাইল নম্বর দিন
city invalid_value শহর বাছুন
address too_small ঠিকানা আরও বিস্তারিত লিখুন
items.0.qty too_small অন্তত ১টি
items.1.sku too_small পণ্য বাছুন
items.1.qty invalid_type পূর্ণ সংখ্যা দিন
--- flatten
{"formErrors":[],"fieldErrors":{…,"items":["অন্তত ১টি","পণ্য বাছুন","পূর্ণ সংখ্যা দিন"]}}
শেষ লাইনটি লক্ষ করুন। z.flattenError সহজ, কিন্তু নেস্টেড ভুলগুলো এক items-এ গুলিয়ে ফেলে। কোন সারির ভুল, তা হারিয়ে গেল। তাই সারিওয়ালা form-এ error.issues-এর path ব্যবহার করুন: i.path.join('.') দেয় items.1.qty, যা React Hook Form-এর ঘরের নামের সঙ্গে হুবহু মেলে। এই মিলটিই পরের সব কিছুর ভিত্তি।
Type আলাদা করে লিখতে হয় না
z.infer schema থেকে TypeScript type বের করে, এটি পর্ব ০৬-এর type inference-এরই ব্যবহার। ফলে নিয়ম আর type আলাদা হতে পারে না। ভুল মান দিলে build-এর আগেই ধরা পড়ে:
সত্যিকারের run · TypeScript 7.0.2
$ npx tsc -b
src/type-demo.ts(4,18): error TS2322: Type 'number' is not assignable to type 'string'.
src/type-demo.ts(4,37): error TS2322: Type '"cumilla"' is not assignable to type '"dhaka" | "khulna" | "rajshahi" | "sylhet"'.
একটি ফাঁদ, যা সত্যিকারের run-এ ধরা পড়ল। ওপরের ভুল ডেটায় pay ছিল bkash, Transaction ID ছিল না, তবু ৭টি ভুলের তালিকায় trxId নেই। Object-এর refine তখনই চলে, যখন ভেতরের field-এর ভুল বাকি যাচাই আটকে দেয়নি। নাম ছোট হলে ফল এলো [ 'name', 'trxId' ], কিন্তু শহর ভুল হলে শুধু [ 'city' ]. অর্থাৎ ক্রেতা শহর ঠিক করার পর নতুন একটি ভুল দেখতে পারেন। পরীক্ষা করে আচরণ জেনে রাখুন।
৪ · Resolver — দুজনকে জোড়া, আর একটি submit-এর পথ
React Hook Form Zod-কে চেনে না, Zod-ও form চেনে না। মাঝখানে থাকে resolver, যেন দুই ভাষার মাঝে একজন দোভাষী। @hookform/resolvers package-এর zodResolver schema-র ভুলগুলোকে RHF-এর errors আকারে সাজায়। Resolver দিলে register-এর ভেতরের required ধরনের নিয়ম আর চলে না, সব নিয়ম schema-তেই।
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { checkoutSchema, type Checkout } from '../shared/checkout';
export const useCheckout = () => useForm<Checkout>({
resolver: zodResolver(checkoutSchema),
mode: 'onTouched',
defaultValues: {
name: '', phone: '', address: '', pay: 'cod', trxId: '',
items: [{ sku: 'dal-1kg', qty: 1 }],
},
});এবার ধীরে দেখুন, একজন ক্রেতা ভুল ফোন দিয়ে submit করলেন, তারপর ঠিক করে আবার করলেন। মান: নাম ' রহিমা ' (দুই পাশে ফাঁকা), ফোন 0171. কোডটি handleSubmit-এর সরল রূপ।
1<form onSubmit={handleSubmit(send)}>2// handleSubmit-এর ভেতরে (সরল রূপ)3e.preventDefault();4state({ isSubmitting: true });5const r = await resolver(values);6if (noErrors(r)) await send(r.values);7else { show(r.errors); focusFirst(); }8state({ isSubmitting: false, submitCount + 1 });
| phone | |
| isSubmitting | |
| errors | |
| focus | |
| submitCount |
- ক্লিক। ক্রেতা “অর্ডার” চাপলেন। Browser
submitevent ছাড়ল, আরhandleSubmit-এর বানানো function সেটি ধরল। - পাতা লোড বন্ধ।
preventDefaultআপনাকে লিখতে হয়নি, library নিজেই করেছে। isSubmittingসত্য। ঠিক এই কারণেই বোতামdisabled={isSubmitting}দিলে দুইবার চাপ পড়ে না।- Resolver চলল। Zod সব মান দেখল।
0171-এ regex ব্যর্থ, তাই pathphone-এর একটি ভুল। নাম ঠিক আছে। - ভুল দেখানো ও focus.
sendচলল না।errors.phonerender-এ পৌঁছাল, আরshouldFocusError(ডিফল্ট true) cursor নিয়ে গেল প্রথম ভুল ঘরে। এই কাজের জন্য ঘরেরrefলাগে, যাregisterদিয়েছে। - শেষ হিসাব।
isSubmittingমিথ্যা,submitCount১. কোনো request যায়নি। - ঠিক করা। ক্রেতা ফোন শেষ করলেন। Submit একবার হয়ে গেছে, তাই
reValidateMode: onChangeকাজ করছে: সঠিক হওয়ামাত্র বার্তা চলে গেল, আবার submit চাপতে হয়নি। - দ্বিতীয় submit. এবার সব ঠিক।
sendপেল Zod-এর ফল, মূল মান নয়: নাম এখন'রহিমা', ফাঁকা বাদ। Request গেল, শেষ হলেsubmitCountহবে ২.
শেষ ধাপটি ছোট কিন্তু দামি। সত্যিকারের run-এ ' রহিমা বেগম ' লিখে পাঠালে server-এ গেল "রহিমা বেগম". trim() আপনি হাতে লেখেননি। Schema শুধু যাচাই করে না, মান গুছিয়েও দেয়। ২০১৯ সালে Alexis King-এর “Parse, don’t validate” লেখাটি এই ভাবনাকে জনপ্রিয় করে: যাচাই শেষে হাতে থাকুক এমন ডেটা, যার ঠিক থাকা নিয়ে আর প্রশ্ন নেই।
৫ · এক schema, দুই পাহারাদার
ব্যাংকের গেটে দারোয়ান আছেন, আবার ভেতরের ভল্টেও আলাদা পাহারা আছে। গেটের দারোয়ান ভদ্রভাবে ফিরিয়ে দেন ভুল ঠিকানার মানুষকে, এতে সবার সময় বাঁচে। কিন্তু দেয়াল টপকে কেউ এলে ভল্টের পাহারাই শেষ ভরসা। Browser-এর যাচাই সেই গেট, server-এর যাচাই সেই ভল্ট।
একই নিয়মের বই, কিন্তু বিশ্বাস শুধু server-এ
One shared schema · the trust boundary is the server
কথাটি নিজে পরীক্ষা করা যায়। Browser, React বা form ছাড়াই curl দিয়ে ভুল ডেটা পাঠালাম। সবচেয়ে ভালো frontend-ও এখানে কিছুই করতে পারে না। দ্বিতীয় চেষ্টায় আকৃতি ঠিক, কিন্তু মজুত নেই, আর সঙ্গে একটি বাড়তি isAdmin.
সত্যিকারের run · একই checkout.ts, ছোট node:http server, 127.0.0.1 · কিছু header ছাঁটা
$ curl -i -X POST -H 'content-type: application/json' \
--data-binary @payload.json http://127.0.0.1:5791/api/orders
HTTP/1.1 422 Unprocessable Entity
content-type: application/json
{"errors":[{"path":"name","message":"পুরো নাম লিখুন"},{"path":"phone","message":"১১ সংখ্যার মোবাইল নম্বর দিন"},{"path":"items.0.qty","message":"একবারে সর্বোচ্চ ১০টি"}]}
$ curl -i … --data-binary @payload2.json …
HTTP/1.1 409 Conflict
content-type: application/json
{"errors":[{"path":"items.0.qty","message":"মজুত আছে 2টি"}]}
Server-এর দিকটি নিচের live বাক্সে আসল node:http দিয়ে দেখুন। Package আনা যায় না, তাই schema-টি ছোট নিয়মের object. Client হিসেবে fetch চলছে, যা browser-এর কোনো যাচাই মানে না।
const http = require('node:http');
const rules = {
name: v => (typeof v === 'string' && v.trim().length >= 2) || 'name: min 2',
phone: v => (typeof v === 'string' && /^01[3-9]\d{8}$/.test(v)) || 'phone: 11 digits',
qty: v => (Number.isInteger(v) && v >= 1 && v <= 10) || 'qty: 1 to 10',
};
function safeParse(input) {
const body = Object(input), data = {}, errors = [];
for (const key in rules) {
const r = rules[key](body[key]);
if (r === true) data[key] = body[key]; else errors.push({ path: key, message: r });
}
return errors.length ? { success: false, errors } : { success: true, data };
}
const server = http.createServer(async (req, res) => {
let raw = '';
for await (const chunk of req) raw += chunk;
const reply = (code, body) => res.writeHead(code, { Connection: 'close' }).end(JSON.stringify(body));
let input;
try { input = JSON.parse(raw); } catch { return reply(400, { message: 'bad JSON' }); }
const r = safeParse(input);
if (!r.success) return reply(422, { errors: r.errors });
const total = 120 * r.data.qty; // দাম server-এর নিজের হিসাব
reply(201, { saved: r.data, total });
});
server.listen(0, '127.0.0.1', async () => {
const url = `http://127.0.0.1:${server.address().port}/api/orders`;
const send = (text) => fetch(url, { method: 'POST', body: text })
.then(async (res) => console.log(res.status, await res.text()));
await send('{broken');
await send(JSON.stringify({ name: 'R', phone: ['01712345678'], qty: 0 }));
await send(JSON.stringify({ name: 'Rahima', phone: '01712345678', qty: 2, price: 1, isAdmin: true }));
server.close();
});আউটপুটের তিনটি লাইন 400 {"message":"bad JSON"}, তারপর 422 তিনটি ভুলসহ, শেষে 201 {"saved":{"name":"Rahima","phone":"01712345678","qty":2},"total":240}. দ্বিতীয় লাইনে ফোন array হিসেবে পাঠানো হয়েছে। typeof না থাকলে regex সেটিকে string বানিয়ে পাস করিয়ে দিত। তৃতীয় লাইনে client price: 1 আর isAdmin: true পাঠালেও সেগুলো বাদ পড়ল, আর মোট হলো ১২০ × ২ = ২৪০. দাম, ছাড়, ভূমিকা কখনো client থেকে নেবেন না।
| নিয়ম | Browser | Server |
|---|---|---|
| আকৃতি, দৈর্ঘ্য, ফোনের ধরন | হ্যাঁ, shared schema | হ্যাঁ, shared schema |
| নম্বর আগে নেওয়া কি না | সুবিধার জন্য জিজ্ঞেস | Database-এর unique নিয়ম, আসল রায় |
| মজুত, দাম, ছাড় | না, শুধু দেখানো | হ্যাঁ, একমাত্র জায়গা |
| ফাইলের ধরন | দ্রুত প্রাথমিক বাধা | ফাইলের ভেতর দেখে যাচাই |
Express-এ এই কাজের পুরো রূপ, যেমন params আর query যাচাই ও middleware, আসবে পর্ব ২৭-এ (Request যাচাই). নিরাপদ ইনপুটের তত্ত্ব আছে API security পর্বে.
৬ · Async যাচাই আর server-এর ভুল field-এ ফেরানো
“এই ফোনে কি আগেই অ্যাকাউন্ট আছে?” এই প্রশ্নের উত্তর browser জানে না, server-কে জিজ্ঞেস করতে হয়। Zod-এ refine async হতে পারে, তাই অনেকে, আর AI প্রায়ই, schema-র ভেতরেই fetch বসান। দেখতে গোছানো, কিন্তু resolver প্রতিবার পুরো schema চালায়। সত্যিকারের run-এ একবার submit করে তারপর শুধু নাম ঘরে “রহিমা” লিখলাম:
সত্যিকারের run · Edge, production build · server-এর গণনা
submit-এর পরে phoneChecks = 1
নাম ঘরে “রহিমা” লেখার পরে phoneChecks = 6
“রহিমা” Unicode-এ ৫টি অক্ষর (র, হ, ি, ম, া), তাই ৫টি বাড়তি request, অথচ ফোন একবারও বদলায়নি। Server-এর ওপর এটি অকারণ চাপ। পথটি বদলান: async যাচাই schema-র বাইরে, ঘর ছাড়ার সময়।
const ctl = useRef<AbortController | null>(null);
async function checkPhone(e: React.FocusEvent<HTMLInputElement>) {
const phone = e.target.value;
if (!/^01[3-9]\d{8}$/.test(phone)) return; // ভুল ধরনে কল নয়
ctl.current?.abort(); // আগের প্রশ্ন বাতিল
ctl.current = new AbortController();
try {
const r = await fetch(`/api/phone-check?phone=${phone}`,
{ signal: ctl.current.signal });
const { taken } = await r.json();
if (taken) setError('phone', { type: 'taken', message: 'এই নম্বরে আগেই অ্যাকাউন্ট আছে' });
else clearErrors('phone');
} catch (err) {
if ((err as Error).name !== 'AbortError') console.warn('যাচাই হয়নি');
} // নেটওয়ার্ক গেলে আটকাবেন না
}
// <input {...register('phone', { onBlur: checkPhone })} />AbortController পুরোনো উত্তরকে নতুন উত্তরের ওপর বসতে দেয় না, এটি race condition-এর চেনা সমাধান। টাইপের সময়েই যাচাই চাইলে debounce লাগবে, যা পর্ব ১৪-এ আছে। ঘর ছাড়ার সময় হলে তার দরকার নেই।
এবার গুরুত্বপূর্ণ কথাটি। সত্যিকারের run-এ 01711111111 দিলে blur-এ বার্তা এলো, তবু “পরের ধাপ” চাপতেই form এগিয়ে গেল। কারণ setError-এর ভুল schema জানে না, পরের যাচাইয়ে schema পাস করলে ভুলটি মুছে যায়। Library-র docs-ও বলে, এমন ভুল টিকে থাকার নিশ্চয়তা নেই। শেষে submit হতেই server বলল 409, আর form ক্রেতাকে ধাপ ১-এর ফোন ঘরে ফেরাল। এটিই ঠিক আচরণ: async যাচাই শুধু আগাম খবর, রায় নয়। দুজন একসাথে একই নম্বর নিলে দুজনকেই “খালি” বলা হতে পারে। শেষ পাহারা database-এর unique নিয়ম, যা আসবে পর্ব ৪৩-এ (Application-এ transaction).
“এই নম্বর আছে কি না” endpoint একটি খোলা দরজাও। কেউ script চালিয়ে লাখো নম্বর মিলিয়ে দেখতে পারে কারা গ্রাহক। একে বলে user enumeration. তাই এই endpoint-এ rate limit রাখুন, আর স্পর্শকাতর জায়গায়, যেমন login বা password reset, এমন উত্তর দেবেন না। বিস্তারিত পর্ব ৪৯-এ (Account-এর কাজ) আর API security পর্বে.
Server-এর ভুল সঠিক ঘরে
ধরুন ক্রেতা ডাল ৩টি চেয়েছেন, মজুত ২টি। এই ভুল শুধু server জানে। পুরো পাতার ওপরে “কিছু একটা ভুল হয়েছে” দেখালে ক্রেতা বুঝবেন না কী করতে হবে। Server যদি ঘরের নাম পাঠায়, form ঠিক সেখানে বার্তা বসাতে পারে। আগেই দেখেছেন, Zod-এর path আর RHF-এর নাম একই ভাষা। তিন ধাপের checkout-এ এই ঘরটি আছে ধাপ ২-এ, আর ক্রেতা এখন ধাপ ৩-এ।
- Browser
POSTপাঠাল - Server
safeParseআকৃতি ঠিক - মজুত
3 > 2আটকাল - Form
setErrorঘরে বসাল
{ "phone": "01712345678",
"items": [ { "sku": "dal-1kg", "qty": 3 },
{ "sku": "rice-5kg", "qty": 1 } ],
"pay": "bkash", "trxId": "8N7K2X1QPA" }
const parsed = checkoutSchema.safeParse(body);
parsed.success // true
order.items.forEach((it, i) => {
if (it.qty > stock[it.sku]) errors.push({
path: `items.${i}.qty`, // i = 0
message: `মজুত আছে ${stock[it.sku]}টি`,
});
});
{ "errors": [{
"path": "items.0.qty",
"message": "মজুত আছে 2টি"
}] }
const first = stepFields.findIndex(names =>
names.some(n => list[0].path.startsWith(n))); // 1
flushSync(() => setStep(first));
list.forEach((e, i) => setError(e.path, {
type: 'server', message: e.message,
}, { shouldFocus: i === 0 }));
// পর্দায়: ডাল − 3 + মজুত আছে 2টি
qty 3 → 2 // বার্তা মুছে গেল POST /api/orders → 201 অর্ডার হয়েছে: BG-1001
- পাঠানো। Browser-এর সব যাচাই পাস। ডাল ৩টি চাওয়া হলো, যা নিয়মে বৈধ, কারণ সর্বোচ্চ ১০।
- Schema আবার পাস। Server নিজে আবার
safeParseচালাল। আকৃতিতে ভুল নেই। - ব্যবসার নিয়মে আটকাল। মজুত ২টি, চাওয়া ৩টি। সারির index দিয়ে path বানানো হলো
items.0.qty. এই তথ্য browser আগে জানত না। - 409 উত্তর। বার্তার সঙ্গে path. Status code-এর আলোচনা পর্ব ৩৩-এ। আকৃতির ভুলে 422, অবস্থার সংঘাতে 409, এটি এখানকার নকশা।
- আগে ধাপ বদল। ঘরটি ধাপ ২-এ, যা এখন DOM-এ নেই। না থাকা ঘরে focus যায় না। প্রথম run-এ ঠিক এই ভুল ধরা পড়েছিল: focus গিয়ে পড়ল
<body>-এ। তাইflushSyncদিয়ে ধাপ আগে mount করা হলো। - ঘরে বার্তা, cursor সেখানে।
setErrorডাল-এর সারিতে “মজুত আছে 2টি” বসাল, আর focus গেল সেই সারির−বোতামে। সত্যিকারের run-এactiveElementছিল এই বোতামই। - ঠিক করে আবার। পরিমাণ ২ করতেই বার্তা সরল। সারির ভুল সারির সঙ্গেই চলে: এক run-এ ওপরের সারি বাদ দিতেই বার্তাটি ঠিক পরিমাণ ৫-এর সারিতেই রইল। এবার 201.
কোনো ঘরের সঙ্গে না মেলা ভুলের জন্য, যেমন server বন্ধ, setError('root.server', { message }) ব্যবহার করুন আর errors.root?.server form-এর ওপরে দেখান। এই ভুল পরের submit-এ নিজে থেকে মুছে যায়।
৭ · Controller, field array আর file input
register চলে সাধারণ HTML ঘরে, কারণ সে ref ধরে DOM পড়ে। কিন্তু date picker, UI library-র select বা নিজের বানানো পরিমাণ-বাটনে পড়ার মতো DOM মান নেই। তখন Controller মধ্যস্থতা করে: সে আপনাকে value, onChange, onBlur, ref দেয়, আপনি component-এ জুড়ে দেন।
<Controller control={control} name={`items.${i}.qty`}
render={({ field }) => (
<QtyStepper ref={field.ref} value={field.value}
onChange={field.onChange} onBlur={field.onBlur} />
)} />field.ref বাদ দিলে কোড চলবে, কিন্তু ভুল হলে focus সেখানে যেতে পারবে না। React 19-এ ref সাধারণ prop হিসেবে আসে, forwardRef লাগে না। মনে রাখবেন, Controller সেই ঘরটিকে controlled বানায়, তাই সব ঘরে নয়, শুধু যেখানে দরকার।
Field array — সারি যোগ আর বাদ
কার্টে কয়টি পণ্য থাকবে আগে জানা নেই। চা-স্টলের বাকির খাতার মতো, নতুন সারি যোগ হয়, পুরোনো কাটা পড়ে। useFieldArray এই কাজের জন্য: fields, append, remove, move, insert, update, replace. প্রতিটি সারিতে সে নিজের একটি id তৈরি করে, যা key হিসেবে দিতে হয়।
const { fields, append, remove } = useFieldArray({ control, name: 'items' });
{fields.map((f, i) => (
<div key={f.id}> {/* index নয়! */}
<input {...register(`items.${i}.sku`)} />
<button type="button" onClick={() => remove(i)}>বাদ</button>
</div>
))}
<button type="button" onClick={() => append({ sku: '', qty: 1 })}>আরও পণ্য</button>Index-কে key করলে uncontrolled ঘরে কী ঘটে, তা নিজে দেখুন। নিচের বাক্সটি ref-ভিত্তিক ছোট একটি field array. প্রতিটি পরিমাণ DOM-এ, আর “যাচাই” চাপলে ref থেকে পড়া হয়। প্রথমে “index-কে key” চিহ্নিত করুন, ডালের পরিমাণ ৫ করুন, ডাল “বাদ” দিন, তারপর “যাচাই” চাপুন। পরে পাতার বাক্সটি আবার চালিয়ে চিহ্ন ছাড়া একই কাজ করুন।
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
let nextId = 3;
function Cart() {
const [rows, setRows] = useState([{ id: 1, sku: 'ডাল' }, { id: 2, sku: 'চাল' }]);
const [byIndex, setByIndex] = useState(false);
const qty = useRef(new Map());
useEffect(() => console.log('সারি:', rows.map(r => r.sku).join(', ')), [rows]);
function read(e) {
e.preventDefault();
const items = rows.map(r => ({ sku: r.sku, qty: Number(qty.current.get(r.id).value) }));
const bad = items.findIndex(x => !(x.qty >= 1 && x.qty <= 10));
console.log(JSON.stringify(items), bad < 0 ? 'ঠিক আছে' : 'ভুল: items.' + bad + '.qty');
}
return (<form onSubmit={read}>
{rows.map((r, i) => <p key={byIndex ? i : r.id}>{r.sku}{' '}
<input type="number" aria-label={r.sku} defaultValue="1"
ref={el => { if (el) qty.current.set(r.id, el); }} />
<button type="button" onClick={() => setRows(rows.filter(x => x !== r))}>বাদ</button></p>)}
<label><input type="checkbox" checked={byIndex}
onChange={e => setByIndex(e.target.checked)} /> index-কে key</label>{' '}
<button type="button" onClick={() => setRows([...rows, { id: nextId++, sku: 'নতুন' }])}>যোগ</button>{' '}
<button>যাচাই</button>
</form>);
}
createRoot(document.getElementById('root')).render(<Cart />);
</script>চালালে আসে সারি: ডাল, চাল. Index key-তে ডাল ৫ করে ডাল বাদ দিলে Edge-এ এলো [{"sku":"চাল","qty":5}] ঠিক আছে. চাল ৫ হয়ে গেল, অথচ কেউ চালে হাত দেয়নি। Key 0 টিকে ছিল বলে React প্রথম সারির DOM রেখে দিল, ভেতরের মান সহ। r.id key-তে একই কাজের ফল [{"sku":"চাল","qty":1}]. Key-র এই আচরণ পর্ব ১১-এ দেখেছেন, আর uncontrolled ঘরে এটি নীরবে ভুল অর্ডার বানায়।
File input
ধরুন মোবাইল ব্যাংকিংয়ের ক্রেতা চাইলে পেমেন্ট স্লিপের ছবি দিতে পারেন। type="file" সব সময় uncontrolled, আর register এতে FileList ফেরত দেয়, একটি File নয়। তাই schema-য় আগে প্রথম ফাইলটি বের করতে হয়। এখানে input আর output-এর type আলাদা, তাই useForm<In, unknown, Out> দিয়ে দুটি জানাতে হয়।
const slipSchema = z.object({
slip: z.custom<FileList>()
.transform((list) => list?.[0] as File | undefined)
.pipe(z.file()
.max(2_000_000, 'ফাইল ২ MB-এর বেশি')
.mime(['image/png', 'image/jpeg'], 'শুধু PNG বা JPEG')
.optional()),
});
type In = z.input<typeof slipSchema>;
type Out = z.output<typeof slipSchema>;সত্যিকারের run · Edge, নকল ফাইল দিয়ে
ফাইল নেই ok no file
slip.txt blocked শুধু PNG বা JPEG
slip.png, 3,000,000 B blocked ফাইল ২ MB-এর বেশি
slip.png, 300,000 B ok slip.png 300000 bytes
JSON.stringify({ slip: file }) → {"slip":{}}
শেষ লাইনটি ফাঁদ। ফাইল JSON-এ যায় না, ফল আসে খালি {}, কোনো error ছাড়াই। তাই ফাইল পাঠাতে হয় FormData দিয়ে, আর এই কারণেই ফাইলের schema shared schema থেকে আলাদা রাখা হয়েছে। Browser-এর mime ধরন আসে ফাইলের নাম ও অপারেটিং সিস্টেম থেকে, যা সহজেই বদলানো যায়, তাই এটি শুধু দ্রুত প্রাথমিক বাধা। Server-এর দিক, multipart আর সীমা, আসবে পর্ব ৩০-এ (File upload).
৮ · Multi-step checkout
পুরো checkout এক পাতায় দিলে ফোনে লম্বা স্ক্রল হয়। তাই ধাপে ভাগ: ঠিকানা, পণ্য, পেমেন্ট। ভর্তির ফরমের কথা ভাবুন, এক পাতা শেষ না হলে পরের পাতায় যাওয়া যায় না, কিন্তু আগের পাতায় ফিরলে লেখা মুছে যায় না। মূল কৌশল তিনটি:
- একটিই
useForm: সবার ওপরে একটি form, প্রতি ধাপের জন্য আলাদা নয়। ভেতরের componentFormProviderআরuseFormContextদিয়ে তা পায়, তাই prop drilling লাগে না। - ধাপের নিয়ম
trigger-এ:trigger(['name', 'phone'])শুধু এই ঘরগুলোর ফল জানায়। পুরো schema একবারে একটিই থাকে। - মান মুছে যায় না:
shouldUnregister-এর ডিফল্টfalse, তাই ধাপ unmount হলেও মান store-এ থাকে।
1const [step, setStep] = useState(0);2async function next() {3 const ok = await trigger(stepFields[step], { shouldFocus: true });4 if (ok) setStep(step + 1);5}6{step === 0 && <Address />}7{step === 1 && <Cart />}8{step === 2 && <Payment />}9<form onSubmit={handleSubmit(send)}>
| step | |
| পর্দায় | |
| store: name | |
| store: items | |
| ok |
- শুরু। ধাপ ০-এ শুধু
Addressmount. Store-এdefaultValues, পণ্যের একটি সারি। - খালি অবস্থায় “পরের ধাপ”।
triggerচারটি নামের ফল দেখল।okমিথ্যা, তাইstepবদলাল না। চার ঘরে চারটি বার্তা এলো, focus গেল নামের ঘরে, আর কোনো request যায়নি। - পূরণ করে এগোনো। এবার
okসত্য,step১.Addressunmount হলো, কিন্তু মান store-এ রইল।Cart-এ “আরও পণ্য” চেপে সারি হলো ২টি। - পেছনে ফেরা। “আগের ধাপ”-এ
triggerলাগে না।Addressআবার mount, সত্যিকারের run-এ ঘরে ছিলরহিমা বেগম. আবার সামনে গেলে সারি ২টি, পরিমাণ['3', '1']. - শেষ ধাপ।
step২. ক্রেতা মোবাইল ব্যাংকিং বাছলেন, Transaction ID ঘর এলো, কিন্তু খালি রেখে অর্ডার চাপলেন। - পুরো schema-র যাচাই।
handleSubmitশুধু শেষ ধাপ নয়, পুরো schema চালায়।refineধরলtrxId, focus গেল সেই ঘরে, request গেল না। ID দিলে তবেই পুরো ডেটা এক JSON-এ গেল।
ধাপের নাম এক জায়গায় থাকে, যাতে ভুল ধাপ খুঁজে বের করতেও একই তালিকা কাজে লাগে:
export const stepFields = [
['name', 'phone', 'city', 'address'],
['items'],
['pay', 'trxId'],
] as const;পাতা refresh হলে store মুছে যায়। লেখা বাঁচাতে ঠিকানা sessionStorage-এ রাখা যায়, ধাপটি রাখা যায় URL-এ ?step=2, যাতে back বোতাম কাজ করে (URL state আসছে পর্ব ২০-এ). পেমেন্ট বা গোপন তথ্য কখনো storage-এ নয়। আর পুরো অর্ডারের “সর্বোচ্চ একবার” নিশ্চয়তা আসে server-এর idempotency থেকে, পর্ব ৬৫-এ।
৯ · সবার জন্য error, ছোট ফাঁদ আর অন্য পথ
পর্ব ১২-এর accessibility নিয়ম এখানেও একই: label, aria-invalid, aria-describedby. React Hook Form শুধু এগুলো সহজ করে। সত্যিকারের run-এ খালি form-এ চাপ দিলে DOM-এ পাওয়া গেল focus name-এ, aria-invalid="true" আর aria-describedby="name-e". একটি ছোট component বারবার লেখা বাঁচায়:
// get, FieldError, Path আসে react-hook-form থেকে
function TextField({ name, label }: { name: Path<Checkout>; label: string }) {
const { register, formState: { errors } } = useFormContext<Checkout>();
const id = useId();
const msg = (get(errors, name) as FieldError | undefined)?.message;
return (
<p>
<label htmlFor={id}>{label}</label>
<input id={id} {...register(name)} aria-invalid={!!msg}
aria-describedby={msg ? `${id}-e` : undefined} />
<span id={`${id}-e`} className="err">{msg}</span>
</p>
);
}লাল রং একা বার্তা নয়, তাই লেখাও থাকবে। Focus ভুল ঘরে গেলে screen reader aria-describedby-র বার্তা পড়ে, তাই আলাদা ঘোষণার দরকার সাধারণত পড়ে না। সার্ভারের সাধারণ ভুলে role="alert" ভালো, যা একটি live region. কিন্তু দশটি ঘরে একসাথে role="alert" দিলে screen reader দশটি বার্তা একসাথে পড়তে পারে।
ছোট ভুল, বড় ফল। Run-এর সময় “পরের ধাপ”-এর একটি click হারিয়ে গেল। কারণ: ঠিকানার ঘর ছাড়তেই blur-এ বার্তা সরল, পাতার লেআউট লাফ দিল, বোতাম মাউসের নিচ থেকে সরে গেল। CSS-এ .err { min-height: 1.5em; } দিয়ে বার্তার জায়গা আগে থেকে রাখতেই সমস্যা গেল। এই লাফানোই CLS-এর একটি উৎস।
Zod-এর বার্তা নিজের লেখা না হলে ইংরেজি আসে, যেমন Invalid input: expected number, received string. Schema-র প্রতিটি নিয়মে বাংলা বার্তা দিন। আর ভুলে যাবেন না, শুধু নিয়মের ভাষা নয়, পর্ব ১২-এর বাংলা অঙ্কের সমস্যাও বড় form-এ থাকে: valueAsNumber ব্যবহার করলে '১২' হয় NaN.
পুরো project-এ যা চলল
সত্যিকারের run · ~/bazarghar · Vite build, Edge · পথের ফলগুলো সংক্ষেপে সাজানো
$ npm ls --depth=0 # লেখার সময়ে, ছাঁটা
+-- @hookform/resolvers@7.0.1
+-- react-hook-form@7.91.0
+-- react@19.3.1
+-- vite@8.3.5
`-- zod@4.6.7
ধাপ 1 / 3 খালি next → errors: name, phone, city, address
ধাপ 1 / 3 ফোন 01711111111 blur → এই নম্বরে আগেই অ্যাকাউন্ট আছে
ধাপ 2 / 3 সারি 2, পরিমাণ ['3', '1']
ধাপ 3 / 3 bkash, ID নেই → Transaction ID দিন · focus: trx · request: 0
ধাপ 2 / 3 server 409 → মজুত আছে 2টি · focus: − বোতাম
ধাপ 3 / 3 পরিমাণ ['2', '1']
অর্ডার হয়েছে: BG-1001
TanStack Form — আরেকটি পথ
TanStack Form (লেখার সময়ে 1.33) একটি headless, framework-নিরপেক্ষ library, React ছাড়াও Vue, Angular, Solid, Svelte আর Lit-এর জন্য adapter আছে। form.Field component আর selector দিয়ে শুধু দরকারি অংশ subscribe করা যায়, type-এর দিকে এটি বেশ কড়া, আর async যাচাইয়ে নিজস্ব debounce আছে। React Hook Form-এর ecosystem আর উদাহরণ বেশি, তাই নতুন দলের জন্য পথটি সহজ। দুটিই ভালো, কোনটি নেবেন তা দলের অভ্যাসের প্রশ্ন। আর React 19-এর useActionState (পর্ব ১২) ছোট form-এ, Server Action (পর্ব ৫৪) Next.js-এ ভালো মানায়, সেখানেও একই Zod schema server-এ চলে।
আরও গভীরে: formState একটি getter-ওয়ালা object: আপনি errors পড়লে library নিজের subscription তালিকায় সেটি চিহ্নিত করে, না পড়লে সেই বদলে render করায় না। Zod 4 আর কয়েকটি schema library Standard Schema নামের একটি সাধারণ interface মানে, তাই @hookform/resolvers-এর standardSchemaResolver বা TanStack Form একই ভাষায় যেকোনো মানা library-র সঙ্গে কাজ করতে পারে। Bundle ছোট রাখতে Zod-এর zod/mini রূপও আছে। React Hook Form docs আর zod.dev আপনার version অনুযায়ী মিলিয়ে নিন।
পরীক্ষা ও ইন্টারভিউতে:
- React Hook Form কেন দ্রুত? ঘরগুলো uncontrolled, ref দিয়ে যুক্ত, তাই টাইপে React render হয় না।
parseআরsafeParse-এর পার্থক্য? প্রথমটি ভুলে throw করে, দ্বিতীয়টিsuccess-সহ object দেয়।- Client-এ যাচাই থাকলে server-এ কেন? Client এড়ানো যায়, তাই client-এর যাচাই শুধু সুবিধা।
useFieldArray-তে key কী?field.id, index নয়, নইলে সারি বাদ দিলে মান ভুল সারিতে যায়।Controllerকখন? যে component-এregister-এর ref দিয়ে মান পড়া যায় না।
১০ · AI-এর যুগে form
Form হলো AI-এর প্রিয় কাজ, কয়েক সেকেন্ডে সুন্দর কোড আসে। কিন্তু form চলা আর form নিরাপদ হওয়া আলাদা জিনিস। একটি সুন্দর checkout, যার API যা পায় তাই নেয়, তা AI-এর কোডে খুবই সাধারণ।
| AI-এর ভুল | ফল | কীভাবে ধরবেন |
|---|---|---|
| শুধু client-এ schema, API ফাঁকা | curl-এ ভুল ডেটা ঢুকে যায় | Form ছাড়া curl দিয়ে API চালান |
| Client থেকে দাম নেয় | কেউ ১ টাকায় অর্ডার করে | Payload বদলে পাঠান, মোট দেখুন |
Schema-র ভেতরে async fetch | প্রতি অক্ষরে request | Network ট্যাব বা server log গুনুন |
key={index} field array-তে | সারি বাদে মান ভুল সারিতে | মাঝের সারি বাদ দিয়ে দেখুন |
পুরোনো API: Zod 3-এর required_error, RHF-এর পুরোনো রূপ | Type error বা চুপচাপ ভিন্ন আচরণ | আপনার version-এর docs দেখুন |
Controller-এ ref বাদ | ভুলে focus যায় না | Tab ছাড়া submit করে দেখুন |
catch {} দিয়ে server-এর ভুল গিলে ফেলা | ক্রেতা কিছুই জানেন না | Server বন্ধ করে submit করুন |
AI-কে কাজ দেওয়ার সময় version, দায়িত্ব আর ভুলের আকৃতি স্পষ্ট বলুন। না বললে সে ধরে নেবে, আর পুরোনো কোড দেবে।
AI-এর লেখা form হাতে পেলে
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১Browser-এ যাচাই করাই আছে। তাহলে server-এ একই কাজ দ্বিতীয়বার করা কি অপচয় নয়?
দুটি যাচাইয়ের কাজ একই দেখালেও উদ্দেশ্য আলাদা। Browser-এর যাচাই ব্যবহারকারীর জন্য: দ্রুত উত্তর, কম অপেক্ষা। Server-এর যাচাই সিস্টেমের জন্য: যাই আসুক, ডেটা নষ্ট হবে না। Browser আপনার নিয়ন্ত্রণে নেই। DevTools খুলে JavaScript বদলানো যায়, curl দিয়ে সরাসরি request পাঠানো যায়, এমনকি একটি পুরোনো ট্যাবে পুরোনো নিয়মও চলতে পারে।
এই ভুলের নিজস্ব নাম আছে। CWE তালিকায় CWE-602 হলো “Client-Side Enforcement of Server-Side Security”, অর্থাৎ যে পাহারা server-এ থাকার কথা তা client-এ রাখা। OWASP-এর নির্দেশনাও বলে, client-side যাচাই সুবিধার জন্য ভালো, কিন্তু নিরাপত্তার জন্য যাচাই server-এ হতে হবে।
বাস্তব উদাহরণ হিসেবে ২০১২ সালে Egor Homakov Rails-এর mass assignment দুর্বলতা দেখাতে GitHub-এ Rails project-এর ওপরেই নিজের key যোগ করে দেখিয়েছিলেন বলে জানা যায়। Mass assignment মানে client যা পাঠায় তা ফিল্টার ছাড়া model-এ বসানো। ওপরের live server-এ isAdmin বাদ পড়াটি এই ভুলেরই ওষুধ।
আর “অপচয়” কথাটি shared schema-তে প্রায় থাকেই না, কারণ নিয়ম একবারই লেখা হয়। যা দুইবার ঘটে, তা শুধু চালানো। সেই খরচ সাধারণত খুব ছোট, আর একটি ভুল অর্ডার বা নষ্ট ডেটা ঠিক করার খরচের তুলনায় নগণ্য। উল্টো দিকে browser-এর যাচাইও বাদ দেওয়া ঠিক নয়। শুধু server-এ যাচাই থাকলে প্রতিটি ছোট ভুলে নেটওয়ার্কে একবার যাওয়া-আসা লাগে, ধীর সংযোগে ক্রেতা অপেক্ষা করেন, আর server-ও অকারণ কাজ পায়।
মূল উপলব্ধি: বিশ্বাসের সীমা browser আর server-এর মাঝখানে। সীমার বাইরের সব কিছু, আপনার নিজের frontend সহ, অপরিচিতের মতো ধরুন।
প্র ০২React-এর docs controlled input শেখায়, তবু React Hook Form uncontrolled পথে গেল কেন? এটি কি React-এর দর্শনের বিরুদ্ধে?
React-এর মূল ভাবনা হলো UI হলো state-এর ফল। Controlled input এই ভাবনার সরল রূপ, তাই docs সেটিই শেখায়। আগের প্রজন্মের জনপ্রিয় form library-গুলো, যেমন Redux Form আর পরে Formik, মূলত এই পথেই চলত। Redux Form প্রতিটি অক্ষর global store-এ পাঠাত, যা বড় form-এ ধীর হতো। Formik state-টি component-এ রাখল, তবু প্রতিটি অক্ষরে পুরো form-এর render-এর সমস্যা থেকে গেল, আর তার জন্য আলাদা optimization লাগত।
Beier Luo-র তৈরি React Hook Form প্রশ্নটি উল্টে দিল: browser তো মান রাখতে জানেই, তাহলে সেটি React-এ নকল করার দরকার কী? যেখানে শুধু submit-এর সময় মান লাগে, সেখানে ঘরের মান দুই জায়গায় রাখাই বাড়তি কাজ। এর ফল ওপরের ছকে: ২২০ বনাম ০।
দাম আছে। Uncontrolled পথে “মান এখন কত” জানতে subscribe করতে হয়, আর watch ভুল জায়গায় বসালে সুবিধা চলে যায়। RHF-এর ভেতরের store React-এর বাইরে থাকে, তাই DevTools-এ সব মান দেখা যায় না, আর কিছু নতুন React ফিচারের সঙ্গে মেলাতে library-কে আলাদা যত্ন নিতে হয়।
দর্শনের বিরুদ্ধে কি না, এটি আসলে ভুল প্রশ্ন। পর্ব ১২-এর নিয়ম ছিল, একটি ঘরের মালিক একজনই। RHF সেটি মানে, মালিক এখানে DOM. React-ও defaultValue আর useRef দিয়ে এই পথকে স্বীকৃতি দেয়, আর React 19-এর form action-ও uncontrolled ঘরের সঙ্গে স্বাভাবিকভাবে চলে।
মূল উপলব্ধি: Framework-এর মূল নীতি মানে সব কাজ একভাবে করা নয়। কোন তথ্য render-এ লাগে আর কোনটি লাগে না, এই ভাগটিই performance-এর আসল নকশা।
প্র ০৩Error কখন দেখাবেন, টাইপ করার সময়, ঘর ছাড়ার পরে, না submit-এ? এর পেছনে কি কোনো গবেষণা আছে?
আছে। Form নকশা নিয়ে Luke Wroblewski-র “Web Form Design” (২০০৮) বইটি একটি পরিচিত সূত্র। ২০০৯ সালে A List Apart-এ তাঁর “Inline Validation in Web Forms” লেখায় একটি ব্যবহারযোগ্যতা পরীক্ষার কথা আছে, যেখানে ঘর ছাড়ার পরে দেখানো বার্তা, টাইপের মাঝখানের বার্তার চেয়ে ভালো কাজ করেছিল বলে জানানো হয়। সংখ্যাগুলো সেই নির্দিষ্ট পরীক্ষার, তাই আপনার ক্ষেত্রে হুবহু মিলবে ধরে নেবেন না।
কারণটি বোঝা সহজ। আপনি 017 লিখলেন, তখনই “ভুল নম্বর” দেখালে মনে হয় form আপনাকে ধমকাচ্ছে, অথচ আপনি লেখা শেষই করেননি। দর্জি মাপ নেওয়ার মাঝখানে “ভুল” বলেন না।
অন্যদিকে ভুল একবার দেখানোর পর ঠিক হওয়ার খবর দেরিতে দিলে ব্যবহারকারী বুঝতে পারেন না সমস্যা গেল কি না। এই দুইয়ের মিলনকে ডিজাইনাররা বলেন “reward early, punish late”: ভুল দেরিতে দেখান, ঠিক হওয়া সঙ্গে সঙ্গে। React Hook Form-এর mode: 'onTouched' ঠিক এই আচরণ দেয়: প্রথম ব্যর্থতা blur-এ, তারপর প্রতিটি বদলে যাচাই।
ব্যতিক্রম আছে। পাসওয়ার্ডের শক্তি মাপার ঘরে, বা অক্ষর-সীমার ক্ষেত্রে, টাইপের সময়ই জানানো ভালো। আর async যাচাই যত দেরিতে হবে, server তত কম চাপ পাবে। ফোন কীবোর্ডে ঘর ছাড়া সহজ নয়, তাই শেষ পর্যন্ত “পরের ধাপ” চাপেও যাচাই থাকতে হবে।
বাংলা ইনপুটে আরেকটি কারণ আছে। পর্ব ১২-এর IME-র কথা মনে করুন: যুক্তাক্ষর লেখার মাঝখানের মান অসম্পূর্ণ থাকে, তাই টাইপের সময়ের যাচাই ভুল বার্তা দিতে পারে।
মূল উপলব্ধি: Validation শুধু যুক্তি নয়, এটি মানুষের সঙ্গে কথা বলা। কী বলছেন যত জরুরি, কখন বলছেন তত জরুরি।
প্র ০৪Blur-এ জিজ্ঞেস করলাম “নম্বর খালি”, তারপর submit করলাম। তবু কেন সেটি নিশ্চিত নয়? আর এই endpoint কি নিরাপদ?
দুটি আলাদা সমস্যা। প্রথমটি সময়ের। “জিজ্ঞেস করা” আর “নেওয়া” দুটি আলাদা মুহূর্ত। এর মাঝে অন্য কেউ একই নম্বর নিতে পারেন। কম্পিউটার বিজ্ঞানে একে বলে TOCTOU: time of check to time of use. নিয়ম হিসেবে যা পরীক্ষা করা হলো, ব্যবহারের সময় সেটি আর সত্য না-ও থাকতে পারে।
ধরুন রহিমা আর করিম দুজনেই ৩ ms-এর ব্যবধানে একই নম্বর দিলেন। দুজনের blur-এই উত্তর এলো “খালি”। দুটি submit-ই server-এ পৌঁছাল। Server যদি আবার SELECT করে তারপর INSERT করে, তাহলেও মাঝখানে ফাঁক থাকে। একমাত্র নিরাপদ পথ database-এর UNIQUE constraint: দ্বিতীয় INSERT ব্যর্থ হবে, আর আপনি সেই error-টিকে phone ঘরের বার্তায় বদলাবেন। তাহলে আগাম যাচাই কেন? কারণ বেশিরভাগ ক্ষেত্রে সে ক্রেতাকে ধাপ ৩ পর্যন্ত গিয়ে আবার ফেরা থেকে বাঁচায়।
দ্বিতীয়টি তথ্য ফাঁসের। “আছে” বা “নেই” বলা endpoint একটি ছোট প্রশ্নোত্তর মেশিন। কেউ script দিয়ে নম্বরের তালিকা চালালে আপনার গ্রাহকের তালিকা পেয়ে যাবে। OWASP-এর Authentication নির্দেশনায় login ও recovery-তে এই কারণে সাধারণ বার্তা দিতে বলা হয়।
Signup-এ পুরোপুরি লুকানো কঠিন, কারণ “এই নম্বরে অ্যাকাউন্ট আছে” বলাই ব্যবহারকারীর জন্য দরকারি। তাই এখানে trade-off: rate limit, CAPTCHA বা OTP-র পরে জানানো। এক পথে জবাব, “নম্বরে একটি কোড পাঠানো হয়েছে”। নম্বর থাকুক বা না থাকুক, উত্তর একই। Friction বাড়ে, ফাঁসও কমে।
মূল উপলব্ধি: Client-এর আগাম যাচাই একটি পরামর্শ, চূড়ান্ত রায় নয়। আর প্রতিটি সুবিধার endpoint একটি নতুন দরজা, তাই খোলার আগে ভাবুন কে কে ঢুকতে পারে।
প্র ০৫একটি schema client ও server-এ ভাগ করা কি সব সময়ই ভালো? কোথায় এটি ক্ষতি করে?
সুবিধা স্পষ্ট: নিয়ম এক জায়গায়, type স্বয়ংক্রিয়, আর ফোনের regex তিন জায়গায় নকল হয় না। এই ভাবনা নতুন নয়। XML Schema, পরে JSON Schema (যার খসড়া ২০১০-এর আশপাশে পরিচিত হয়) আর OpenAPI সবই “ডেটার আকৃতি এক জায়গায় লেখা”-র চেষ্টা। পার্থক্য হলো, TypeScript-এর জগতে সেটি এখন সাধারণ import, আলাদা tool লাগে না। tRPC এই ধারণাকে আরও দূরে নেয়, যা আসবে পর্ব ৩৭-এ। Zod-এর স্রষ্টা Colin McDonnell, আর ২০২৫ সালে Zod 4 এসেছে।
কিন্তু ঝুঁকি চারটি। এক, bundle: যা shared, তা browser-এ যায়। Validation library-র ওজন ক্রেতার ফোনে আসে। দুই, গোপন তথ্য ফাঁস: schema-র ফাইলে server-এর কিছু import করলে, যেমন database client বা API key, সেটি bundle-এ ঢুকতে পারে। Shared ফাইল তাই শুধু শুদ্ধ নিয়ম রাখে। তিন, নিয়ম আসলে এক নয়: file input-এ দেখলেন client-এ FileList, আর server-এ অন্য আকৃতি। Client-এর string আর server-এর database সংখ্যাও আলাদা হতে পারে। জোর করে এক schema-য় ঢোকালে শর্তে ভরে যায়। তাই একটি ভিত্তি schema থেকে .extend() বা ভাগ করা shape দিয়ে দুই দিকের আলাদা রূপ বানান।
চার, সময়ের অমিল: আপনি নতুন schema deploy করলেন, কিন্তু কেউ এক ঘণ্টা আগের ট্যাবে checkout করছেন। তাঁর browser পুরোনো নিয়মে পাঠাবে। মোবাইল অ্যাপে এই ফাঁক সপ্তাহের। তাই server-কে কিছু সময় পুরোনো আকৃতি মানতে হয়, যা versioning-এর প্রশ্ন, আসবে পর্ব ৩৫-এ।
ব্যবসার নিয়ম, যেমন মজুত, schema-য় ঢোকে না, কারণ সেগুলো ডেটার আকৃতি নয়, পৃথিবীর অবস্থা।
মূল উপলব্ধি: ভাগ করুন আকৃতি, ভাগ করবেন না দায়িত্ব। Shared schema কোড কমায়, কিন্তু client আর server যে দুটি আলাদা জগৎ, সে কথা মোছে না।
অনুশীলন
হিসাব, চালানো, অনুমান আর সমালোচনা, চারটিই আছে।
-
Render-এর হিসাব: ৩০ ঘরের একটি controlled form-এ সব মান এক state-এ। ক্রেতা মোট ২০০টি অক্ষর লেখেন। ধরে নিন প্রতি field render ০.৪ ms. (ক) মোট কতটি field render? (খ) মোট কত সময়? (গ) প্রতি অক্ষরে কত ms, frame বাজেট ১৬.৭ ms-এর মধ্যে কি? (ঘ) সব ঘরে memo দিলে field render কত? (ঙ) React Hook Form-এ
watch()ছাড়া টাইপের সময় কত?(ক) ২০০ × ৩০ = ৬,০০০ render. (খ) ৬,০০০ × ০.৪ = ২,৪০০ ms, অর্থাৎ ২.৪ সেকেন্ড। (গ) ৩০ × ০.৪ = ১২ ms, বাজেটের মধ্যে, কিন্তু ফাঁক মাত্র ৪.৭ ms, ধীর ফোনে ছাড়িয়ে যাবে। (ঘ) প্রতি অক্ষরে ১টি, মোট ২০০ render, ৮০ ms (তবে parent নিজে ২০০ বার render হবে)। (ঙ) ০টি, যদি render-এ
errorsবা অন্য বদলাতে থাকাformStateনা পড়া হয়। ০.৪ ms ধরে নেওয়া সংখ্যা। নিজের অ্যাপে React DevTools Profiler দিয়ে মাপুন। -
Browser ছাড়া আক্রমণ: নিজের যেকোনো API-র জন্য একটি ফাইলে ভুল JSON লিখুন, তাতে একটি বাড়তি
isAdminরাখুন, তারপরcurl -i -X POST -H 'content-type: application/json' --data-binary @bad.json URLচালান। কী দেখলে পাস, কী দেখলে বাগ?পাস: 422-এর মতো স্পষ্ট 4xx, ভুলের তালিকা, আর database-এ কিছু জমেনি। বৈধ ডেটায় বাড়তি key থাকলে 201 ঠিক আছে, যদি সংরক্ষিত ডেটায় key না থাকে। বাগ: 500, যার মানে কোড ভুল ডেটা ধরার আগেই ভেঙেছে; অথবা 201 আর ডেটা জমে যাওয়া; অথবা উত্তরে stack trace. এই পর্বের run-এ
payload.jsonদিল 422 আর তিনটি ভুল। -
আউটপুট অনুমান: Zod 4-এর একটি object schema-য়
nameন্যূনতম ২ অক্ষর,cityএকটিenum, আর object-এর ওপরেrefinetrxId-কে দেখে। সবকিছু ঠিক, শুধু (ক)trxIdখালি; (খ)trxIdখালি আর নাম এক অক্ষর; (গ)trxIdখালি আরcity: 'cumilla'. প্রতিটিতেissues-এর path কী হবে?সত্যিকারের run-এর ফল (zod 4.6.7): (ক)
[ 'trxId' ]; (খ)[ 'name', 'trxId' ]; (গ)[ 'city' ]. দৈর্ঘ্যের ভুল পরের যাচাই আটকায় না, তাই (খ)-তেrefineচলেছে। ভুলenumমান পেলে object-এর শুদ্ধতা নিশ্চিত থাকে না, তাই (গ)-তেrefineচলেনি। এজন্যই ক্রেতা শহর ঠিক করার পর নতুন বার্তা দেখবেন। -
AI-এর কোডের সমালোচনা: এক AI বাজারঘরের জন্য লিখল: সব ঘর
useState-এ এক object, প্রতি অক্ষরেschema.safeParse(form), schema-র ভেতরেfetch, সারিতেkey={i}, submit-এfetch('/api/orders', { body: JSON.stringify({ ...form, total }) }), আরcatch (e) {}. কমপক্ষে পাঁচটি সমস্যা আর সমাধান বলুন।- এক state আর প্রতি অক্ষরে parse: বড় form-এ ধীর। RHF অথবা state নিচে নামান।
- Schema-র ভেতরে
fetch: প্রতি যাচাইয়ে request. Blur-এ আলাদাcheckPhone, সঙ্গেAbortController. key={i}: সারি বাদে মান ভুল সারিতে।field.idদিন।totalclient থেকে: দাম বদলানো সম্ভব। Server নিজে হিসাব করবে।catch (e) {}: ভুল চাপা পড়ে। Status দেখেsetErrorঅথবাroot.server.- Server-এ যাচাইয়ের উল্লেখ নেই: একই schema দিয়ে
safeParseবসান, দুইবার submit আটকাতেisSubmittingদেখুন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমReact Hook Form আর Zod-এর একটি সফল ব্যবহারের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ক্রেতা submit চাপেন,
handleSubmitপাতা লোড থামায় - Resolver browser-এ Zod schema চালায়
sendপরিষ্কার মান নিয়েfetchপাঠায়- Server একই schema-য়
safeParseকরে - Server মজুত দেখে 409 অথবা 201 ফেরত দেয়
- Client path দেখে
setErrorদিয়ে ঘরে বার্তা বসায়
২০ ঘরের একটি form-এ React Hook Form ব্যবহার করা হলো, watch() বা formState ছাড়া। একটি ঘরে ১১টি অক্ষর টাইপ করলে টাইপের কারণে প্রায় কতবার form component render হবে?
- ১১ বার
- ০ বার
- ২২০ বার
- ২২ বার
Zod-এ parse আর safeParse-এর মূল পার্থক্য কী?
safeParseকম নিয়ম যাচাই করেsafeParseসব সময় asyncparseভুলে throw করে,safeParseফলাফল-object ফেরত দেয়safeParseশুধু server-এ চলে
একটি API শুধু React form থেকে ব্যবহার হয়, আর form-এ Zod যাচাই আছে। তবু server-এ যাচাই রাখতে হবে কেন?
- Request form ছাড়াও পাঠানো যায়, client-এর যাচাই এড়ানো সম্ভব
- React-এ যাচাই ঠিকমতো চলে না
- Server যাচাই না করলে JSON পড়তে পারে না
- TypeScript type চালানোর সময়ে আপনাআপনি যাচাই করে, তাই সেটি জাগাতে হয়
পণ্যের সারি যোগ-বাদের জন্য useFieldArray ব্যবহার করা হচ্ছে। সারির key কী হওয়া উচিত, আর কেন?
- Index, কারণ এটি সবচেয়ে সরল
Math.random(), কারণ সব সময় আলাদা- পণ্যের
sku, কারণ সেটি অর্থবহ - Library-র দেওয়া
field.id, কারণ এটি সারির সঙ্গে স্থির থাকে
Server উত্তর দিল {"path":"items.1.qty","message":"মজুত নেই"}. ঘরটি form-এর ধাপ ২-এ, ক্রেতা এখন ধাপ ৩-এ। সঠিক কাজ কোনটি?
- ধাপ ৩-এর ওপরে একটি সাধারণ বার্তা দেখানো
- আগে ধাপ ২-এ যাওয়া, তারপর
setErrorদিয়ে সেই path-এ বার্তা ও focus - আগে
setErrorকরে focus, তারপর ধাপ বদল - পুরো form
resetকরে নতুন করে শুরু করা
Zod schema-র ভেতরে async refine দিয়ে ফোন যাচাই করা হলো, আর resolver হিসেবে zodResolver বসানো হলো। একবার submit-এর পরে শুধু নামের ঘরে টাইপ করলে কী ঘটতে পারে?
- শুধু নামের নিয়ম চলে, ফোনের কোনো request যায় না
- Async refine চলে না, schema error ছোড়ে
- প্রতিটি বদলে পুরো schema চলে, তাই ফোনের request-ও বারবার যায়
- Zod ফলটি cache করে, তাই একটিই request যায়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- React Hook Form মান রাখে DOM-এ। টাইপে render হয় না, যদি না ওপরে
watch()ডাকেন। - Zod schema থেকে আসে যাচাই, বাংলা বার্তা আর
z.infer-এর type;safeParseভুলে throw করে না। - একই schema দুই দিকে চলে, কিন্তু রায় দেয় server. দাম, মজুত, ভূমিকা client থেকে নয়।
- ভুলের path field-এর নাম, তাই
setErrorঠিক ঘরে বসায়; আগে ধাপ, পরে focus. - Async যাচাই schema-র বাইরে, field array-তে
field.id, ফাইলFormData-তে, বার্তার জায়গা আগে থেকে রাখা।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২২ · Styling — Tailwind, CSS Modules, component library আর design systemপরবর্তী পর্ববড় অ্যাপের চেহারা গোছানোর পথ।
- ফুল-স্ট্যাক ওয়েব ফ্রেমওয়ার্ক · কোর্স হোম৯৩টি পর্বপুরো কোর্সের রোডম্যাপ।
- পর্ব ১২ · Form — controlled input, submit আর ব্যবহারকারীর ইনপুটভিত্তিControlled, FormData, React 19 form action আর বাংলা IME.
- Cybersecurity · API securityগভীরে যানServer-এর ইনপুট যাচাই আর API-র নিরাপত্তা।
- HTML ও CSS · Accessibilityপাশের ট্র্যাকLabel, ARIA আর সবার ব্যবহারযোগ্য form.