এই পর্বে যা শিখবেন
- একটি link ক্লিকে browser-এর ভেতরে কী কী ঘটে, History API-সহ
- React Router 8-এর তিনটি mode থেকে নিজের project-এর জন্য একটি বেছে নেওয়া
- “বাজারঘর”-এর route, nested layout, loader, action আর error boundary লেখা
- Filter-কে URL-এ রাখা, login-এর পাহারা বসানো, পাতা ধরে code ভাগ করা
- cPanel-এর shared hosting-এ SPA fallback ঠিকভাবে বসানো আর যাচাই করা
১ · ঠিকানা বদলায়, পাতা নতুন করে আসে না
“বাজারঘর”-এর এখন পর্যন্ত একটিই পাতা। কিন্তু একটি দোকানে হোম, পণ্যের তালিকা, একটি পণ্যের বিস্তারিত, ঝুড়ি আর অ্যাকাউন্ট লাগে। প্রতিটির নিজের ঠিকানা (URL) থাকা চাই, যাতে রিনা তাঁর বোনকে WhatsApp-এ একটি পণ্যের link পাঠাতে পারেন, আর Back বোতাম ঠিক আগের জায়গায় ফেরায়।
পর্ব ০৩-এ আপনি SPA দেখেছেন: server একটিই HTML পাঠায়, বাকি পর্দা JavaScript আঁকে। তাহলে একটি পাতার ভেতরেই অনেক ঠিকানা কীভাবে সম্ভব? উত্তরটি History API. Browser JavaScript-কে অনুমতি দেয়: server-এ না গিয়েই address bar-এর লেখা বদলানো যাবে, ইতিহাসে একটি ঘরও যোগ হবে।
একটি বড় সুপারশপের কথা ভাবুন। পুরোনো ওয়েবসাইট (MPA) মানে প্রতিটি তাকের জন্য দোকান থেকে বেরিয়ে আবার মূল গেট দিয়ে ঢোকা, ব্যাগ জমা দেওয়া, টোকেন নেওয়া। SPA মানে আপনি দোকানের ভেতরেই থাকেন, শুধু করিডরের সাইনবোর্ড বদলায়: “তাক ৩”, “তাক ৭”। Router হলো সেই করিডরের কর্মী, যিনি সাইনবোর্ড দেখে ঠিক করেন কোন তাক সামনে আসবে। আর History API হলো সাইনবোর্ড বদলানোর চাবি।
তিনটি ছোট API দিয়েই পুরো ব্যবস্থা চলে। DOM আর event-এর ভিত্তি JavaScript কোর্সে আছে, এখানে শুধু এটুকু দরকার:
| API | কাজ | যা মনে রাখবেন |
|---|---|---|
history.pushState() | URL বদলায়, ইতিহাসে নতুন ঘর যোগ করে | Server-এ কোনো request যায় না, পর্দাও নিজে বদলায় না |
history.replaceState() | URL বদলায়, চলতি ঘরটিই ঘষে দেয় | Back চাপলে পুরোনো ঠিকানায় ফেরা যায় না |
popstate event | Back বা Forward চাপলে আসে | pushState ডাকলে এই event আসে না |
এবার সত্যিকারের একটি ক্লিক ধাপে ধাপে দেখুন। সংখ্যাগুলো “বাজারঘর”-এর build থেকে Edge browser-এ Playwright দিয়ে নেওয়া (React Router 8.5.1, Data mode).
- ক্লিক
<a>পণ্য - থামানো
preventDefaultrequest নেই - Router
matchloader চলছে - URL
pushStateঠিকানা বদল - React
renderপর্দা বদল
location.pathname = "/"
history.state = {"idx":0}
history.length = 2
document request = 1
event.preventDefault() // browser আর /products চাইবে না router.navigate("/products")
location.pathname = "/"
#pending দেখাচ্ছে = হ্যাঁ
h1 = বাজারঘর-এ স্বাগতম
location.pathname = "/products"
history.state = {"usr":null,"key":"tfx56etj","idx":1}
history.length = 3
h1 = পণ্য document request = 1 // শুরুর পর আর বাড়েনি window.__mark = same page
- শুরু। ব্যবহারকারী হোম পাতায়। শুরুর একটি document request হয়ে গেছে।
history.length2, কারণ পরীক্ষার tab-এ আগে একটি ফাঁকা পাতা ছিল। - ক্লিক থামল।
<Link>আসলে একটি সাধারণ<a>, কিন্তু তার click handlerpreventDefaultডেকে browser-কে থামায়। তাই server-এ নতুন HTML চাওয়া হয় না। Ctrl চেপে ক্লিক করলে অবশ্য browser নিজের মতো নতুন tab খোলে। - Router কাজে নামল।
/products-এর সাথে মিলে যাওয়া route খুঁজে তার loader চালাল। খেয়াল করুন, Data mode-এ ১০০ ms পরেও URL পুরোনো, আর পর্দায় “লোড হচ্ছে…” দেখাচ্ছে। - URL বদলাল। Loader শেষে router
pushStateকরল।history.state-এidx0 থেকে 1 হলো, আরkeyএই ঘরটির পরিচয়, যা পরে scroll ফেরাতে কাজে লাগে। - React আঁকল।
h1“পণ্য” হলো, কিন্তু document request 1-ই রইল। শুরুতেwindow-এ রাখা__markমানটিও টিকে আছে, যার অর্থ পাতা আবার load হয়নি।
Back বোতাম: browser popstate পাঠায়, router নতুন URL পড়ে আবার route মেলায়। এজন্যই নিজের হাতে <a href> লিখলে SPA ভেঙে যায়: পুরো পাতা load হয়, state হারায়, ঝুড়ির memory মুছে যায়।
২ · React Router-এর এখনকার রূপ: তিনটি mode
React নিজে route দেখে না। এই কাজটি করে আলাদা library, আর সবচেয়ে প্রচলিত নামটি React Router. লেখার সময়ে (নভেম্বর ২০২৬) npm-এ latest হলো 8.5.1. একই library তিনভাবে ব্যবহার করা যায়, আর প্রতিটি mode আগেরটির ওপর বাড়তি সুবিধা যোগ করে।
| Mode | শুরু হয় | যা পাবেন | কখন |
|---|---|---|---|
| Declarative | <BrowserRouter> + <Routes> | URL মেলানো, Link, params | ছোট অ্যাপ, অথবা ডেটার জন্য আলাদা library আছে |
| Data | createBrowserRouter + <RouterProvider> | ওপরের সব, সাথে loader, action, pending UI, route-ভিত্তিক error | SPA চান, build আর server নিজের হাতে রাখতে চান |
| Framework | Vite plugin @react-router/dev | ওপরের সব, সাথে SSR, pre-render, typed route, স্বয়ংক্রিয় code splitting | পূর্ণ framework চান, server rendering লাগতে পারে |
এই পর্বে আমরা Data mode শিখব, কারণ এটি shared hosting-এর static SPA-তেও চলে, আর loader-এর ধারণা পরিষ্কার দেখায়। Declarative mode অল্প কথায় আগে দেখে নেব। Framework mode-এর SSR-এর ভাবনা আপনি পর্ব ৫১-এ Next.js দিয়ে শিখবেন।
একটি সত্যিকারের project বানাই। Vite project-এর গঠন পর্ব ০৫-এ দেখেছেন।
npm create vite@latest bazarghar-routes -- --template react-ts
cd bazarghar-routes
npm install
npm install react-router$ npm ls react react-router vite
+-- react-router@8.5.1
+-- react@19.3.1
`-- vite@8.3.5
দুটি পুরোনো অভ্যাস বদলেছে। v8-এ react-router-dom আলাদা package হিসেবে আর ব্যবহার হয় না: সব import react-router থেকে, আর RouterProvider আসে react-router/dom থেকে। আর react-router 8.5.1-এর package.json-এ চাহিদা লেখা আছে Node.js 22.22 বা নতুন, React 19.2.7 বা নতুন। পুরোনো টিউটোরিয়ালের কোড তাই সরাসরি চলবে না।
TanStack Router, একটি বিকল্প: TanStack Router-এর জোর type safety-তে। Path, params আর search params সবই TypeScript-এ যাচাই হয়, search params validate করা যায়, loader-এর ভেতরেই cache আছে। লেখার সময়ে এর version 1.170.46. কোনোটি সবার জন্য সেরা নয়: React Router-এর ব্যবহারকারী আর শেখার উপকরণ বেশি, TanStack Router-এ typed URL-এর সুবিধা বেশি। ধারণাগুলো (nested route, loader, search state) দুটোতেই প্রায় এক।
৩ · Route মেলানো: Routes, Link, NavLink, params আর navigate
প্রথমে সবচেয়ে সরল রূপ, Declarative mode. একটি route মানে একটি নিয়ম: “এই URL হলে এই component”. :id অংশটি একটি route param, মানে URL-এর ওই টুকরো যা-ই হোক, সেটি id নামে পাওয়া যাবে।
import { BrowserRouter, Link, Outlet, Route, Routes, useParams } from "react-router";
function Shell() {
return (
<>
<nav><Link to="/">হোম</Link> <Link to="/products/7">পণ্য ৭</Link></nav>
<Outlet />
</>
);
}
function Product() {
const { id } = useParams();
return <h1>পণ্য নম্বর {id}</h1>;
}
export default function DeclarativeApp() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Shell />}>
<Route index element={<h1>হোম</h1>} />
<Route path="products/:id" element={<Product />} />
<Route path="*" element={<h1>৪০৪</h1>} />
</Route>
</Routes>
</BrowserRouter>
);
}সত্যিকারের run (Edge-এ সরাসরি দুটি URL খুলে পাওয়া h1):
/products/7 → পণ্য নম্বর 7
/nope/x → ৪০৪
index route হলো বাবার ঠিক নিজের ঠিকানায় যা দেখাবে। * মানে “বাকি যেকোনো কিছু”, তাই এটিই catch-all route. বাকি চারটি জিনিস রোজ লাগবে:
| API | কাজ | উদাহরণ |
|---|---|---|
<Link> | পাতা load ছাড়া যাওয়া | <Link to="/products"> |
<NavLink> | Link, সাথে “আমি এখন এখানে” বোঝে; সক্রিয় হলে active class আর aria-current="page" বসায় | className={({ isActive }) => …} |
useParams() | URL-এর param পড়া; মান সবসময় string | const { id } = useParams() |
useNavigate() | কোড থেকে যাওয়া, যেমন কাজ শেষে | navigate(-1), navigate("/x", { replace: true }) |
Link আর navigate-এর পার্থক্য সহজ: ব্যবহারকারী যেটিতে ক্লিক করবেন, সেটি <Link>. Screen reader, Ctrl-ক্লিক, “নতুন tab-এ খুলুন” সবই সেখানে কাজ করে। navigate শুধু কোনো কাজের পরে। <button onClick> দিয়ে পাতা বদলানো accessibility-র পরিচিত ভুল।
Router কীভাবে সেরা route বাছে
/products/new তো /products/:id-এর সাথেও মেলে! তাহলে কে জেতে? v6 থেকে React Router প্রতিটি route-কে নম্বর দেয়, লেখার ক্রম দেখে না। নম্বরের নিয়ম সোর্স কোডে লেখা: টুকরো-প্রতি ১ শুরুতে, স্থির শব্দ ১০, :param ৩, ফাঁকা টুকরো ১, আর * থাকলে ২ কাটা। নিচের ব্লকটি সেই ranking-এর ছোট মডেল।
// শেখার মডেল: React Router-এর route ranking
const paths = ['/', '/products', '/products/new', '/products/:id',
'/products/:id/reviews', '/products/*', '/*'];
function score(p) {
const segs = p.split('/');
let s = segs.length;
if (segs.includes('*')) s -= 2;
for (const g of segs) {
if (g === '*') continue;
s += g === '' ? 1 : g[0] === ':' ? 3 : 10;
}
return s;
}
function fits(pattern, url) {
const a = pattern.split('/'), b = url.split('/'), params = {};
for (let i = 0; i < a.length; i++) {
if (a[i] === '*') { params['*'] = b.slice(i).join('/'); return params; }
if (i >= b.length) return null;
if (a[i][0] === ':') params[a[i].slice(1)] = b[i];
else if (a[i] !== b[i]) return null;
}
return a.length === b.length ? params : null;
}
const ranked = [...paths].sort((x, y) => score(y) - score(x));
console.log(ranked.map((p) => p + '=' + score(p)).join(' '));
for (const url of ['/products/new', '/products/7',
'/products/7/reviews', '/products/7/x', '/about']) {
const hit = ranked.find((p) => fits(p, url));
console.log(url, '->', hit, JSON.stringify(fits(hit, url)));
}চালালে দেখবেন:
/products/:id/reviews=28 /products/new=24 /products/:id=17 /products=13 /products/*=12 /=4 /*=1
/products/new -> /products/new {}
/products/7 -> /products/:id {"id":"7"}
/products/7/reviews -> /products/:id/reviews {"id":"7"}
/products/7/x -> /products/* {"*":"7/x"}
/about -> /* {"*":"about"}
/products/new-এর হিসাব: ৩টি টুকরো (৩) + ফাঁকা (১) + products (১০) + new (১০) = ২৪. /products/:id পায় ৩ + ১ + ১০ + ৩ = ১৭. তাই স্থির শব্দটি জেতে। একই পাঁচটি URL আসল matchRoutes ফাংশনে চালিয়েও হুবহু এই ফল পেয়েছি। পুরো algorithm-এ nested route আর index-ও হিসাবে আসে, এই মডেলে তা বাদ।
এবার মূল ধারণাটি হাতে পরখ করুন। এটি শেখার মডেল, আসল React Router নয়, কারণ live block-এ বাইরের package আসে না। Location এখানে একটি সাধারণ variable, আর পুরো router প্রায় ২৫ লাইনের।
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
// শেখার মডেল, আসল React Router নয়: location থাকে memory-তে
let loc = '/';
const subs = new Set();
const go = (to) => { loc = to; console.log('go', to); subs.forEach((f) => f()); };
const useLoc = () => useSyncExternalStore((f) => (subs.add(f), () => subs.delete(f)), () => loc);
function match(pattern, path) {
const a = pattern.split('/'), b = path.split('/'), params = {};
if (a.length !== b.length) return null;
for (let i = 0; i < a.length; i++) {
if (a[i][0] === ':') params[a[i].slice(1)] = b[i];
else if (a[i] !== b[i]) return null;
}
return params;
}
const routes = [['/', () => <h3>হোম</h3>], ['/products/:id', (p) => <h3>পণ্য নম্বর {p.id}</h3>]];
function Router() {
const path = useLoc();
for (const [pattern, View] of routes) {
const params = match(pattern, path);
if (params) { console.log('match', pattern, JSON.stringify(params)); return View(params); }
}
console.log('no match', path);
return <h3>৪০৪</h3>;
}
const Link = ({ to, children }) => <a href={to} onClick={(e) => { e.preventDefault(); go(to); }}>{children} · </a>;
createRoot(document.getElementById('root')).render(<><Link to="/">হোম</Link><Link to="/products/7">পণ্য ৭</Link><Link to="/x/y">ভুল ঠিকানা</Link><Router /></>);
</script>পাতা খুলতেই নিচে আসে:
match / {}
এবার link-গুলোতে ক্লিক করুন। Link ক্লিক থামিয়ে go ডাকে, go সব subscriber-কে জানায়, Router আবার render হয়ে route খোঁজে। useSyncExternalStore হলো React-এর বাইরের কোনো মানে সাড়া দেওয়ার hook. আসল router-এ loc-এর জায়গায় থাকে window.location, go-র ভেতরে থাকে pushState, আর popstate-এও একই subscriber ডাকা হয়।
৪ · Nested route আর Outlet: layout একবার, ভেতরটা বদলায়
“বাজারঘর”-এর ওপরে সবসময় একটি header থাকে। অ্যাকাউন্টের ভেতরে আবার একটি নিজস্ব menu থাকে। প্রতিটি পাতায় এগুলো কপি করবেন? না। URL যেমন /account/orders ভাঁজে ভাঁজে সাজানো, UI-ও তেমন। একে বলে nested route. বাবা route-এর component নিজের ভেতরে একটি ফাঁকা জায়গা রাখে, <Outlet />, আর সন্তান route সেখানে বসে। এমন বাবাকে বলে layout route.
দর্জির দোকানের শো-কেসটি ভাবুন। কাঠের ফ্রেম (header) বছরের পর বছর একই থাকে, ভেতরের ম্যানিকিন (অ্যাকাউন্ট menu) মাঝে মাঝে বদলায়, আর ম্যানিকিনের গায়ের পোশাক (অর্ডার বা রিপোর্ট) রোজ বদলায়। পোশাক বদলাতে ফ্রেম ভাঙার দরকার পড়ে না।
Route tree আর পর্দা একই গঠনের
URL segments map to nested layouts through Outlet
এবার Data mode-এ পুরো অ্যাপের route দেখুন। এখানে route হলো সাধারণ JavaScript object, আর সেগুলো React-এর বাইরে একবারই তৈরি হয়।
export const router = createBrowserRouter([
{
id: "root",
path: "/",
Component: RootLayout,
loader: rootLoader,
HydrateFallback: Splash,
ErrorBoundary: RootError,
children: [
{ index: true, Component: Home },
{ path: "products", Component: Catalog, loader: catalogLoader },
{
path: "products/:id",
Component: ProductPage,
loader: productLoader,
action: addToCartAction,
ErrorBoundary: ProductError,
},
{ path: "login", Component: Login, action: loginAction },
{
path: "account",
middleware: [requireUser],
Component: AccountLayout,
children: [
{ index: true, loader: () => redirect("/account/orders") },
{ path: "orders", lazy: () => import("./routes/orders") },
{ path: "reports", lazy: () => import("./routes/reports") },
],
},
{ path: "*", Component: NotFound },
],
},
]);import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { RouterProvider } from "react-router/dom";
import { router } from "./router";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
);সন্তানের path-এ শুরুর / নেই, কারণ সেটি বাবার ঠিকানার সাথে জোড়া লাগে। নিচের stepper-এর প্রতিটি log সত্যিকারের production build থেকে নেওয়া। Development-এ StrictMode render দুবার চালায়, তাই সেখানে log দ্বিগুণ দেখবেন।
- Root
/header - Account
accountmenu - পাতা
orders / reportsপরিবর্তনশীল
matches = [ root, account, orders ]
[render] RootLayout nav=idle cart=2 [render] AccountLayout
[render] RootLayout nav=idle cart=2
[render] AccountLayout
[render] Orders
আগে = [ root, account, orders ]
এখন = [ root, account, reports ]
নেটওয়ার্ক: /assets/reports-sWvaNfC6.js
[middleware] requireUser user=রিনা
[render] RootLayout nav=loading cart=2
[render] RootLayout nav=idle cart=2
[render] Reports
h1 এখনো: আমার অ্যাকাউন্ট
- শিকল তৈরি। Router URL-কে ভেঙে route tree-র ওপর থেকে নিচে মিলিয়ে তিনটি route-এর একটি তালিকা পায়।
- বাবারা আগে।
RootLayout, তারপর তার<Outlet />-এAccountLayoutrender হয়। - পাতা শেষে।
AccountLayout-এর Outlet-এ বসেOrders. ছবি ২০.১-এর তিনটি ভাঁজ এখন পর্দায়। - পাশের পাতায় ক্লিক। নতুন শিকলের প্রথম দুটি ঘর আগের মতোই, শুধু শেষ ঘরটি আলাদা। Router তাই Reports-এর আলাদা JavaScript ফাইলটি আনে।
- Layout নড়ল না।
[render] AccountLayoutআর আসেনি, মানে layout-এর ভেতরের state, scroll, খোলা menu অক্ষত।RootLayoutদুবার render হলো শুধু কারণ সেuseNavigationপড়ে। Middleware অবশ্য প্রতি navigation-এ চলে।
এবার নিজের হাতে দেখুন। আবার শেখার মডেল: tree থেকে শিকল বানিয়ে, ভেতর থেকে বাইরে Context দিয়ে মুড়ে Outlet তৈরি।
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
// শেখার মডেল: route tree থেকে layout-এর শিকল, Context দিয়ে Outlet
const Slot = createContext(null);
const Outlet = () => useContext(Slot);
function Shop() { return <div><b>বাজারঘর</b><Outlet /></div>; }
function Account() {
const [n, setN] = useState(0);
return <section><button onClick={() => setN(n + 1)}>layout-এর state: {n}</button><Outlet /></section>;
}
const Orders = () => <p>অর্ডারের তালিকা</p>;
const Reports = () => <p>বিক্রির রিপোর্ট</p>;
const tree = { path: '', C: Shop, kids: [{ path: 'account', C: Account, kids: [
{ path: 'orders', C: Orders }, { path: 'reports', C: Reports }] }] };
function chain(node, segs) {
if (node.path !== segs[0]) return null;
if (segs.length === 1) return [node];
for (const k of node.kids ?? []) { const c = chain(k, segs.slice(1)); if (c) return [node, ...c]; }
return null;
}
function App() {
const [path, setPath] = useState('/account/orders');
const nodes = chain(tree, path.split('/'));
console.log(path, '=', nodes.map((n) => n.C.name).join(' > '));
const ui = nodes.reduceRight((inner, n) => <Slot.Provider value={inner}><n.C /></Slot.Provider>, null);
return <>{['orders', 'reports'].map((p) => <button key={p} onClick={() => setPath('/account/' + p)}>{p}</button>)}{ui}</>;
}
createRoot(document.getElementById('root')).render(<App />);
</script>চালাতেই আসে:
/account/orders = Shop > Account > Orders
“layout-এর state” বোতামটি কয়েকবার চাপুন, তারপর reports চাপুন। সংখ্যাটি মুছবে না, কারণ Account একই জায়গায় একই ধরনের component হিসেবে থেকে যায়। পর্ব ১৫-এর reconciliation-এর নিয়ম এটি। এই মডেলে Account আবার render হয়, আর আসল router সেটুকুও এড়াতে পারে, যা stepper-এর log-এ দেখেছেন।
৫ · URL-ই state: useSearchParams দিয়ে filter
রিনা “ঘরের জিনিস” বেছে দাম অনুযায়ী সাজালেন, তারপর একটি পণ্য খুলে Back চাপলেন। Filter যদি useState-এ থাকে, তা মুছে যায়। আবার link পাঠালে বোনও সাজানো তালিকা পান না। পর্ব ১০-এর state এখানে ভুল ঘর। যে state ঠিকানার অংশ হওয়া উচিত, তা URL-এর query অংশে রাখুন: /products?cat=home&sort=price. এই অংশকে বলে search params.
export function Catalog() {
const { list } = useLoaderData() as { list: Product[] };
const [params, setParams] = useSearchParams();
const cat = params.get("cat") ?? "";
function choose(value: string) {
const next = new URLSearchParams(params);
if (value) next.set("cat", value);
else next.delete("cat");
setParams(next);
}
// … select আর তালিকা
}খেয়াল করুন, params কপি করে বদলানো হচ্ছে। এতে sort-এর মতো অন্য param হারায় না। আর setParams প্রতিবার ইতিহাসে নতুন ঘর যোগ করে। টাইপ করার সাথে সাথে search হলে setParams(next, { replace: true }) দিন, নইলে Back বোতামে অক্ষর ধরে ধরে ফিরতে হবে।
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
// শেখার মডেল: useSearchParams-এর ছোট রূপ, URL থাকে memory-তে
let search = '?cat=home&sort=price';
const subs = new Set();
const useSearch = () => useSyncExternalStore((f) => (subs.add(f), () => subs.delete(f)), () => search);
function useParamsModel() {
const params = new URLSearchParams(useSearch());
const set = (key, value) => {
const next = new URLSearchParams(search);
if (value) next.set(key, value); else next.delete(key);
search = next.size ? '?' + next : '';
subs.forEach((f) => f());
};
return [params, set];
}
const items = [{ name: 'চাল', cat: 'grocery', price: 410 }, { name: 'তেল', cat: 'grocery', price: 380 },
{ name: 'গামছা', cat: 'home', price: 150 }, { name: 'জগ', cat: 'home', price: 520 }];
function Catalog() {
const [params, set] = useParamsModel();
const cat = params.get('cat'), sort = params.get('sort');
const rows = items.filter((i) => !cat || i.cat === cat).sort((a, b) => (sort ? a.price - b.price : 0));
console.log('/products' + search, '=', rows.map((r) => r.name).join(', '));
return <div><code>/products{search}</code><br /><button onClick={() => set('cat', 'grocery')}>মুদি</button>
<button onClick={() => set('cat', '')}>সব</button><button onClick={() => set('sort', sort ? '' : 'price')}>দাম</button>
<ul>{rows.map((r) => <li key={r.name}>{r.name} ৳{r.price}</li>)}</ul></div>;
}
createRoot(document.getElementById('root')).render(<Catalog />);
</script>চালাতেই আসে:
/products?cat=home&sort=price = গামছা, জগ
তালিকা কোনো নিজস্ব state নয়, URL থেকে হিসাব করে বের করা (derived state). তাই বোতাম চাপলে শুধু URL বদলায়, বাকিটা নিজে থেকেই মেলে। আসল অ্যাপের run-এ select বদলাতেই URL হলো /products?cat=home, আর loader নতুন করে চলে ২টি সারি আনল।
| State | কোথায় | কেন |
|---|---|---|
| Filter, sort, পাতা নম্বর, খোঁজার শব্দ | URL search params | Share, bookmark, Back, reload সবই কাজ করে |
| কোন পণ্য, কোন অর্ডার | URL path (:id) | এটিই পাতার পরিচয় |
| Modal খোলা, input-এ লেখা অর্ধেক | useState | Link-এ পাঠানোর মতো জিনিস নয় |
| Password, token, ব্যক্তিগত তথ্য | কখনোই URL নয় | History, server log আর Referer-এ থেকে যায় |
৬ · Loader আর action: ডেটা আগে, পাতা পরে
পর্ব ১৩-এ আপনি useEffect দিয়ে fetch করেছেন। তার ধরন হলো: আগে খালি পাতা আঁকা, তারপর ডেটা আনা, তারপর আবার আঁকা। Nested পাতায় এতে সিঁড়ি তৈরি হয়: বাবা শেষ হলে সন্তান শুরু। Loader ক্রমটি উল্টে দেয়। Route-এর সাথে একটি async ফাংশন জুড়ে দেওয়া হয়, আর router সেটি component render হওয়ার আগেই চালায়।
async function catalogLoader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const cat = url.searchParams.get("cat");
const sort = url.searchParams.get("sort");
await wait(300); // এখানে আসল fetch বসবে
const list = products.filter((p) => !cat || p.cat === cat);
if (sort === "price") list.sort((a, b) => a.price - b.price);
return { list };
}
async function productLoader({ params }: LoaderFunctionArgs) {
const product = await getProduct(Number(params.id));
if (!product) throw data("এই পণ্য নেই", { status: 404 });
return { product };
}Component শুধু useLoaderData() ডেকে তৈরি ডেটা পড়ে, তার কোনো loading শাখা লাগে না। লক্ষ করুন, request হলো Web standard Request object, যদিও এখানে কোনো server নেই। Router নিজের ভেতরেই এটি বানায়।
1async function catalogLoader() {2 log("catalog start");3 await wait(300);4 log("catalog done rows=4");5 return { list };6}7function RootLayout() { log(nav.state) }8function Catalog() {9 const { list } = useLoaderData();10 log("Catalog rows=" + list.length);11}
| URL | |
| navigation.state | |
| পর্দার পাতা |
- ক্লিক। হোম পাতা থেকে “পণ্য”-তে ক্লিক। এখনো সব আগের মতো।
- Loader শুরু। Route মিলতেই
catalogLoaderচলল।Catalogcomponent এখনো একবারও ডাকা হয়নি।navigation.stateহলোloading. - পুরোনো পাতাই থাকে। ডেটা না আসা পর্যন্ত হোম পাতা পর্দায় থাকে।
RootLayoutuseNavigationপড়ে “লোড হচ্ছে…” দেখায়। ফাঁকা পাতার ঝলক নেই। - Loader শেষ। ৩০০ ms পরে ৪টি সারি এলো। URL এখনো
/. - একসাথে বদল। URL
/productsহলো, stateidleহলো, আর এই প্রথমCatalogrender হলো, ডেটা হাতে নিয়েই।
Pending UI: অপেক্ষার সময় কী দেখাবেন
Pending UI মানে ডেটা আসার মাঝের সময়ের সংকেত। ৩০০ ms-এর অপেক্ষাতেও ব্যবহারকারী ভাবতে পারেন ক্লিক হয়নি। Router দুই মাপে সংকেত দেয়।
const navigation = useNavigation(); // "idle" | "loading" | "submitting"
{navigation.state !== "idle" && <div role="status">লোড হচ্ছে…</div>}
// শুধু ওই link-এর পাশে
<NavLink to="/products">
{({ isPending }) => <>পণ্য {isPending && "…"}</>}
</NavLink>একদম প্রথম load-এ দেখানোর মতো পুরোনো পাতা থাকে না, তাই route-এ HydrateFallback দিতে হয়। আমাদের Splash সেই কাজ করে।
Action: ডেটা বদলানো আর পাতা নিজে থেকে নতুন হওয়া
Loader পড়ে, action লেখে। React Router-এর <Form> দেখতে HTML form-এর মতো, কিন্তু submit হলে server-এর বদলে route-এর action ফাংশনে যায়। Action শেষে পাতার loader-গুলো আবার চলে, একে বলে revalidation. তাই ঝুড়ির সংখ্যা হাতে বাড়াতে হয় না।
async function addToCartAction({ request, params }: ActionFunctionArgs) {
const form = await request.formData();
const qty = Number(form.get("qty"));
const product = products.find((p) => p.id === Number(params.id));
if (!product) throw data("এই পণ্য নেই", { status: 404 });
if (!Number.isInteger(qty) || qty < 1 || qty > product.stock) {
return data({ error: `পরিমাণ 1 থেকে ${product.stock}-এর মধ্যে দিন` }, { status: 400 });
}
addToCart(qty);
return { ok: true };
}গামছার পাতায় দুটি সত্যিকারের submit. প্রথমে ভুল পরিমাণ (99), তারপর ঠিক পরিমাণ (2):
--- qty=99
[action] add-to-cart product=3 qty=99
[render] RootLayout nav=idle cart=0
[render] ProductPage id=3
alert: পরিমাণ 1 থেকে 30-এর মধ্যে দিন ঝুড়ি: 0
--- qty=2
[action] add-to-cart product=3 qty=2
[loader] root
[loader] product 3 start
[render] RootLayout nav=loading cart=0
[render] ProductPage id=3
[loader] product 3 done
[render] RootLayout nav=idle cart=2
[render] ProductPage id=3
ঝুড়ি: 2 url: /products/3
দুটি জিনিস লক্ষ করুন। ৪০০ status ফিরলে কোনো loader চলেনি, কারণ ডেটা বদলায়নি। সফল হলে root আর product, দুটি loader-ই আবার চলেছে, তাই header-এর ঝুড়ি 2 হলো। কখন কোন loader আবার চলবে, সেই ডিফল্ট নিয়ম হলো: নিজের route param বদলালে, search param বদলালে, অথবা action সফল হলে। শুধু পথ বদলালে আগে থেকে পর্দায় থাকা বাবার loader চলে না। নিজের মতো ঠিক করতে route-এ shouldRevalidate দেওয়া যায়। Action-এর validation browser-এ চলে, তাই এটি শুধু সুবিধা। বিশ্বাসযোগ্য যাচাই হবে server-এ, যা পর্ব ২৭-এ দেখবেন।
Loader, নাকি TanStack Query? পর্ব ১৯-এ দেখা TanStack Query আর loader একে অন্যের শত্রু নয়। Loader বলে “এই পাতার জন্য কখন আনব”, Query রাখে cache, stale সময় আর background refetch. ছোট অ্যাপে শুধু loader চলে। বড় অ্যাপে অনেকে loader-এর ভেতরে Query-র cache ভরে নেন, আবার অনেকে শুধু Query রেখে Declarative mode-এই থাকেন। দুটি পথই প্রচলিত, নির্ভর করে আপনার cache-এর চাহিদার ওপর।
৭ · Error boundary, 404 আর protected route
প্রতিটি route-এর নিজের দুর্ঘটনা-ঘর
Loader, action অথবা render-এ কিছু ভাঙলে পুরো পর্দা সাদা হওয়া উচিত নয়। Route-এ ErrorBoundary দিলে সবচেয়ে কাছের error boundary শুধু ওই route-এর অংশটুকু বদলে দেয়। Header, menu অক্ষত থাকে। এখানে useRouteError error-টি দেয়, আর isRouteErrorResponse বলে দেয় এটি ইচ্ছা করে ছোড়া সাড়া (যেমন 404), না আসল বাগ।
export function ProductError() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return <h1>{error.status}: {String(error.data)}</h1>;
}
return <h1>পণ্য দেখানো গেল না, একটু পরে আবার চেষ্টা করুন</h1>;
}তিন রকম ভুল ঠিকানায় সত্যিকারের ফল:
| URL | কী ঘটল | পর্দার h1 | Header ছিল? |
|---|---|---|---|
/products/99 | Loader পণ্য পায়নি, throw data(…, 404) | 404: এই পণ্য নেই | হ্যাঁ |
/products/13 | নকল database-এ throw new Error | পণ্য দেখানো গেল না, একটু পরে আবার চেষ্টা করুন | হ্যাঁ |
/foo/bar | কোনো route মেলেনি, * ধরেছে | ৪০৪: পাতাটি পাওয়া যায়নি | হ্যাঁ |
/nope, * route ছাড়া | Router নিজে 404 বানিয়ে root boundary-তে দিল | Root boundary 404 Not Found | না |
শেষ সারিটিই * route রাখার কারণ: তা না থাকলে root-এর boundary পুরো layout-এর জায়গা নেয়, header-ও চলে যায়। আর boundary-র বার্তায় error.message সরাসরি দেখাবেন না, ব্যবহারকারীকে ভেতরের তথ্য জানানোর দরকার নেই।
Protected route: পাহারা, কিন্তু তালা নয়
“আমার অর্ডার” পাতা শুধু login করা মানুষ দেখবেন। এমন route-কে বলে protected route. React Router 8-এ পাহারার পরিষ্কার জায়গা হলো route middleware: এটি সেই route আর তার সব সন্তানের loader-এর আগে চলে।
export const userContext = createContext<User | null>(null);
const requireUser: MiddlewareFunction = async ({ request, context }) => {
const user = getUser();
if (!user) {
const { pathname, search } = new URL(request.url);
throw redirect(`/login?next=${encodeURIComponent(pathname + search)}`);
}
context.set(userContext, user);
};
// next শুধু নিজের সাইটের ভেতরের path হলে তবেই মানা হবে
function safeNext(raw: string | null) {
return raw && raw.startsWith("/") && !raw.startsWith("//") ? raw : "/";
}--- login ছাড়া অ্যাকাউন্টে ক্লিক
[middleware] requireUser user=none
url /login?next=%2Faccount scripts []
--- লগইন করে ঢোকা
[middleware] requireUser user=রিনা
…
[loader] orders for রিনা
…
[render] AccountLayout
[render] Orders
url /account/orders scripts ['/assets/orders-LK8phY5Y.js']
--- /login?next=//evil.example/x দিয়ে লগইন
url / host same True
Login ছাড়া ঢুকতে চাইলে scripts [], মানে অর্ডারের ফাইলটি পর্যন্ত নামেনি। Login শেষে next ফেরত নিয়ে গেল। আর safeNext না থাকলে ওই শেষ link-টি login-এর পরে ব্যবহারকারীকে অন্য সাইটে পাঠাতে পারত। এই ফাঁদের নাম open redirect.
এটি নিরাপত্তা নয়। Browser-এর সব কোড ব্যবহারকারীর হাতে। DevTools খুলে যে কেউ requireUser এড়াতে পারেন, আর orders-LK8phY5Y.js ফাইলটি সবার জন্য খোলা। Route পাহারার কাজ শুধু সুন্দর অভিজ্ঞতা দেওয়া। আসল তালা হলো API: প্রতিটি request-এ server যাচাই করবে এই মানুষ এই অর্ডার দেখতে পারেন কি না। তত্ত্ব আছে Broken Access Control পর্বে, আর বাস্তব কোড পর্ব ৪৮-এ।
৮ · Route ধরে code splitting আর scroll ফেরানো
যে ক্রেতা শুধু গামছা কিনতে এসেছেন, তাঁকে অ্যাডমিনের chart library কেন নামাতে হবে? Code splitting মানে JavaScript-কে ভাগ করা, আর route হলো ভাগের সবচেয়ে স্বাভাবিক দাগ। আধুনিক dynamic import (import("./x")) দেখলেই Vite একটি আলাদা chunk বানায়। Route-এর lazy সেটি প্রথম দরকারের সময় আনে।
import { useLoaderData } from "react-router";
import type { LoaderFunctionArgs } from "react-router";
import { userContext } from "../router";
// lazy এই ফাইলের loader আর Component নিজে তুলে নেয়
export async function loader({ context }: LoaderFunctionArgs) {
const user = context.get(userContext);
return { owner: user?.name, orders: ["#1042", "#1043"] };
}
export function Component() {
const { orders } = useLoaderData() as { orders: string[] };
return <ul>{orders.map((o) => <li key={o}>{o}</li>)}</ul>;
}রিপোর্ট পাতায় একটি সত্যিকারের chart library (chart.js 4.6.1) বসিয়ে দুবার build করলাম, একবার lazy দিয়ে, একবার সব সরাসরি import করে:
$ npm run build # lazy সহ
dist/index.html 0.39 kB │ gzip: 0.27 kB
dist/assets/orders-LK8phY5Y.js 0.42 kB │ gzip: 0.32 kB
dist/assets/reports-sWvaNfC6.js 204.13 kB │ gzip: 70.10 kB
dist/assets/index-CfHZm8TN.js 326.34 kB │ gzip: 103.29 kB
# lazy ছাড়া, সব এক ফাইলে
dist-static/assets/index-D_WESTq1.js 529.57 kB │ gzip: 172.81 kB
হিসাব করি। সময় ≈ আকার × 8 ÷ গতি (নেটওয়ার্কের বাকি দেরি বাদ দিয়ে)। ধরে নিই গতি 1 Mbps, মানে সেকেন্ডে 1000 kilobit (অনুমিত সংখ্যা):
| ব্যবস্থা | প্রথম পাতায় gzip | হিসাব | সময় |
|---|---|---|---|
| সব এক ফাইলে | 172.81 kB | 172.81 × 8 ÷ 1000 | ≈ 1.38 s |
| Route ধরে ভাগ | 103.29 kB | 103.29 × 8 ÷ 1000 | ≈ 0.83 s |
প্রথম load-এ ৬৯.৫২ kB, মানে প্রায় ৪০% কম নামে। ধরুন মাত্র ৫% ভিজিটর রিপোর্ট খোলেন। তাহলে ভিজিটর-প্রতি গড় ডাউনলোড 103.29 + 0.05 × 70.10 ≈ 106.80 kB. এই সংখ্যাগুলো একটি ছোট নমুনা অ্যাপের এক দফা build, সাধারণ নিয়ম নয়। Parse আর চালানোর সময়ও যোগ হবে, যা ধীর ফোনে বেশি। Bundle-এর ভেতরের হিসাব পাবেন পর্ব ৭৯-এ, আর React-এর নিজস্ব React.lazy ও Suspense পর্ব ২৩-এ।
দাম একটিও আছে: chunk নামতে যতটা সময়, প্রথমবার পাতা বদলাতে ততটাই দেরি। Run-এর log-এ দেখা গেছে দ্বিতীয়বার orders পাতায় ফিরলে scripts [], মানে ফাইল আর নামেনি।
Scroll: পাতা বদলালে কোথায় থাকবেন
MPA-তে browser নিজে scroll সামলায়। SPA-তে পাতা আসলে একই, তাই নতুন পাতাও মাঝখান থেকে দেখা দিতে পারে, আর Back চাপলে পুরোনো জায়গা হারিয়ে যায়, কারণ ডেটা তখনো আসেনি। Data mode-এ <ScrollRestoration /> root layout-এ একবার বসালেই হয়। এটি sessionStorage-এ প্রতিটি ইতিহাস-ঘরের key ধরে অবস্থান রাখে। Declarative mode-এ এটি নেই।
তালিকায় পণ্যের পাতায় Back চাপার পরে
ScrollRestoration সহ 900 0 900
ScrollRestoration ছাড়া 900 0 0
Edge-এর এই run-এ চাওয়া ফলটি মিলেছে: সামনে গেলে ওপরে, পেছনে ফিরলে আগের জায়গায়।
৯ · Server-কে জানানো: SPA fallback আর cPanel
সবচেয়ে পরিচিত অভিযোগটি হলো: “আমার কম্পিউটারে চলছিল, hosting-এ তুললাম, হোম ঠিক আছে, কিন্তু পণ্যের পাতায় reload দিলেই 404!” কারণটি পরিষ্কার। Link ক্লিকে router কাজ করে। কিন্তু reload, bookmark, WhatsApp-এর link মানে browser সরাসরি server-কে বলছে: “/products/3 পাতাটি চাই”। এটি একটি deep link. Server তখনো React Router চেনে না, আর ওই নামে কোনো ফাইল নেই।
আর dev server এই সমস্যা লুকিয়ে রাখে। Vite preview একই URL-এ 200 দেয়, আসল Apache দেয় 404. সত্যিকারের run (Apache 2.4.58, নিজের কম্পিউটারে, dist সরাসরি রেখে):
$ curl -s -o /dev/null -w "%{http_code} %{content_type}\n" <url> # ডানের path আমরা পাশে লিখেছি
# vite preview
200 text/html /products/3
# Apache, .htaccess ছাড়া
200 text/html /
404 text/html; charset=iso-8859-1 /products/3
200 text/javascript /assets/index-CfHZm8TN.js
সমাধান SPA fallback: বাস্তব ফাইল থাকলে সেটিই পাঠানো হবে, না থাকলে index.html. বাকিটা browser-এর router বুঝে নেবে। cPanel-এর সাধারণ shared hosting-এ Apache বা তার সমতুল্য server থাকে, আর সেখানে এই নিয়ম লেখা হয় .htaccess ফাইলে, mod_rewrite দিয়ে।
একটি সরাসরি অনুরোধে Apache কী ভাবে
How the .htaccess rules decide, then the browser router takes over
ইন্টারনেটে যে নিয়মটি সবচেয়ে বেশি কপি হয়, তা প্রায় ঠিক, তবে একটি ফাঁদ আছে: হারানো JavaScript ফাইলের জন্যও সে index.html দেয়। তাই এই সংস্করণটি ব্যবহার করুন। ফাইলটি Vite project-এর public/ ফোল্ডারে রাখলে build-এ dist/-তে চলে আসে।
# বাজারঘর SPA fallback
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
# assets/ ফোল্ডারের হারানো ফাইল আসল 404 পাবে
RewriteRule ^assets/ - [L]
# বাস্তব ফাইল বা ফোল্ডার থাকলে সেটিই, না থাকলে index.html
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
<IfModule mod_headers.c>
<FilesMatch "\.html$">
Header set Cache-Control "no-cache"
</FilesMatch>
<FilesMatch "\.(js|css)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
</IfModule># ইন্টারনেটের সরল নিয়ম (assets লাইন ছাড়া)
200 text/html /products/3
200 text/html /assets/orders-OLD123.js ← ফাঁদ
200 text/html /nope
# ওপরের .htaccess
200 text/html /products/3
404 text/html; charset=iso-8859-1 /assets/orders-OLD123.js
200 text/javascript /assets/index-CfHZm8TN.js
$ curl -si …/products/3 → Cache-Control: no-cache
$ curl -si …/assets/index-… → Cache-Control: public, max-age=31536000, immutable
ফাঁদটি কখন কামড়ায়? নতুন version আপলোড করলে পুরোনো chunk-এর নাম হারিয়ে যায় (পর্ব ০৫-এর content hash). যে ক্রেতার tab আগে থেকে খোলা, তিনি ক্লিক করলে পুরোনো নাম চাইবেন। আমরা chunk সরিয়ে Edge-এ এটি করে দেখলাম। assets লাইন ছাড়া browser HTML পেল, আর বার্তা এলো:
RESP 200 text/html /assets/orders-LK8phY5Y.js
Failed to load module script: Expected a JavaScript-or-Wasm module script but
the server responded with a MIME type of "text/html".
আর assets লাইন থাকলে আসল 404 এলো, যা log-এ সহজে ধরা পড়ে। দুই ক্ষেত্রেই পর্দায় root boundary দেখা গেছে। তাই সেখানে “নতুন version এসেছে, পাতাটি reload করুন” বোতাম রাখা ভালো।
cPanel-এ তোলার ধাপ
npm run buildচালান, তৈরি হবেdist/.- File Manager-এ
public_htmlখুলে Settings থেকে “Show Hidden Files” চালু করুন, নইলে.htaccessচোখে পড়বে না। distফোল্ডারটি নয়, তার ভেতরের সবpublic_html-এ তুলুন। আগে থেকে.htaccessথাকলে না দেখে মুছবেন না।- Browser-এ
/products/3সরাসরি খুলে reload দিন, আরcurl -Iদিয়ে status দেখুন।
Hosting ভেদে নিয়ম আলাদা হতে পারে। mod_rewrite সাধারণত চালু থাকে, তবু 500 error এলে আপনার hosting সহায়তায় জিজ্ঞেস করুন। Nginx-এ একই কাজের রূপ try_files $uri /index.html; (উদাহরণ, এখানে চালানো হয়নি)। অনেক static hosting-এর নিজস্ব rewrite সেটিং থাকে। অপশনগুলোর তুলনা পর্ব ৮২-এ।
Subfolder-এ অ্যাপ
অ্যাপ example.com/shop/-এ বসলে তিন জায়গায় ঠিকানা মেলাতে হয়। Vite-এ base, router-এ basename, আর .htaccess-এ RewriteBase. Vite-এর base ভুলে গেলে index.html /assets/… চায়, যা /shop/-এর বাইরে, তাই আমাদের run-এ 404 আর সাদা পর্দা।
# বিল্ড
npx vite build --base=/shop/
# src/router.tsx: createBrowserRouter-এর দ্বিতীয় argument
{ basename: import.meta.env.BASE_URL }
# /shop/.htaccess-এ বদল
RewriteBase /shop/
RewriteRule . /shop/index.html [L]# সত্যিকারের run, Apache-তে /shop/products/3 সরাসরি খুলে
url /shop/products/3 h1: গামছা
Fallback-এর একটি দাম আছে: যেকোনো ভুল ঠিকানাও এখন HTTP 200 পায়। ওপরের /nope-এ তাই দেখেছেন। পর্দায় “৪০৪” লেখা থাকলেও server বলছে সব ঠিক। Search engine-এর ভাষায় এটি soft 404. Static SPA-তে এর পুরো সমাধান নেই। সত্যিকারের status দরকার হলে SSR বা pre-render লাগবে, যার ভিত্তি আপনি পর্ব ০৩-এ দেখেছেন। Server একদমই ছোঁয়া না গেলে createHashRouter ব্যবহার করা যায়: URL হয় /#/products/3, আর #-এর পরের অংশ server-এ যায় না। তবে URL দেখতে খারাপ, আর SEO-র জন্য ভালো নয়।
আরও গভীরে: Data router ভেতরে একটি ছোট state machine: প্রতিটি navigation একটি AbortController-সহ Request বানায়, মাঝপথে নতুন ক্লিক এলে আগেরটি বাতিল হয়, তাই পুরোনো loader-এর ফল নতুন পাতায় বসে না। এটি পর্ব ১৩-এর race condition নিজে সামলানোর একই ধারণা। মেলা route-গুলোর loader একসাথে শুরু হয়, পরপর নয়। ওপরের action-এর log-এ [loader] root-এর ঠিক পরেই [loader] product 3 start এসেছে, মাঝে root শেষ হওয়ার অপেক্ষা নেই। Screen reader পাতা বদল নিজে থেকে টের পায় না, তাই নতুন পাতায় document.title বদলানো আর focus নেওয়া আপনার দায়িত্ব।
পরীক্ষা ও ইন্টারভিউতে: Client-side routing কীভাবে কাজ করে? Link ক্লিক থামিয়ে pushState, তারপর route মিলিয়ে React দিয়ে আঁকা; Back-এ popstate. Reload-এ 404 কেন? Server-এ ওই নামে ফাইল নেই, SPA fallback দরকার। Loader আর useEffect-এর পার্থক্য? Loader render-এর আগে চলে, useEffect render-এর পরে। Link আর a tag? a পুরো পাতা load করে, Link করে না। Protected route কি নিরাপদ? না, শুধু UI; যাচাই server-এ।
১০ · AI-এর যুগে React Router
Routing-এ AI-এর ভুলের প্রধান কারণ বয়স। এই library-র API কয়েকবার বড় করে বদলেছে, আর পুরোনো কোডের নমুনা ইন্টারনেটে অনেক বেশি। AI তাই আত্মবিশ্বাসের সাথে v5-এর কোড লেখে, যা v8-এ চলবেই না। আমরা নিজেরা Switch, useHistory আর json import করে tsc চালিয়ে দেখেছি:
error TS2305: Module '"react-router"' has no exported member 'Switch'.
error TS2305: Module '"react-router"' has no exported member 'useHistory'.
error TS2305: Module '"react-router"' has no exported member 'json'.
| AI যা লেখে | সমস্যা | কী করবেন |
|---|---|---|
import … from "react-router-dom" | v8-এ এটি ব্যবহার হয় না; npm-এ 7.x-এই থেমে আছে | react-router আর react-router/dom |
<Switch>, useHistory, component= | v5-এর API, এখন নেই | <Routes>, useNavigate, element |
return json({ … }) | এখন আর export হয় না | সাধারণ object, অথবা data() |
<BrowserRouter>-এর ভেতরে useLoaderData | Run-এ error: useLoaderData must be used within a data router | createBrowserRouter |
| Route পাহারাই একমাত্র যাচাই | API খোলা, যে কেউ ডেটা নিতে পারে | Server-এ authorization |
navigate(searchParams.get("next")) | Open redirect-এর ঝুঁকি | শুধু ভেতরের path মানা |
| Hosting-এর কথা না বলা | Deploy-এর পরে 404 | Deep link সরাসরি খুলে পরীক্ষা |
AI-কে version আর mode বলে দিন, আর তাকে অনুমান করতে না দিয়ে docs-এর সাথে মিলিয়ে নিন:
AI-এর লেখা routing কোড নেওয়ার আগে
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১History API আসার আগে SPA কীভাবে URL সামলাত? #!-এর ইতিহাস থেকে কী শেখার আছে?
প্রথম দিকের AJAX অ্যাপের সমস্যা ছিল অদ্ভুত: পর্দা বদলায়, কিন্তু URL একই। Back চাপলে সাইট থেকেই বেরিয়ে যেত, আর কোনো নির্দিষ্ট জায়গার link দেওয়া যেত না। ডেভেলপাররা একটি ফাঁক খুঁজে পেলেন। URL-এর #-এর পরের অংশ (fragment) browser server-এ পাঠায় না, কিন্তু সেটি বদলালে ইতিহাসে নতুন ঘর হয়, আর hashchange event পাওয়া যায়। অর্থাৎ পাতার ভেতরের জায়গায় লাফানোর জন্য বানানো একটি সুবিধাকে routing-এর কাজে লাগানো হলো।
কিন্তু crawler #-এর পরের অংশ দেখত না, তাই সব পাতা তার কাছে একটিই। ২০০৯ সালে Google একটি রীতি প্রস্তাব করে: #! দেখলে সে একটি বিশেষ URL-এ HTML চাইবে। অনেক বড় সাইট এই hashbang নেয়। পরে ২০১২ সালে Twitter জানায়, তারা #! ছেড়ে দিচ্ছে, কারণ browser-কে আগে JavaScript নামিয়ে তবেই বুঝতে হচ্ছিল কোন ডেটা আনতে হবে, ফলে প্রথম পাতা ধীর হচ্ছিল। Google-ও ২০১৫ সালে ওই রীতি বাতিল করে, কারণ ততদিনে তাদের crawler অনেকটা JavaScript চালাতে পারে।
HTML5-এর History API এই ঝামেলা কাটায়: সাধারণ দেখতে URL, কিন্তু server-এ না গিয়েই বদলায়। তবে দাম হলো, এখন সেই URL সরাসরি server-এ পৌঁছায়, তাই server-কেও SPA fallback শিখতে হয়। # ব্যবস্থায় এই সমস্যা ছিল না। আজও createHashRouter টিকে আছে সেইসব জায়গার জন্য, যেখানে server নিজের হাতে নেই, যেমন কিছু সাধারণ ফাইল-hosting অথবা ফোনের অ্যাপের ভেতরে খোলা ওয়েব পাতা।
মূল উপলব্ধি: URL শুধু একটি ঠিকানা নয়, এটি ওয়েবের সবচেয়ে পুরোনো চুক্তি: link দিলে একই জিনিস পাওয়া যাবে। ভালো SPA সেই চুক্তি রাখে। আর একটি সমস্যার সমাধান প্রায়ই আরেক জায়গায় নতুন দায়িত্ব তৈরি করে, যেমন এখানে browser থেকে সমস্যা সরে গেল server-এ।
প্র ০২Loader কি useEffect-এর fetch-এর চেয়ে সবসময় ভালো? আসল পার্থক্যটা কোথায়?
পার্থক্য হলো কে, কখন ডেটা চায়। useEffect-এ component আগে পর্দায় আসে, তারপর নিজে ডেটা চায়। একে অনেকে “fetch-on-render” বলেন। ধরুন বাবা layout-এর ডেটা ২০০ ms, আর সন্তান পাতার ডেটা ৩০০ ms নেয়। সন্তান তখনই render হবে যখন বাবা ডেটা পেয়েছে, তাই মোট ২০০ + ৩০০ = ৫০০ ms, আর পর্দায় দুবার spinner. এটি network waterfall.
Router route গুলো আগে থেকেই জানে, তাই URL দেখেই দুটি loader একসাথে শুরু করতে পারে। মোট সময় হয় বড়টির সমান, ৩০০ ms. এই সংখ্যা উদাহরণের জন্য ধরা, কিন্তু যুক্তিটি সাধারণ। এর সাথে আসে আরও তিনটি সুবিধা: পাতা বদলের সময় আগের পাতা থাকে, দ্রুত ক্লিকে পুরোনো request বাতিল হয়, আর ভুল হলে error boundary নিজে ধরে।
তবে loader সবকিছুর উত্তর নয়। URL-এর সাথে বাঁধা নয় এমন ডেটা, যেমন খোলা dropdown-এর জন্য আনা তালিকা, component-এর কাছেই মানায়। Loader-এ cache নেই বলে Back চাপলে আমাদের run-এ catalog loader আবার চলেছে, ৩০০ ms-ও আবার লেগেছে। ঘন ঘন বদলানো ডেটা, background refresh আর পাতায় পাতায় cache ভাগ করার জন্য TanStack Query বা ওই ধরনের library দরকার পড়ে। আবার loader শেষ না হওয়া পর্যন্ত পুরোনো পাতা থাকে, ফলে ডেটা ধীর হলে ক্লিকটি অকেজো লাগতে পারে। তাই pending UI বাধ্যতামূলক।
Declarative mode-এর সাথে Query, Data mode-এর loader, অথবা দুটি একসাথে, তিনটি পথই বাস্তবে দেখা যায়। ছোট দোকান অ্যাপে loader-ই যথেষ্ট। অনেক widget-ওয়ালা dashboard-এ Query বেশি মানায়।
মূল উপলব্ধি: ডেটা আনার সিদ্ধান্ত যত ওপরে, যত আগে নেওয়া যায়, তত কম অপেক্ষা হয়। Router URL জানে, তাই সে component-এর চেয়ে আগে জানে কী লাগবে। কিন্তু cache করা অন্য কাজ, সেটি আলাদা যন্ত্রের।
প্র ০৩একই library কয়েকবার এত বড়ভাবে বদলাল কেন? এই ইতিহাস থেকে dependency বাছাইয়ে কী শিখবেন?
npm-এর release তারিখ ধরে পথটি দেখুন। v4 এলো ২০১৭ সালে, route হলো সাধারণ component. v6 এলো নভেম্বর ২০২১-এ, relative route আর ranking নিয়ে, Switch গেল। ২০২২-এর সেপ্টেম্বরে 6.4-এ এলো data router, যা Remix framework-এর loader আর action-এর ধারণা নিয়ে আসে। নভেম্বর ২০২৪-এ v7 Remix-কে নিজের ভেতরে মিশিয়ে Framework mode তৈরি করে। আর জুন ২০২৬-এ v8 এসে react-router-dom বাদ দেয়, আর middleware flag ছাড়াই পাওয়া যায়।
প্রতিটি বদলের পেছনে সত্যিকারের কারণ ছিল। Render-এর সময়ে route জানলে আগে থেকে ডেটা আনা যায় না, তাই route-কে render-এর বাইরে আনতে হলো। Server rendering চাইলে browser আর server-এর route এক হওয়া দরকার। কিন্তু ব্যবহারকারীর কাছে এর অর্থ বারবার কোড বদলানো, পুরোনো টিউটোরিয়াল অকেজো হওয়া, আর এই সমালোচনা কমিউনিটিতে অনেক পুরোনো। Maintainer-রা এখন future flag ব্যবহার করেন: নতুন আচরণ আগে থেকেই পুরোনো version-এ একটি flag দিয়ে চালু করে দেখা যায়, তারপর বড় upgrade প্রায় সাধারণ কাজ হয়। v7 থেকে v8-এর গাইড ঠিক এই পথ দেখায়, যদিও Node আর React-এর ন্যূনতম version-ও বাড়ে।
আপনার জন্য শিক্ষা তিনটি। এক, route-এর কোড অ্যাপের মেরুদণ্ড, তাই library-র ওপর একটি পাতলা নিজস্ব স্তর রাখা (যেমন requireUser, safeNext) পরে সুবিধা দেয়। দুই, upgrade guide পড়ুন, আন্দাজে নয়। তিন, যেকোনো টিউটোরিয়াল বা AI উত্তরের আগে version দেখুন। v5 আর v8-এর কোড পাশাপাশি একই রকম আত্মবিশ্বাসে পাওয়া যায়, অথচ একটি আজ চলেই না।
মূল উপলব্ধি: Library বেছে নেওয়া মানে তার বদলের গতিও বেছে নেওয়া। ধারণা (nested route, URL state, render-এর আগে ডেটা) অনেক দিন টেকে, API-র নাম টেকে না। তাই ধারণা শিখুন, আর নামের জন্য version-এর docs খুলুন।
প্র ০৪Route পাহারা নিরাপত্তা না হলে তা রাখব কেন? ঠিক কোথায় কী যাচাই করা উচিত?
দুটি আলাদা প্রশ্ন আছে। “এই মানুষকে কোন পাতা দেখানো সুবিধাজনক?” এটি UX. “এই মানুষ এই ডেটা নিতে পারেন কি?” এটি authorization. প্রথমটির উত্তর browser দিতে পারে, দ্বিতীয়টির পারে না, কারণ browser ব্যবহারকারীর নিজের যন্ত্র। JavaScript bundle যে কেউ নামাতে, পড়তে, বদলাতে পারেন। user মানটিও DevTools-এ বদলানো যায়।
ধরুন “বাজারঘর”-এর /account/orders পাতা পাহারায়, কিন্তু API /api/orders/1042 শুধু দেখে login আছে কি না, অর্ডারটি তাঁর কি না দেখে না। তাহলে রিনা সংখ্যা বদলে অন্যের অর্ডার পড়তে পারবেন, আর কোনো route guard তাতে বাধা দেবে না, কারণ তিনি UI-ই ব্যবহার করছেন না। OWASP-এর তালিকায় Broken Access Control অনেক বছর ধরে ওপরের দিকে থাকার কারণ এই জাতের ভুল। এখানে যাচাই হবে server-এ, প্রতিটি request-এ, session বা token থেকে ব্যবহারকারী চিনে, ডেটার মালিকানা মিলিয়ে।
তাহলে route পাহারা রাখব কেন? কারণ সেটি অভিজ্ঞতা ভালো করে। Login-এর পাতায় ঠিক সময়ে নিয়ে যায়, ফিরে আসার ঠিকানা মনে রাখে, অকারণে API ডেকে 401 খাওয়া আটকায়, দরকারি নয় এমন chunk নামতে দেয় না। আমাদের run-এ login ছাড়া orders chunk নামেনি, তবু মনে রাখবেন ওই ফাইলটি সরাসরি URL দিয়ে যে কেউ পেতে পারেন। তাই bundle-এ গোপন কিছু রাখা যাবে না। Admin পাতার ডেটা না, শুধু কাঠামো থাকলে ফাঁস হলেও ক্ষতি কম।
আরেকটি বাস্তব কথা: session শেষ হয়ে গেলে API 401 দেবে, তখন loader থেকে redirect করুন। Redirect-এর next মান URL থেকে আসে, মানে আক্রমণকারীও বসাতে পারে, তাই open redirect আটকান। আর URL-এ token রাখবেন না।
মূল উপলব্ধি: ফ্রন্টএন্ডের যাচাই হলো দোকানের সামনে সিকিউরিটি গার্ডের “স্যার, এদিকে”, আর backend-এর যাচাই হলো ভল্টের তালা। গার্ড ভদ্রভাবে পথ দেখান, তালা ছাড়া টাকা নিরাপদ নয়। দুটিই লাগে, কাজ দুটি আলাদা।
প্র ০৫“সব ঠিকানায় index.html” কৌশলের গোপন খরচ কী? কখন SPA ছেড়ে SSR বা pre-render ভাবা উচিত?
কৌশলটি অসাধারণ সস্তা। একটি static ফোল্ডার, একটি .htaccess, আর সাধারণ shared hosting-এই পুরো অ্যাপ চলে। বাংলাদেশের অনেক ছোট ব্যবসার জন্য এটি খুব বাস্তব পথ, কারণ Node server চালানোর ঝামেলা নেই, সব সময় চালু রাখার দুশ্চিন্তাও নেই, আর একজন মানুষই FTP দিয়ে পুরো সাইট তুলতে পারেন। কিন্তু খরচ চারটি জায়গায় লুকিয়ে থাকে, আর সেগুলো সাধারণত পরীক্ষার সময়ে নয়, ব্যবহারকারী আসার পরে চোখে পড়ে।
- ভুল status: সব URL 200. Search engine একে soft 404 ধরতে পারে, আর মনিটরিং টুল ভাঙা link-কে ঠিক ভাবে। আমাদের Apache run-এ
/nopeঠিক এই ফল দিয়েছে। - প্রথম পাতার দেরি:
/products/3চাইলেও আগে খালি HTML, তারপর 103 kB gzip JavaScript, তারপর loader-এর ডেটা। ধীর সংযোগে প্রথম অর্থবহ পর্দা অনেক পরে আসে। - SEO আর link preview: Facebook বা WhatsApp-এর bot সাধারণত JavaScript চালায় না, তাই সব পণ্যের preview-তে একই শিরোনাম আসতে পারে। Google JavaScript চালাতে পারে, তবে সবসময় একই গতিতে নয়।
- Deploy-এর ঝুঁকি: পুরোনো tab পুরোনো chunk চাইবে, যা আমরা Edge-এ ঘটিয়ে দেখেছি। সমাধান হলো পুরোনো chunk কিছুদিন রেখে দেওয়া, অথবা error-এ reload করতে বলা।
সিদ্ধান্তের প্রশ্ন একটি: পাতাগুলো কি login-এর পেছনে, নাকি search থেকে মানুষ আসে? Admin panel, ড্যাশবোর্ড, ভেতরের সফটওয়্যারে SPA চমৎকার। দোকানের পণ্যের পাতা, ব্লগ, সংবাদ, যেখানে SEO আর link preview-ই ব্যবসা, সেখানে SSR বা pre-render ভাবা উচিত। React Router-এর Framework mode-এই pre-render আছে, আবার Next.js-ও পথ। একটি মাঝামাঝি নকশাও প্রচলিত: বিক্রির পাতা pre-render, অ্যাকাউন্ট অংশ SPA.
মূল উপলব্ধি: Rendering-এর কৌশল প্রযুক্তির ফ্যাশন নয়, ট্রাফিকের উৎস আর hosting-এর বাজেটের হিসাব। আপনার ক্রেতারা কোন পথে আসেন, সেটি আগে জানুন, তারপর কৌশল বাছুন। পর্ব ০৩-এর মানচিত্রটি এখানেই কাজে লাগে।
অনুশীলন
নিজে করে তারপর উত্তর মেলান। সংখ্যার ক্ষেত্রে হিসাবের ধাপও লিখুন।
-
Bundle-এর হিসাব: একটি অ্যাপের এক-ফাইলের bundle 420 kB (gzip). Route ধরে ভাগ করলে main 120 kB, আর admin chunk 300 kB হয়। ২% ভিজিটর admin খোলেন, গতি ধরুন 10 Mbps (সেকেন্ডে 10000 kilobit). (ক) ভিজিটর-প্রতি গড় ডাউনলোড কত? (খ) প্রথম load-এ দুই ক্ষেত্রে সময় কত?
(ক) গড় = 120 + 0.02 × 300 = 120 + 6 = 126 kB. আগে ছিল 420 kB, মানে 294 kB কম, প্রায় ৭০% সাশ্রয়।
(খ) এক ফাইলে: 420 × 8 ÷ 10000 = 0.336 s. ভাগ করলে: 120 × 8 ÷ 10000 = 0.096 s. Admin ব্যবহারকারীর প্রথম প্রবেশে 300 × 8 ÷ 10000 = 0.24 s বাড়তি লাগবে, যা বাকি ৯৮% মানুষের দিতে হবে না। হিসাবে latency, parse আর execution ধরা হয়নি।
-
ফল আন্দাজ করুন: Root route-এ
rootLoader, তার সন্তানproducts/:id-এproductLoader. কোনোshouldRevalidateনেই, পর্দায় এখন/products/3. ক্রমে তিনটি ঘটনা: (১)/products/4-এ Link ক্লিক (২)/products/4?ref=fb-তে Link ক্লিক (৩) সেই পাতায়<Form method="post">submit, action{ ok: true }ফেরাল। প্রতিবার কোন loader চলবে?(১) শুধু
productLoader, কারণ root-এর নিজের param বদলায়নি।(২)
rootLoaderআরproductLoaderদুটিই, কারণ search param বদলালে পর্দায় থাকা loader-গুলোও আবার চলে। আসল run-এ?cat=home-এ ঠিক এটি ঘটেছে।(৩) আবারও দুটিই, কারণ action সফল হয়েছে। Action যদি 400 ফেরাত, তবে একটিও চলত না।
-
নকশা ও সমালোচনা: AI বাজারঘরের অ্যাডমিন অংশের জন্য লিখল: প্রতিটি অ্যাডমিন পাতায় নিজে
HeaderআরSidebarবসানো,localStorage.isAdmin === "true"হলে পাতা দেখানো, আর filteruseState-এ রাখা। কোথায় কোথায় সমস্যা? কীভাবে বদলাবেন?Layout: কপি করা header মানে পাতা বদলে সব আবার mount হয়, state হারায়। সমাধান
adminনামে একটি layout route আর<Outlet />.পাহারা:
localStorageযে কেউ DevTools-এ বদলাতে পারেন, তাই এটি শুধু UX হিসেবেও দুর্বল। সার্ভার থেকে পাওয়া ব্যবহারকারীর তথ্য middleware-এ দেখে redirect, আর প্রতিটি admin API-তে server-এর যাচাই।Filter: Search params-এ নিন, যাতে কাউকে link পাঠানো যায়, Back-এ ঠিক থাকে।
বাড়তি: Admin route-গুলো
lazy, নিজেরErrorBoundary, যাতে ক্রেতার bundle-এ অ্যাডমিনের কোড না যায়, আর একটি পাতা ভাঙলে menu থাকে। -
Deploy যাচাই: আপনার SPA
example.com-এ তোলা হলো। তিনটিcurlকমান্ড লিখুন যা দেখাবে (ক) deep link ঠিক আছে (খ) হারানো chunk আসল 404 পায় (গ) hash-ওয়ালা ফাইল দীর্ঘ cache পায়। প্রত্যাশিত ফলও লিখুন।curl -sI https://example.com/products/3 # 200, text/html, no-cache curl -sI https://example.com/assets/nai-abc123.js # 404 curl -sI https://example.com/assets/index-XXXX.js # 200, text/javascript, # max-age=31536000, immutable-Iশুধু header আনে,-sঅগ্রগতির লেখা লুকায়।XXXX-এর জায়গায়dist/assets-এর আসল নাম বসান। দ্বিতীয়টি 200 দিলে বুঝবেনassetsলাইনটি কাজ করছে না, অথবা host এটি অন্যভাবে সামলাচ্ছে। CDN সামনে থাকলে তার cache-ও আলাদা করে দেখতে হয়।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমReact Router-এর Data mode-এ একটি Link ক্লিকের ধাপগুলো সাজান (▲▼ চেপে সরান)
- ব্যবহারকারী
<Link>-এ ক্লিক করলেন preventDefaultbrowser-এর page load থামাল- Router route মিলিয়ে loader শুরু করল,
navigation.stateহলোloading - Loader শেষে
pushState, URL বদলাল - নতুন component loader-এর ডেটা নিয়ে render হলো
history.pushState() ডাকলে কী ঘটে?
- Browser server থেকে নতুন HTML আনে
- Address bar-এর URL আর ইতিহাস বদলায়, পাতা load হয় না
- popstate event এসে পর্দা নিজে থেকে বদলায়
- ইতিহাসের চলতি ঘরটি মুছে যায়
cPanel-এ তোলা React SPA-তে হোম পাতা চলে, কিন্তু /products/3 সরাসরি খুললে Apache 404 দেয়। এর কারণ কী?
- React Router-এ route লেখা ভুল
- Build ফাইলের নামে hash আছে
- Server-এ ওই নামে ফাইল নেই, আর SPA fallback বসানো হয়নি
- Hosting-এ Node.js নেই
Route tree: account (AccountLayout, ভেতরে <Outlet />), তার সন্তান orders আর reports. ব্যবহারকারী /account/orders থেকে /account/reports-এ গেলে AccountLayout-এর useState-এর মানের কী হবে?
- মান থেকে যাবে, কারণ শুধু Outlet-এর ভেতরের অংশ বদলায়
- মান মুছে শুরুর মানে ফিরবে
- মান URL-এ চলে যাবে
- React error দেবে
একটি দোকানের পণ্য তালিকার category আর sort, রিনা চান link পাঠালে বোনও একই তালিকা দেখুন। এগুলো কোথায় রাখা উচিত?
- Component-এর useState-এ
- localStorage-এ
- একটি global store-এ
- URL-এর search params-এ
React Router-এর Data mode-এ products/:id route-এর loader সম্পর্কে কোনটি সত্য?
- Component render হওয়ার পরে useEffect-এর মতো চলে
- Component render হওয়ার আগে চলে, component useLoaderData দিয়ে ফল পড়ে
- শুধু Node.js server-এ চলে
- প্রথমবার চলার পর ফল চিরকাল cache থাকে
একটি login form শেষে navigate(params.get("next")) লেখা। next মান URL থেকে আসে, আর কোনো যাচাই নেই। প্রধান ঝুঁকি কী, আর বড় ছবিতে route পাহারা সম্পর্কে কোনটি ঠিক?
- Bundle বড় হবে; route পাহারাই আসল নিরাপত্তা
- Hash URL দিলেই সব ঝুঁকি শেষ
- Open redirect-এর ঝুঁকি; route পাহারা শুধু UX, আসল যাচাই server-এ
- SEO নষ্ট হবে; client-এর যাচাইই যথেষ্ট
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- SPA-র routing মানে click থামানো +
pushState+ route মেলানো + React render, আর Back-এpopstate. - React Router 8-এর তিন mode: Declarative, Data, Framework; import
react-routerথেকে। - Nested route + Outlet layout বাঁচিয়ে রাখে; share করার মতো state রাখুন URL-এ।
- Loader render-এর আগে চলে, action-এর পরে revalidation হয়; প্রতিটি route-এর নিজের error boundary রাখুন।
- Production-এ SPA fallback লাগে; route পাহারা UX, আসল তালা server-এ।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২১ · বড় form — React Hook Form, Zod আর দুই দিকের validationপরবর্তী পর্বঅনেক ঘরের form-কে সামলানো।
- ফুল-স্ট্যাক ওয়েব ফ্রেমওয়ার্ক · কোর্স হোম৯৩টি পর্বপুরো কোর্সের রোডম্যাপ।
- Broken Access Controlনিরাপত্তাRoute পাহারার বাইরে আসল authorization.
- HTTPভিত্তিStatus code, header আর cache কীভাবে কাজ করে।
- Accessibilityপাশের ট্র্যাকLink, বোতাম আর focus-এর সঠিক ব্যবহার।