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

Server state — TanStack Query, cache, mutation আর optimistic update

Server State — TanStack Query, Caching, Mutations and Optimistic Updates

Taught in Bangla, this lesson explains why server state is remote, shared and possibly stale, counts the requests useEffect fetching makes, introduces QueryClient, useQuery and query keys as cache addresses, staleTime versus gcTime, refetching, retries and loading and error UI, useMutation with invalidation, optimistic updates with rollback, pagination, infinite queries, prefetch and DevTools, and when SWR or other options fit.

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

  • Server state কেন client state থেকে আলাদা, আর useEffect-এ আনার সীমা কোথায়
  • QueryClient, useQuery আর query key দিয়ে Vite প্রজেক্টে ডেটা আনা
  • staleTime আর gcTime-এর পার্থক্য, আর কোন ঘটনায় refetch হয় তা হিসাব করে বলতে পারা
  • useMutation, invalidation আর rollback সহ optimistic update লেখা
  • Pagination, infinite query, prefetch আর DevTools ব্যবহার; SWR-এর সাথে তুলনা

১ · Server state আলাদা কেন: দূরের, ভাগ করা, পুরোনো হতে পারে

“বাজারঘর”-এর হোম পাতায় চালের দাম দেখাচ্ছে ৪২০ টাকা। রিনা পাতাটি খোলা রেখে চা খেতে গেলেন। এর মধ্যে দোকানের ম্যানেজার দাম করলেন ৪৫০ টাকা। রিনার পর্দায় এখনো ৪২০। তাঁর browser কি ভুল করেছে? না। Browser শুধু জানে না যে সার্ভারে কিছু বদলেছে।

পর্ব ১৬-এ আপনি state-কে local, lifted, global আর server, এই চার ভাগে দেখেছেন। শেষ ভাগটি বাকিদের মতো নয়। Client state আপনার নিজের: drawer খোলা কি না, কোন ট্যাব বাছাই করা। Server state ধার করা: আপনি শুধু একটি কপি হাতে রাখছেন।

Client stateServer state
আসল কপি কোথায়Browser-এর memory-তেদূরের সার্ভার আর database-এ
কে বদলাতে পারেশুধু এই ব্যবহারকারীসবাই: অন্য ক্রেতা, ম্যানেজার, background কাজ
পড়তে কত সময়সাথে সাথেNetwork-এর সময় লাগে, ব্যর্থও হতে পারে
পুরোনো হতে পারে?নাহ্যাঁ, আসার পরের মুহূর্ত থেকেই
উদাহরণSearch box-এর লেখা, dark modeপণ্যের তালিকা, স্টক, অর্ডারের অবস্থা

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

এই “কাগজ” আসলে একটি cache. আর পুরো সমস্যাটি পরিচিত: কম্পিউটার বিজ্ঞানে cache নতুন রাখা কঠিন বলেই পরিচিত। Backend-এর cache নিয়ে পর্ব ৪৪-এ কাজ করবেন, তত্ত্ব আছে সিস্টেম ডিজাইনের eviction ও invalidation পর্বে। এখানে cache-টি থাকে browser-এর memory-তে, আর তার হিসাব রাখে একটি library.

মূল ধারণা

Server state দেখানো মানে “ডেটা আনা” নয়। মানে একটি দূরের সত্যের স্থানীয় কপি পরিচালনা করা: কখন আনবেন, কতক্ষণ রাখবেন, কখন বাতিল করবেন, দুজন একসাথে চাইলে কী হবে, ব্যর্থ হলে কী হবে।

২ · useEffect-এ আনার সীমা: অনুরোধ গুনে দেখা

পর্ব ১৩-এ আপনি effect-এ ডেটা এনেছেন: loading, error আর পুরোনো উত্তর বাতিল, এই তিন দায়িত্ব নিজের হাতে। পর্ব ১৪-এ সেই কোড একটি custom hook-এ তুলেছেন। কোডটি ঠিক আছে, শুধু একটি জিনিস নেই: hook-এর প্রতিটি ব্যবহার আলাদা, তাদের মধ্যে ভাগ করা কোনো স্মৃতি নেই।

function useProductsEffect() {
  const [data, setData] = useState(null);
  useEffect(() => {
    let ignore = false;
    getJson('/api/products').then((d) => { if (!ignore) setData(d); });
    return () => { ignore = true; };
  }, []);
  return data;
}

এবার সত্যিকারের একটি প্রজেক্টে মাপা যাক। একটি ছোট node:http API প্রতিটি অনুরোধ গুনে রাখে, আর Vite-এর React অ্যাপে একই পাতায় দুটি component একই তালিকা চায়: header-এর পণ্য সংখ্যা আর মূল তালিকা। অনুরোধ Vite-এর proxy হয়ে API-তে যায়। Edge-এ Playwright দিয়ে পাতা খুলে API-র গণনা পড়া হয়েছে। সত্যিকারের run; লেখার সময়ে TanStack Query 5.105.1, SWR 2.5.1, React 19.3.1, Vite 8.3.5, Node.js 24.15.0.

$ cd ~/bazarghar-query/api && node server.mjs
API চলছে: http://127.0.0.1:5719

$ cd ~/bazarghar-query/web
$ npm install @tanstack/react-query @tanstack/react-query-devtools swr
$ npm run dev
  VITE v8.3.5  ready in 132 ms
  ➜  Local:   http://127.0.0.1:5720/
দুটি component, একই ডেটাnpm run dev (StrictMode)Production build
useEffect custom hook৪টি অনুরোধ২টি অনুরোধ
useQuery, signal ছাড়া১টি১টি
useQuery, signal সহ২টি (প্রথমটি বাতিল হয়)১টি
SWR১টি১টি

সারির তৃতীয়টি বিস্ময়কর, তাই খেয়াল করুন। StrictMode dev-এ component-কে mount, unmount, আবার mount করে। Query function signal ব্যবহার করলে শেষ দর্শক চলে যাওয়ার মুহূর্তে TanStack Query অনুরোধটি বাতিল করে, তাই দ্বিতীয় mount-এ নতুন অনুরোধ যায়। এটি শুধু dev-এর দৃশ্য, ভুল নয়। Production-এ একটিই। তাই অনুরোধ গোনার সিদ্ধান্ত production build-এ নিন। এই পর্বের বাকি log-গুলো vite preview-এর production build থেকে নেওয়া।

হিসাবটি দুই component-এ ছোট দেখায়। কিন্তু একই তালিকা পাঁচটি component চাইলে useEffect-এ ৫টি অনুরোধ যায়, আর প্রতিবার পাতা বদলে ফিরলে আবার ৫টি, সাথে আবার spinner. যা নেই তার তালিকা লম্বা:

  • ভাগ করা cache: দুই component একই ডেটা আলাদাভাবে আনে, আর তাদের মান সাময়িকভাবে আলাদাও হতে পারে।
  • পুরোনো হওয়ার ধারণা: পাতা ফিরে এলেও জানার উপায় নেই ডেটা কতক্ষণ আগের।
  • স্বয়ংক্রিয় নতুন করা: Tab-এ ফিরলে, network ফিরলে কিছুই হয় না।
  • Retry: একটি ব্যর্থতাই পর্দায় error.
  • Mutation-এর পরে সংযোগ: অর্ডার করলে কোন কোন তালিকা বদলাবে, তা হাতে লিখতে হয়।

এই সব হাতে লেখা সম্ভব। কিন্তু লিখতে গেলে আপনি একটি ছোট cache library-ই বানাবেন। React-এর নথিও effect-এ হাতে লেখার বদলে framework বা এমন library ব্যবহারের পরামর্শ দেয়।

Global store কি এটি পারে না? পর্ব ১৮-এর store-এ তালিকা রাখলে ভাগ করার সমস্যা মেটে। বাকি সব, যেমন stale হিসাব, refetch, retry, dedupe, আপনাকেই লিখতে হয়। Store সাধারণ বাক্স, আর server state-এর দরকার একজন হিসাবরক্ষক। Redux Toolkit-এর সাথে আসা RTK Query ঠিক এই কারণেই তৈরি হয়েছে।

৩ · TanStack Query-র ধারণা: QueryClient, useQuery, query key

TanStack Query (আগের নাম React Query) নিজে ডেটা আনে না। ডেটা আনে আপনার লেখা ফাংশন। Library রাখে সেই ফাংশনের ফলাফলের হিসাব। তিনটি অংশ মনে রাখুন।

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App.jsx';

const queryClient = new QueryClient();   // পুরো অ্যাপের একটিই cache

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </StrictMode>,
);
import { useQuery } from '@tanstack/react-query';
import { getJson } from './api.js';

export function ProductList() {
  const { data, isPending, isError, error } = useQuery({
    queryKey: ['products', 'list'],
    queryFn: ({ signal }) => getJson('/api/products', { signal }),
  });

  if (isPending) return <p>লোড হচ্ছে…</p>;
  if (isError) return <p role="alert">{error.message}</p>;
  return <ul>{data.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}
  • QueryClient: কেন্দ্রীয় cache. Component-এর বাইরে একবার তৈরি হয়, যাতে render-এ নতুন না হয়। Provider দিয়ে পুরো গাছে পৌঁছায়।
  • Query key: একটি array, যা cache-এর ঘরের ঠিকানা। Key একই হলে ঘর একই।
  • Query function: Promise ফেরত দেয়। পাওয়া signal fetch-এ দিলে অদরকারি অনুরোধ বাতিল হয়। পর্ব ১৩-এর AbortController-এর কাজটি এখানে library করে।
  • useQuery: Component-কে সেই ঘরের দর্শক (observer) বানায়। ঘর বদলালে দর্শক component আবার render হয়।

তিনটি component, দুটি cache-এর ঘর, দুটি অনুরোধ

Components observe cache entries; entries talk to the network

QueryClient-এর ভেতরে query key অনুযায়ী cache-এর ঘর HeaderuseQuery ListuseQuery DetailuseQuery QueryClient: cache-এর ঘরগুলো products, list data: ৬টি পণ্য dataUpdatedAt observer: ২ জন products, detail, 7 data: ১টি পণ্য observer: ১ জন ১টি GET ১টি GET API সার্ভার আসল সত্য এখানে
ছবি ১৯.১ — Component সরাসরি network-এ যায় না। সে একটি ঘরের দর্শক হয়, আর ঘরই ঠিক করে অনুরোধ যাবে কি না।

Key-এর নিয়ম: ফাংশন যা ব্যবহার করে, সব key-তে

Query function যে মানের ওপর নির্ভর করে, তার সবই key-তে থাকতে হবে, ঠিক dependency array-এর মতো। ['products', page] লিখলে page বদলালে নতুন ঘর, নতুন অনুরোধ। Key-তে page না থাকলে সব পাতা একই ঘরে পড়বে আর পর্দায় ভুল পাতা থাকবে। TanStack-এর ESLint plugin এই ভুল ধরে দেয়।

Key-এর সুবিধাটি আরও বড়। পর্ব ১৩-এর race condition মনে আছে? “চা” লিখে “চাল” লিখলে পুরোনো ধীর উত্তর নতুনটির ওপরে বসে যেত। এখানে প্রতিটি key-এর ঘর আলাদা, তাই দেরিতে আসা উত্তর নিজের ঘরেই বসে। পর্দা আছে এখনকার ঘরে। API-তে “চা” ইচ্ছা করে 600 ms ধীর করা ছিল; run-এ পর্দায় “চাল”-এর ফলই থেকেছে, আর “চা”-র অনুরোধ signal দিয়ে client-এর দিক থেকে বাতিল হয়েছে।

নিজে চালান: ২৫ লাইনের শিক্ষণ মডেল

Live block-এ npm package আসে না, আর TanStack Query-র ভেতরটা অনেক বড়। তাই নিচের কোডটি একটি শিক্ষণ মডেল, আসল library নয়। এটি শুধু তিনটি ধারণা ধরে: key অনুযায়ী ঘর, চলমান অনুরোধ থাকলে নতুনটি না পাঠানো, আর simulated clock. চালিয়ে requests দেখুন।

// শিক্ষণ মডেল: ধারণা বোঝার জন্য, আসল TanStack Query-র কোড নয়
let now = 0, requests = 0;          // simulated clock (ms), অনুরোধের গণনা
const LATENCY = 200;                // simulated network delay
const cache = new Map();            // key → { data, updatedAt, pending }

function tick(ms) {                 // ঘড়ি এগোয়, সময় হলে উত্তর cache-এ বসে
  now += ms;
  for (const entry of cache.values()) {
    if (entry.pending && now >= entry.pending.doneAt) {
      entry.data = entry.pending.data;
      entry.updatedAt = entry.pending.doneAt;
      entry.pending = null;
    }
  }
}

function queryModel(key, staleTime) {
  if (!cache.has(key)) cache.set(key, { data: null, updatedAt: -Infinity, pending: null });
  const entry = cache.get(key);
  const stale = now - entry.updatedAt >= staleTime;
  if (stale && !entry.pending) {    // দৌড়ে থাকা অনুরোধ থাকলে নতুনটি যায় না
    requests += 1;
    entry.pending = { doneAt: now + LATENCY, data: `${key} #${requests}` };
  }
  return entry.data ?? 'spinner';
}

const show = (who, key) =>
  console.log(`t=${now} ${who}: ${queryModel(key, 1000)} | requests=${requests}`);

show('Header', 'products');         // miss: অনুরোধ যায়
show('List', 'products');           // একই key: নতুন অনুরোধ নেই
tick(200);
show('List', 'products');           // hit
show('Detail', 'product-1');        // অন্য key: আবার miss

আউটপুট চারটি লাইন: t=0 Header: spinner | requests=1, t=0 List: spinner | requests=1, t=200 List: products #1 | requests=1, t=200 Detail: spinner | requests=2. Header ঘর তৈরি করে অনুরোধ পাঠাল। List একই key চাইল, চলমান অনুরোধ দেখে নতুন কিছু পাঠাল না। ফলে দুজন মিলে ১টি অনুরোধ, এটিই deduplication. t=200-এ উত্তর বসেছে, তাই List সাথে সাথে ডেটা পেল (cache hit). Detail-এর key আলাদা, তাই miss আর দ্বিতীয় অনুরোধ।

৪ · staleTime বনাম gcTime: দুটি আলাদা ঘড়ি

এই দুটি গুলিয়ে ফেলা সবচেয়ে সাধারণ ভুল। দোকানের কাগজ দিয়ে মনে রাখুন। staleTime: সকালে লেখা দর কতক্ষণ বিনা প্রশ্নে বিশ্বাস করবেন। gcTime: কাগজটি কেউ না দেখলে কতক্ষণ পরে ঝুড়িতে ফেলবেন।

  • Fresh: বয়স staleTime-এর কম। ডেটা সরাসরি দেওয়া হয়, কোনো অনুরোধ নয়।
  • Stale: বয়স staleTime ছাড়িয়েছে। ডেটা তবু দেখানো হয়, আর সুযোগ পেলে পেছনে নতুন করে আনা হয়। Stale মানে “ভুল” নয়, মানে “মিলিয়ে দেখার সময় হয়েছে”।
  • Inactive: কোনো observer নেই। এই মুহূর্ত থেকে gcTime-এর ঘড়ি চলে। শেষ হলে ঘর মুছে যায় (garbage collection).

Default দুটি মনে রাখুন: staleTime: 0 আর gcTime ৫ মিনিট। অর্থাৎ ডেটা আসার সাথে সাথেই stale, তবে অব্যবহৃত ঘর ৫ মিনিট থাকে। v4 পর্যন্ত gcTime-এর নাম ছিল cacheTime; নামটি ভুল ধারণা দিত, তাই v5-এ বদলানো হয়েছে।

একটি cache-এর ঘরের জীবন

staleTime counts from the last response; gcTime counts from the last observer leaving

staleTime আর gcTime দুটি আলাদা সময়রেখা ঘড়ি ১: ডেটার বয়স (শেষ উত্তর থেকে) ঘড়ি ২: ঘরের ব্যবহার (শেষ observer থেকে) fresh stale: দেখায়, পেছনে আনে active inactive মুছল staleTime শেষ component গেল gcTime শেষ একটি ঘর একসাথে stale আর active হতে পারে
ছবি ১৯.২ — ঘড়ি দুটি আলাদা জিনিস মাপে। staleTime ডেটার বিশ্বাসযোগ্যতা, gcTime memory-র হিসাব।

নিচের ধাপগুলোর সময় staleTime: 30_000 দিয়ে করা সত্যিকারের run-এর। প্রথম চারটি ধাপ run-এ মাপা। শেষ দুটি default নিয়ম থেকে; ৫ মিনিট অপেক্ষা করা হয়নি।

1function ProductList() {2  const { data, isPending } = useQuery({3    queryKey: ['products', 'list'],4    queryFn: fetchProducts,5    staleTime: 30_000,6  });7  if (isPending) return <Skeleton />;8  return <Grid items={data} />;9}
Cache-এর অবস্থা
সময়
ঘরে data
observer
status
fetchStatus
মোট অনুরোধ
  1. Cache miss. Component mount হলো। ['products', 'list']-এর কোনো ঘর নেই, তাই ঘর তৈরি হয়ে অনুরোধ গেল। Data নেই, তাই isPending true আর পর্দায় Skeleton.
  2. উত্তর এল। ঘরে ৬টি পণ্য বসল, dataUpdatedAt লেখা হলো। Observer জানল, component আবার render হলো। ৩০ সেকেন্ডের fresh ঘড়ি শুরু।
  3. পাতা ছাড়লেন। Component unmount হলো, observer ০। ডেটা মুছল না, ঘরটি inactive হলো আর gcTime-এর ঘড়ি শুরু। useEffect-এর সাথে বড় পার্থক্য এটিই: state component-এর সাথে মরে, cache মরে না।
  4. ফিরে এলেন, cache hit. ঘরে ডেটা আছে, বয়স ৩০ সেকেন্ডের কম। Skeleton ছাড়াই তালিকা, অনুরোধ নেই। Run-এ API-র মোট গণনা ১-ই থেকেছে।
  5. ৫ মিনিট কেউ দেখেনি। Inactive ঘর gcTime পার করল। ঘর memory থেকে মুছে গেল। এই অংশ default নিয়ম থেকে নেওয়া, মাপা নয়।
  6. আবার miss. এখন ফিরলে একদম শুরুর মতোই: Skeleton আর নতুন অনুরোধ। মোট ২টি।

এবার staleTime default 0 হলে কী হয়। এখানে সময়গুলো সত্যিকারের run-এর। দাম ৪২০ থেকে ৪৫০ হওয়াটি বোঝানোর জন্য ধরে নেওয়া; run-এর API-তে দাম বদলানো হয়নি।

staleTime: 0, চালের দাম
সময়
সার্ভারে দাম
Cache-এ দাম
isStale
isPending
isFetching
মোট অনুরোধ
  1. উত্তর এল, সাথে সাথেই stale. staleTime 0, তাই fresh সময় বলে কিছু নেই। ডেটা ঠিকই দেখানো হচ্ছে, শুধু দাগ পড়ল “মিলিয়ে দেখতে হবে”।
  2. আপনি চলে গেলেন, দাম বদলাল। সার্ভারে 450, cache-এ এখনো 420. Library জানে না, জানার উপায়ও নেই। কোনো অনুরোধ যায়নি।
  3. ফিরলেন: পুরোনোটি দেখান, পেছনে আনুন। এটিই stale-while-revalidate. isPending false, তাই Skeleton নেই। isFetching true, তাই চাইলে ছোট চিহ্ন দেখান। অনুরোধ #2 গেল।
  4. উত্তর এল, ২১১ ms পরে। Cache-এ 450, পর্দা নিজে থেকেই বদলাল। ব্যবহারকারী শূন্য অপেক্ষায় কিছু দেখলেন, তারপর সঠিকটি পেলেন।
  5. Tab-এ ফিরলেন। ডেটা stale, তাই window focus-ও একটি refetch ঘটায়। Run-এ staleTime: 0-তে এই ঘটনায় অনুরোধ বাড়ল, আর staleTime: 30000-এ বাড়েনি।

এবার মডেলে দুটি ঘড়ি একসাথে চালান। এটিও শিক্ষণ মডেল: staleTime ধরা হয়েছে 1000 ms, gcTime 3000 ms, যাতে ছোট সময়েই সব দেখা যায়।

// শিক্ষণ মডেল: staleTime আর gcTime, simulated clock-এ (আসল library নয়)
const STALE = 1000, GC = 3000, LATENCY = 200;
let now = 0, requests = 0, serverPrice = 420;
let entry = null, leftAt = null;    // একটি key-এর ঘর; শেষ observer কখন গেল

function advance(to) {
  now = to;
  if (entry?.pending && now >= entry.pending.doneAt) {
    entry.data = entry.pending.data;
    entry.updatedAt = entry.pending.doneAt;
    entry.pending = null;
  }
  if (leftAt !== null && now - leftAt >= GC) entry = null;   // garbage collect
}
function visit() {
  leftAt = null;
  entry ??= { data: null, updatedAt: -Infinity, pending: null };
  const stale = now - entry.updatedAt >= STALE;
  if (stale && !entry.pending) {
    requests += 1;
    entry.pending = { doneAt: now + LATENCY, data: serverPrice };
  }
  return `দেখায় ${entry.data ?? 'spinner'}, ${stale ? 'পেছনে আনছে' : 'fresh, অনুরোধ নেই'}`;
}

const log = (t, what) => { advance(t); console.log(`t=${t} ${what()} | requests=${requests}`); };
log(0, visit);
log(200, () => { leftAt = now; return 'উত্তর এল, পাতা ছেড়ে গেলেন'; });
log(700, visit);                    // 500 ms পুরোনো: fresh
log(800, () => { leftAt = now; serverPrice = 450; return 'দাম হলো 450'; });
log(1500, visit);                   // 1300 ms পুরোনো: stale, পুরোনো 420 দেখায়
log(1700, () => { leftAt = now; return `পেছনের উত্তরে এখন ${entry.data}`; });
log(5000, visit);                   // gc হয়ে গেছে: আবার spinner

শেষ লাইনগুলো পড়ুন: t=700 দেখায় 420, fresh, অনুরোধ নেই | requests=1 (বয়স 500 ms). t=1500 দেখায় 420, পেছনে আনছে | requests=2: সার্ভারে দাম 450 হলেও পর্দায় আগে পুরোনো 420, কারণ বয়স 1300 ms. t=1700 পেছনের উত্তরে এখন 450 | requests=2: নতুন দাম বসল। t=5000 দেখায় spinner, পেছনে আনছে | requests=3: t=1700 থেকে t=5000 পর্যন্ত, মানে 3300 ms কেউ দেখেনি, gcTime পার হয়ে ঘর মুছে গেছে, তাই পুরোনো ডেটাও নেই। প্রথম লাইনে “পেছনে আনছে” লেখা আছে কারণ মডেলটি সরল; সেখানে আসলে সামনে spinner.

হিসাব: staleTime কত অনুরোধ বাঁচায়

ধরুন একজন ক্রেতা ৫ মিনিটে তালিকা পাতায় ১৫ বার আসেন, প্রতি ২০ সেকেন্ডে একবার (t = 0, 20, … 280 s). gcTime default, উত্তরের সময় 0.2 s.

  • staleTime: 0: প্রতিবার stale, তাই ১৫টি অনুরোধ। Hit rate ০% (পুরোনো কপি দেখলেও প্রতিবার সার্ভারে যায়)।
  • staleTime: 50_000: t=0-তে আনা ডেটার বয়স t=40-এ 39.8 s, fresh. t=60-এ 59.8 s, stale. অর্থাৎ অনুরোধ যায় t = 0, 60, 120, 180, 240-এ, মোট ৫টি। বাকি ১০ বার cache থেকে। Hit rate = ১০/১৫ = ৬৬.৭%.
  • সার্ভারের ওপর প্রভাব: ধরে নেওয়া ১০,০০০ ক্রেতা একই আচরণ করলে ৩০০ সেকেন্ডে ১,৫০,০০০ অনুরোধ, অর্থাৎ ৫০০ req/s. staleTime দিলে ৫০,০০০ অনুরোধ, প্রায় ১৬৭ req/s.

staleTime বাড়ানো মানে পুরোনো ডেটা দেখার ঝুঁকি বাড়ানো। পণ্যের বিবরণে ৫ মিনিট চলে, স্টকের সংখ্যায় নাও চলতে পারে। আর দাম বা স্টকের চূড়ান্ত যাচাই সবসময় সার্ভারে, অর্ডার নেওয়ার মুহূর্তে। Client-এর cache কখনো সত্যের উৎস নয়।

৫ · কখন নতুন করে আনে: refetch, retry আর loading ও error-এর UI

একটি ভুল ধারণা ভাঙা যাক: stale ডেটা নিজে নিজে আনা হয় না। Refetch ঘটে শুধু কোনো ঘটনায়, আর তখনও শুধু ডেটা stale হলে। দোকানদার ঘড়ি দেখে আড়তে ফোন করেন না; ক্রেতা এলে দেখেন কাগজটি পুরোনো কি না।

ঘটনাOptionDefaultFresh ডেটায়
নতুন observer mountrefetchOnMounttrueআনে না
Window-এ ফিরে আসাrefetchOnWindowFocustrueআনে না
Network ফিরে আসাrefetchOnReconnecttrueআনে না
নির্দিষ্ট বিরতিrefetchIntervalবন্ধতবু আনে
হাতে ডাকাrefetch()—তবু আনে

Run-এ window focus-এর ঘটনা পাঠিয়ে মাপা হয়েছে। staleTime: 0-তে মোট অনুরোধ ১ থেকে ২ হলো, staleTime: 30000-এ ১-ই থাকল। নতুন শেখা ডেভেলপাররা প্রায়ই অবাক হন যে অন্য tab থেকে ফিরলেই Network ট্যাবে অনুরোধ আসে। এটি bug নয়, default.

Retry: ব্যর্থ হলেই error নয়

Query ব্যর্থ হলে library নিজে থেকে ৩ বার retry করে, প্রতিবার অপেক্ষা দ্বিগুণ করে: এটি exponential backoff. Library-র উৎস কোডে সূত্রটি min(1000 × 2n, 30000) ms. API সবসময় 503 দিলে কী হয়, সত্যিকারের run:

API  #1     30 ms  GET /api/down -> 503
API  #2   1146 ms  GET /api/down -> 503
API  #3   3251 ms  GET /api/down -> 503
API  #4   7375 ms  GET /api/down -> 503
UI      107 ms  status=pending failureCount=1
UI     1223 ms  status=pending failureCount=2
UI     3340 ms  status=pending failureCount=3
UI     7466 ms  status=error failureCount=4

হিসাব মিলিয়ে নিন। মোট ১ + ৩ = ৪টি অনুরোধ। অপেক্ষা ১ + ২ + ৪ = ৭ সেকেন্ড, সাথে প্রতিটি অনুরোধে API-র 100 ms, মোট প্রায় ৭.৪ সেকেন্ড। এই পুরো সময় status ছিল pending. অর্থাৎ সার্ভার বন্ধ থাকলে default-এ ব্যবহারকারী ৭ সেকেন্ডের বেশি Skeleton দেখেন, তারপর error. দুবার ব্যর্থ হয়ে তৃতীয়বার সফল হওয়া /api/flaky-তে অনুরোধ ৩টি, আর সাফল্য এসেছে 3350 ms-এ, error দেখাই যায়নি।

Default retry সব error-এ চলে, 404-এও। পণ্য না থাকলে ৩ বার চেষ্টা অর্থহীন। তাই শর্ত দিন। এতে 404-এ ১টি অনুরোধেই (226 ms) error হয়েছে:

useQuery({
  queryKey: ['products', 'detail', id],
  queryFn: () => getJson(`/api/products/${id}`),
  retry: (count, err) => err.status >= 500 && count < 3,
});

fetch 404 বা 500-এ reject করে না, Promise সফলভাবেই শেষ হয়। Query function throw না করলে library ধরে নেবে সব ঠিক, আর error-এর JSON আপনার data-তে বসবে। তাই res.ok দেখে throw করতেই হবে। মূল ধারণা আছে JavaScript কোর্সের fetch পর্বে।

export async function getJson(url, options) {
  const res = await fetch(url, options);
  if (!res.ok) {
    const body = await res.json().catch(() => null);
    const err = new Error(body?.message ?? `HTTP ${res.status}`);
    err.status = res.status;
    throw err;
  }
  return res.json();
}

Loading আর error-এর UI: দুটি আলাদা অক্ষ

useQuery দুটি প্রশ্নের আলাদা উত্তর দেয়। status বলে ডেটা আছে কি: pending, success, error. fetchStatus বলে এখন network-এ কিছু চলছে কি: fetching, idle, paused. Network বন্ধ থাকলে default-এ অনুরোধ paused থাকে, আর সংযোগ ফিরলে চলে। দুটিকে এক ভাবলেই UI ভুল হয়।

অবস্থাযা দেখে চিনবেনভালো UI
প্রথমবার, ডেটা নেইisPendingSkeleton, layout নড়ে না
ডেটা আছে, পেছনে আনছেisFetching && !isPendingডেটা থাকবে, শুধু ছোট চিহ্ন
প্রথমবারেই ব্যর্থisError, data নেইবোধগম্য বার্তা আর “আবার চেষ্টা” (refetch())
পুরোনো ডেটা আছে, refetch ব্যর্থisError, data আছেডেটা রাখুন, ছোট সতর্কতা দিন; পুরো পাতা মুছবেন না
অফলাইনfetchStatus === 'paused'“সংযোগ নেই” বলুন, চিরকাল ঘোরাবেন না

v5-এ isLoading-এর অর্থ isPending && isFetching. চতুর্থ সারিটি খেয়াল করুন: if (isError) return <Error /> লিখলে পেছনের একটি ব্যর্থ refetch দেখতে থাকা তালিকা মুছে দেয়। বাংলাদেশে দুর্বল মোবাইল network-এ এটি বেশ চোখে পড়ে। আগে data দেখুন। দুর্বল সংযোগে latency বেশি বলে এই পার্থক্য আরও জরুরি।

৬ · useMutation আর invalidation: সার্ভার বদলালে cache-কে জানানো

এতক্ষণ শুধু পড়া হলো। লেখার কাজে অর্থাৎ POST, PATCH, DELETE-এ লাগে useMutation. শব্দটি সাবধানে পড়ুন: পর্ব ১০-এর mutation ছিল object সরাসরি বদলানো, আর এখানে mutation মানে সার্ভারের ডেটা বদলানোর অনুরোধ। পার্থক্য তিনটি: mutation নিজে থেকে চলে না, আপনি mutate() ডাকেন; তার cache-এর ঘর নেই; আর default-এ retry হয় না (উৎস কোডে retry ?? 0). শেষটি ইচ্ছাকৃত: একই অর্ডার দুবার পাঠানোর ঝুঁকি আছে, যা পর্ব ৩৩-এর idempotency-তে দেখবেন।

নিচের কোডটি গঠন বোঝানোর উদাহরণ, এটি run করা হয়নি। একই গঠনের চালানো কোড পরের অংশে আছে।

const queryClient = useQueryClient();

const rename = useMutation({
  mutationFn: ({ id, name }) => patchProduct(id, { name }),
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['products'] }),
});

<button disabled={rename.isPending}
  onClick={() => rename.mutate({ id: 1, name: 'নাজিরশাইল চাল ৫ কেজি' })}>
  সেভ করুন
</button>

সার্ভার বদলে গেল, কিন্তু cache তা জানে না। Invalidation বলে: “এই key-গুলোর কাগজ বাতিল।” invalidateQueries দুটি কাজ করে: মিলে যাওয়া সব query-কে stale দাগায়, staleTime যাই হোক; আর যেগুলোর observer আছে, সেগুলো পেছনে refetch করে। Inactive ঘর শুধু দাগ পায়, দর্শক এলে তখন আনবে। সত্যিকারের দুটি run-এর ছাঁটা log:

# list আর detail দুটোই পর্দায়; ['cart'] query-ও পর্দায়
# invalidateQueries({ queryKey: ['products'] })
API  #3    682 ms  GET /api/products -> 200
API  #4    682 ms  GET /api/products/1 -> 200
UI       4 ms  cart queryFn          ← পুরো run-এ একবারই

# detail পর্দায় নেই (inactive), তারপর একই invalidate
API  #3    791 ms  GET /api/products -> 200
# detail খুলতেই
API  #4   1311 ms  GET /api/products/1 -> 200

মিল হয় key-এর শুরু দেখে। তাই ['products'] দিলে তালিকা আর বিবরণ দুটোই পড়ে, ['cart'] পড়ে না। এজন্যই key-কে সাধারণ থেকে নির্দিষ্টের দিকে সাজানো হয়: ['products', 'list', { cat, page }], ['products', 'detail', id]. নিচের মডেল plain JavaScript-এ মিলের নিয়মটি দেখায়।

// শিক্ষণ মডেল: query key মেলানো, plain JavaScript-এ
function sorted(value) {            // object-এর key সাজিয়ে একই hash
  if (value === null || typeof value !== 'object' || Array.isArray(value)) return value;
  return Object.fromEntries(Object.keys(value).sort().map((k) => [k, value[k]]));
}
const hash = (key) => JSON.stringify(key, (_, v) => sorted(v));

function partial(a, b) {            // b-এর সব অংশ a-র ভেতরে আছে?
  if (a === b) return true;
  if (typeof a !== typeof b || !a || !b || typeof a !== 'object') return false;
  return Object.keys(b).every((k) => partial(a[k], b[k]));
}
function matches(queryKey, filter, exact = false) {
  return exact ? hash(queryKey) === hash(filter) : partial(queryKey, filter);
}

const keys = [
  ['products', 'list', { cat: 'চাল', page: 1 }],
  ['products', 'list', { cat: 'ডাল', page: 1 }],
  ['products', 'detail', 7],
  ['cart'],
];
const run = (label, filter, exact) => {
  const hit = keys.filter((k) => matches(k, filter, exact));
  console.log(label, '→', hit.length, 'টি:', hit.map(hash).join('  '));
};
run("['products']", ['products']);
run("['products','list']", ['products', 'list']);
run("{cat:'চাল'} অংশ", ['products', 'list', { cat: 'চাল' }]);
run("['products'] exact", ['products'], true);
console.log('hash একই?', hash(['p', { a: 1, b: 2 }]) === hash(['p', { b: 2, a: 1 }]));

['products'] মেলে ৩টি key-তে, ['products','list'] ২টিতে, {cat:'চাল'} অংশটি ১টিতে: object-এর সব key না মিললেও চলে, যা দেওয়া আছে তা মিললেই হলো। exact দিলে ০টি, কারণ হুবহু ['products'] নামে কোনো ঘর নেই। শেষ লাইন hash একই? true: object-এর key-এর ক্রম গুরুত্বহীন, কারণ hash-এর আগে key সাজানো হয়। আসল library-র partialMatchKey আর hashKey ডেকে একই চারটি ক্ষেত্র Node-এ চালিয়ে ৩, ২, ১, ০ আর true পাওয়া গেছে। তবে array-র ক্রম গুরুত্বপূর্ণ: ['a','b'] আর ['b','a'] আলাদা ঘর।

Invalidate ছাড়া আরেক পথ: PATCH-এর উত্তরে নতুন পণ্যটি যদি আসেই, setQueryData দিয়ে সরাসরি ঘরে বসান। অনুরোধ একটি কম, কিন্তু তালিকার sort বা গণনা বদলে থাকলে তা ধরা পড়বে না। নিরাপদ শুরু invalidate; মাপার পরে প্রয়োজন হলে অন্যটি।

৭ · Optimistic update: আগে দেখান, ব্যর্থ হলে ফেরান

ক্রেতা “পছন্দের তালিকা”-য় হৃদয় চিহ্নে চাপলেন। সার্ভারের উত্তর আসতে 300 ms, দুর্বল network-এ ২ সেকেন্ডও লাগতে পারে। এতক্ষণ চিহ্নটি না বদলালে মনে হয় চাপ পড়েনি, আর মানুষ আবার চাপে। Optimistic update মানে সাফল্য ধরে নিয়ে UI আগেই বদলানো, আর ব্যর্থ হলে rollback.

রেস্তোরাঁয় ওয়েটার আপনার অর্ডার শুনেই খাতায় টিক দিলেন, রান্নাঘরে না জেনেই বললেন “আসছে”। বেশির ভাগ দিন সত্যিই আসে। কোনো দিন রান্নাঘর বলল মাছ শেষ। তখন ওয়েটার ফিরে এসে টিক কেটে দেন আর ক্ষমা চান। Optimistic update এটাই। আগের খাতার ছবিটিই snapshot.

দুটি পথ আছে। শুধু একটি component-এ ফল দেখালে useMutation-এর variables দিয়ে অস্থায়ী রূপ দেখান, cache ছুঁতে হয় না, rollback-ও লাগে না। একাধিক component-এ দেখালে cache বদলাতে হয়। সেটিই নিচের কোড, যা run-এ চলেছে। v5-এর সাম্প্রতিক সংস্করণে callback-এর শেষ argument-এ client পাওয়া যায়; useQueryClient()-ও চলে।

const listKey = ['products', 'list'];

function useToggleWish() {
  return useMutation({
    mutationFn: patchProduct,
    onMutate: async ({ id, wish }, ctx) => {
      await ctx.client.cancelQueries({ queryKey: listKey });
      const previous = ctx.client.getQueryData(listKey);
      ctx.client.setQueryData(listKey, (old) =>
        old.map((p) => (p.id === id ? { ...p, wish } : p)));
      return { previous };
    },
    onError: (err, vars, result, ctx) => {
      ctx.client.setQueryData(listKey, result.previous);
    },
    onSettled: (data, err, vars, result, ctx) =>
      ctx.client.invalidateQueries({ queryKey: ['products'] }),
  });
}

API-তে id 3-এর পণ্যে 409 ফেরত দেওয়ার ব্যবস্থা ছিল। নিচের ধাপের সময় সেই run থেকে, চাপ দেওয়ার মুহূর্তকে 0 ধরে।

1onMutate: async ({ id, wish }) => {2  await cancelQueries(listKey);3  const previous = getQueryData(listKey);4  setQueryData(listKey, withWish);5  return { previous };6},7onError: (err, v, result) =>8  setQueryData(listKey, result.previous),9onSettled: () => invalidate(['products']),
id 3-এর wish
সময়
সার্ভারে
Cache-এ
snapshot
অনুরোধ
  1. শুরুর অবস্থা। সার্ভার, cache আর পর্দা, তিনটিই একমত: false. Query key-এর ঘরে পুরো তালিকা আছে।
  2. চাপ পড়ল: প্রস্তুতি। onMutate সবার আগে চলে। চলমান refetch বাতিল হয়, নইলে তার পুরোনো উত্তর এসে optimistic মান মুছে দিতে পারত। তারপর snapshot রাখা হয়।
  3. আগেই দেখানো। Cache-এ true বসল, পর্দা বদলাল। Run-এ চাপের 30 ms পরে পড়ে দেখা গেছে চিহ্ন on, যখন PATCH-এর উত্তর আসেনি। সার্ভার এখনো false: পর্দা আর সত্য সাময়িকভাবে আলাদা।
  4. 409 এল: rollback. onError snapshot থেকে পুরোনো মান ফিরিয়ে আনল। পর্দায় চিহ্ন off আর সার্ভারের বার্তা। ব্যর্থতা জানানো জরুরি; নিঃশব্দে উল্টে গেলে মানুষ ভাবেন অ্যাপ নষ্ট।
  5. onSettled: আবার মেলানো। সফল বা ব্যর্থ, দুই ক্ষেত্রেই চলে। Invalidate করায় GET গেল। কারণ snapshot নেওয়ার পরে সার্ভারে অন্য কিছু বদলাতে পারে।
  6. সত্য নিশ্চিত। GET-এর উত্তর cache-এ বসল। তিনটিই আবার একমত। মোট অনুরোধ: প্রথম GET, PATCH, শেষ GET.

এই ক্রমটি মডেলে চালান, একবার সফল আর একবার ব্যর্থ। এটি শিক্ষণ মডেল; ব্যর্থতা simulated, আর ঘটনাগুলো simulated clock-এর তালিকায় সাজানো।

// শিক্ষণ মডেল: optimistic update আর rollback, আসল library নয়
let now = 0;
const events = [];                  // simulated clock-এর ঘটনা তালিকা
const at = (t, fn) => events.push({ t, fn });
const server = { 1: false, 3: false };   // server-এর সত্য
const cache = { list: { 1: false, 3: false } };
const ui = () => `পর্দা: id1=${cache.list[1]} id3=${cache.list[3]}`;
const say = (m) => console.log(`t=${String(now).padStart(4)} ${m}`);

function toggle(id, wish, startAt) {
  at(startAt, () => {                                  // onMutate
    const snapshot = { ...cache.list };
    cache.list = { ...cache.list, [id]: wish };
    say(`onMutate id=${id}, snapshot রাখা হলো | ${ui()}`);
    at(now + 300, () => {                              // server-এর উত্তর
      if (id === 3) {                                  // simulated failure
        cache.list = snapshot;                         // onError
        say(`409 এল, snapshot ফিরল | ${ui()}`);
      } else { server[id] = wish; say(`200 এল | ${ui()}`); }
      at(now + 200, () => {                            // onSettled, invalidate
        cache.list = { ...server };
        say(`refetch শেষ, server-এর সত্য | ${ui()}`);
      });
    });
  });
}

toggle(1, true, 0);
toggle(3, true, 1000);
while (events.length) {
  events.sort((a, b) => a.t - b.t);
  const e = events.shift();
  now = e.t;
  e.fn();
}

প্রথম তিন লাইনে id 1 সফল: t= 0-তেই id1=true, t= 300-এ 200, পরে সার্ভারও একমত। পরের তিন লাইনে id 3: t=1000 onMutate id=3, snapshot রাখা হলো | পর্দা: id1=true id3=true, তারপর t=1300 409 এল, snapshot ফিরল | পর্দা: id1=true id3=false. খেয়াল করুন, rollback-এ id1 নষ্ট হয়নি, কারণ snapshot নেওয়া হয়েছে তার সফল হওয়ার পরে। দুটি mutation একসাথে চললে পরেরটির snapshot-এ আগেরটির অনিশ্চিত মান ঢুকে যায়, আর rollback তা-ও ফিরিয়ে আনে। তাই বড় অ্যাপে শেষ mutation শেষ হলে তবেই invalidate করা হয়।

সত্যিকারের run-এর log একই গল্প বলে। শেষ দুই লাইন driver script-এর ছাপা:

API  #1     29 ms  GET /api/products -> 200
API  #2    412 ms  PATCH /api/products/3 -> 409
API  #3    717 ms  GET /api/products -> 200
UI     386 ms  onMutate: cache-এ id=3 wish=true
UI     691 ms  onError: পণ্যটি আর বিক্রি হচ্ছে না; আগের কপি ফিরল
UI     691 ms  onSettled: invalidate
after click id3: on
settled id3: off | err: পণ্যটি আর বিক্রি হচ্ছে না

Optimistic update সবখানে নয়। পছন্দের চিহ্ন, like, কার্টে পরিমাণ বদল: ব্যর্থতা বিরল আর ক্ষতি সামান্য, এগুলো উপযুক্ত। পেমেন্ট, অর্ডার নিশ্চিত করা, স্টকের শেষ পণ্য কেনা: এখানে “হয়ে গেছে” দেখিয়ে পরে ফিরিয়ে নেওয়া আস্থা নষ্ট করে। সেখানে অপেক্ষার স্পষ্ট UI দিন, বোতাম নিষ্ক্রিয় রাখুন, আর সার্ভারের উত্তরের পরেই সাফল্য দেখান। পর্ব ৬৫-এর পেমেন্টে এই নিয়মই মানবেন।

৮ · Pagination, infinite query, prefetch আর DevTools

Pagination: পাতা বদলে লাফ বন্ধ করা

Pagination-এ প্রতিটি পাতা আলাদা key, আলাদা ঘর। কিন্তু page 2 চাইলে সেই ঘর খালি, তাই তালিকা হঠাৎ Skeleton হয়ে পাতা লাফিয়ে ওঠে। সমাধান placeholderData: নতুন ঘরের ডেটা আসা পর্যন্ত আগের পাতা দেখান। Offset আর cursor-এর সার্ভার-দিকের হিসাব পর্ব ৩৪-এ।

const { data, isPlaceholderData } = useQuery({
  queryKey: ['products', 'page', page],
  queryFn: ({ signal }) => getJson(`/api/products?page=${page}`, { signal }),
  placeholderData: keepPreviousData,
  staleTime: 30_000,
});
API  #1     36 ms  GET /api/products?page=1 -> 200
API  #2    589 ms  GET /api/products?page=2 -> 200
UI     215 ms  page=1 দেখাচ্ছে=1 placeholder=false fetching=false
UI     561 ms  page=2 দেখাচ্ছে=1 placeholder=true fetching=true
UI     780 ms  page=2 দেখাচ্ছে=2 placeholder=false fetching=false
UI    1092 ms  page=1 দেখাচ্ছে=1 placeholder=false fetching=false

561 ms-এ page 2 চাওয়া হলেও পর্দায় page 1 থেকেছে, তাই লাফ নেই। isPlaceholderData দিয়ে “পরের পাতা” বোতাম সেই সময় বন্ধ রাখা যায়। Page 1-এ ফেরার সময় তৃতীয় অনুরোধ যায়নি, ঘরটি fresh ছিল।

Infinite query: “আরও দেখুন”

useInfiniteQuery এক ঘরেই সব পাতা জমা রাখে। v5-এ initialPageParam আর getNextPageParam বাধ্যতামূলক।

const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
  queryKey: ['products', 'feed'],
  queryFn: ({ pageParam, signal }) =>
    getJson(`/api/products?page=${pageParam}`, { signal }),
  initialPageParam: 1,
  getNextPageParam: (lastPage) => lastPage.nextPage,   // null মানে শেষ
});
const items = data?.pages.flatMap((p) => p.items) ?? [];
API  #1     32 ms  GET /api/products?page=1 -> 200
API  #2    590 ms  GET /api/products?page=2 -> 200
API  #3   1016 ms  GET /api/products?page=3 -> 200
# ৬টি পণ্য, hasNextPage=false; এবার invalidate
API  #4   1443 ms  GET /api/products?page=1 -> 200
API  #5   1655 ms  GET /api/products?page=2 -> 200
API  #6   1868 ms  GET /api/products?page=3 -> 200

লক্ষ করুন, invalidate-এ তিনটি পাতাই আবার এসেছে, একটির পর একটি. পরের পাতার cursor আগের পাতার উত্তর থেকে আসে, তাই একসাথে আনা যায় না। Run-এ পাতা প্রতি প্রায় 210 ms. কেউ ২০ পাতা স্ক্রল করলে এক refetch-এ ২০ × 0.21 = ৪.২ সেকেন্ড আর ২০টি অনুরোধ। এর ওষুধ maxPages: cache-এ কত পাতা থাকবে তার সীমা।

Prefetch: চাওয়ার আগেই আনা

মাউস link-এ গেলে ধরে নেওয়া যায় ক্লিক আসছে। prefetchQuery ডেটা এনে cache-এ রাখে, কোনো UI ছাড়া। Run-এ: 306 ms-এ hover থেকে GET গেল, 707 ms-এ ক্লিক, আর একই মুহূর্তে detail isPending=false: Skeleton একবারও আসেনি। staleTime দিতে ভুলবেন না, নইলে prefetch-করা ডেটা সাথে সাথে stale হয়ে ক্লিকে আবার অনুরোধ যাবে। ফোনে hover নেই, আর বেশি prefetch মানে ব্যবহারকারীর মোবাইল ডেটা খরচ। পরের পর্বের route loader-ও পাতা আসার আগে ডেটা আনার একটি পথ।

const detail = {
  queryKey: ['products', 'detail', 2],
  queryFn: ({ signal }) => getJson('/api/products/2', { signal }),
  staleTime: 30_000,
};

<a href="/products/2" onMouseEnter={() => queryClient.prefetchQuery(detail)}>
  দেখুন
</a>

DevTools: cache-এর ভেতরটা চোখে দেখা

@tanstack/react-query-devtools আলাদা package. Provider-এর ভেতরে <ReactQueryDevtools initialIsOpen={false} /> বসালে পাতার কোণে একটি প্যানেল আসে: প্রতিটি key, তার অবস্থা (fresh, fetching, stale, inactive), observer সংখ্যা আর ডেটা। নথি অনুযায়ী এটি শুধু NODE_ENV === 'development'-এ bundle-এ ঢোকে। “কেন আবার অনুরোধ গেল” প্রশ্নে এটিই প্রথম জায়গা, সাথে ব্রাউজারের DevTools-এর Network ট্যাব। Component-এর দিকটি দেখতে থাকে React DevTools.

Suspense mode: useSuspenseQuery ব্যবহার করলে loading সামলায় Suspense আর error সামলায় error boundary, তাই component-এ data সবসময় থাকে। Run-এ প্রথমে fallback “অপেক্ষা”, তারপর তালিকা, ১টি অনুরোধ। পুরোনো suspense: true option v5-এ নেই। Suspense-এর পূর্ণ আলোচনা পর্ব ২৩-এ।

৯ · SWR আর অন্য পথ: কখন কোনটি

SWR একই সমস্যার ছোট সমাধান, Vercel-এর তৈরি। নামটি এসেছে stale-while-revalidate থেকে। API একটি লাইনের: useSWR(key, fetcher). Run-এ দুই component মিলে ১টি অনুরোধ হয়েছে, কারণ এর dedupingInterval default 2000 ms.

TanStack Query 5SWR 2
পুরোনো হওয়ার হিসাবstaleTime, gcTime আলাদাdedupingInterval আর পুনঃযাচাইয়ের option
KeyArray, অংশ ধরে invalidateString, array বা ফাংশন; matcher ফাংশন দিয়ে একাধিক
MutationuseMutation, পূর্ণ lifecycleuseSWRMutation, optimisticData, rollbackOnError
FrameworkReact, Vue, Solid, Svelte, AngularReact
DevToolsনিজস্ব প্যানেলসরকারি প্যানেল নেই (লেখার সময়ে জানা মতে)
মাপা বাড়তি bundle+10.01 kB gzip+5.45 kB gzip

শেষ সারির সংখ্যা একটি ছোট vite build থেকে: শুধু React আর useEffect-এর পাতা 68.65 kB gzip, useQuery যোগে 78.66 kB, useSWR যোগে 74.10 kB. এটি একটি local মাপ, সাধারণ সিদ্ধান্ত নয়; ব্যবহার করা feature বাড়লে সংখ্যা বদলাবে। তবে হিসাব পরিষ্কার: বড় অ্যাপে ১০ kB-র বদলে অনেক হাতে লেখা কোড বাদ যায়।

বাছাইয়ের ছক

  • দু-একটি পাতা, প্রায় স্থির ডেটা: সাধারণ fetch আর custom hook-ও যথেষ্ট হতে পারে। Library না নেওয়াও একটি সিদ্ধান্ত।
  • অনেক mutation, optimistic UI, pagination: TanStack Query-র গঠন কাজে লাগে।
  • মূলত পড়া, bundle ছোট রাখা দরকার: SWR ভালো বিকল্প।
  • আগে থেকেই Redux Toolkit: RTK Query বিবেচনা করুন, দুটি পাশাপাশি আনার আগে ভাবুন।
  • Next.js App Router: ডেটার বড় অংশ server component-এ আসে (পর্ব ৫৩, ৫৪), তখন client-এর library কম লাগে, তবে infinite scroll বা polling-এর মতো client-প্রধান জায়গায় এখনো লাগে।

মানুষ এখানে একমত নন, আর সেটিই স্বাভাবিক। কেউ সব server state-এ library চান, কেউ route loader বা server component-কেই যথেষ্ট মনে করেন। সিদ্ধান্ত নিন আপনার mutation-এর সংখ্যা আর ডেটা কতটা ঘনঘন বদলায় তা দেখে।

আরও গভীরে: stale-while-revalidate ধারণাটি এসেছে HTTP থেকে: Mark Nottingham-এর RFC 5861 (২০১০) cache-কে পুরোনো উত্তর দিতে দিতে পেছনে নতুন করার অনুমতি দেয়, যা web caching পর্বে দেখতে পারেন। TanStack Query-তে একটি QueryCache-এ Query object থাকে, আর প্রতিটি useQuery একটি QueryObserver বানিয়ে React-এর সাথে useSyncExternalStore দিয়ে যুক্ত হয়; এজন্যই ন্যূনতম React 18 লাগে। নতুন উত্তর আগের সমান হলে structural sharing পুরোনো reference রাখে, তাই useMemo ভাঙে না। আর component শুধু যে field পড়ে, সেগুলো বদলালেই render হয়। এসব না জানলেও চলে, তবে re-render খুঁজতে গেলে কাজে আসে।

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

  • staleTime আর gcTime-এর পার্থক্য? প্রথমটি কতক্ষণ ডেটা fresh, দ্বিতীয়টি observer ছাড়া ঘর কতক্ষণ থাকবে।
  • isPending আর isFetching? প্রথমটি ডেটা নেই, দ্বিতীয়টি network-এ অনুরোধ চলছে; পেছনের refetch-এ শুধু দ্বিতীয়টি true.
  • Default-এ query কতবার চেষ্টা করে? মোট ৪ বার, অপেক্ষা ১, ২, ৪ সেকেন্ড। Mutation-এ retry ০।
  • Optimistic update-এর চার ধাপ? চলমান query বাতিল, snapshot, cache বদল, ব্যর্থ হলে ফেরানো ও শেষে invalidate.
  • Query key-তে কী থাকবে? Query function যে সব মানের ওপর নির্ভর করে তার সবই।

১০ · AI-এর যুগে: server state-এর কোড যাচাই

TanStack Query-র API v3, v4 আর v5-এ অনেক বদলেছে, আর ইন্টারনেটের বহু পুরোনো কোড দিয়ে AI শিখেছে। তাই AI প্রায়ই পুরোনো রূপ লেখে, এবং কিছু কোড চলেও, শুধু যা করার কথা তা করে না।

AI-এর ভুলকী হয়যাচাই
useQuery(['key'], fn)v5-এ চলে না, শুধু object রূপVersion-এর নথি, TypeScript error
cacheTime, keepPreviousData: truev5-এ বাতিল optionMigration guide
useQuery-তে onSuccessv5-এ সরানো হয়েছে, কখনো চলে নাLog বসিয়ে দেখুন
Key-তে filter নেইFilter বদলালেও পুরোনো ডেটাESLint plugin, Network ট্যাব
res.ok যাচাই নেই500-এর JSON data হয়ে বসেAPI বন্ধ করে পরীক্ষা
onMutate-এ cancelQueries নেইচলমান refetch optimistic মান মুছে দেয়ধীর network-এ পরীক্ষা
Token বা গোপন key client-এর কোডেসবাই দেখতে পায়Diff পড়ুন, bundle-এ খুঁজুন
Client-এ cache থেকে দাম নিয়ে অর্ডারপুরোনো বা বদলানো দাম সার্ভারে যায়সার্ভারে দাম নতুন করে হিসাব

AI-কে version, key-র গঠন আর ব্যর্থতার পথ, তিনটিই বলে দিন। শেষ লাইনটি AI-কে নিজের কাজ পরীক্ষার পথ জানাতে বাধ্য করে।

আমি React 19 আর @tanstack/react-query v5 ব্যবহার করি। PATCH /api/products/:id দিয়ে wish বদলানোর একটি optimistic mutation লিখে দাও। নিয়ম: ১. শুধু v5-এর object syntax, cacheTime বা useQuery-র onSuccess নয়। ২. Query key: ['products', 'list'], invalidate করবে ['products']. ৩. onMutate-এ cancelQueries আর snapshot, onError-এ rollback, onSettled-এ invalidate. ৪. fetch 200 ছাড়া অন্য status-এ throw করবে। ৫. নতুন কোনো package যোগ করবে না, কোনো secret client-এ রাখবে না। কোডের পরে বলো: ধীর network আর 409 উত্তরে আমি কীভাবে হাতে পরীক্ষা করব, আর Network ট্যাবে কয়টি অনুরোধ দেখব।

AI-এর লেখা server state-এর কোড হাতে পেলে

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

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

প্র ০১Redux-এ সব রাখাই তো এক সময় নিয়ম ছিল। Server state আলাদা করার ধারণা কীভাবে এল, আর এটি কি সবসময় ঠিক?

২০১৫ সালের পরে React-এর জগতে Redux প্রায় মানদণ্ড হয়ে ওঠে। তখনকার সাধারণ চর্চা ছিল: API থেকে আনা সবকিছু store-এ। একটি তালিকা আনতে তিনটি action (request, success, failure), একটি reducer, thunk বা saga, আর loading ও error-এর আলাদা field. বড় অ্যাপে store-এর বেশির ভাগ অংশ হয়ে যেত সার্ভারের ডেটার হাতে লেখা নকল।

২০১৯ সালের দিকে Tanner Linsley-র React Query আর Vercel-এর SWR একটি ভিন্ন প্রশ্ন তোলে: এই ডেটা কি আসলেই “state”, নাকি একটি cache? ধারণাটি ছড়ানোর পরে অনেক দল লক্ষ করে, server state সরিয়ে নিলে store-এ খুব কম জিনিসই থাকে। পরে Redux দলও RTK Query যুক্ত করে, যা একই চিন্তার প্রমাণ। v5 প্রকাশ পায় ২০২৩ সালের অক্টোবরে।

  • পক্ষে: হাতে লেখা loading ও error-এর boilerplate কমে, আর dedupe, stale হিসাব, retry সবার জন্য একই নিয়মে চলে।
  • বিপক্ষে: দুই জায়গায় state, নতুন একটি dependency, আর দলের সবাইকে staleTime বুঝতে হয়।
  • সীমারেখা ঝাপসা: Form-এর draft শুরুতে সার্ভার থেকে আসে, কিন্তু সম্পাদনার পরে সেটি client state. সেখানে copy রাখাই ঠিক, পর্ব ২১-এ দেখবেন।

এই ভাগের একটি ব্যবহারিক পরীক্ষা আছে। নিজের অ্যাপের store খুলে প্রতিটি field-এর পাশে লিখুন, এটি reload-এর পরে সার্ভার থেকে আবার পাওয়া যাবে কি না। যা পাওয়া যাবে, সেটি সম্ভবত server state; যা হারিয়ে যাবে, সেটিই আসল client state. অনেক দল এই পরীক্ষায় দেখে store-এর বড় অংশ সরিয়ে ফেলা যায়, আর বাকি অংশ এতই ছোট যে useState বা context-ই যথেষ্ট হয়ে যায়। অবশ্য বড় পুরোনো কোডে একবারে সব সরানো ঝুঁকিপূর্ণ; একটি একটি পাতা করে সরানোই নিরাপদ পথ।

মূল উপলব্ধি: ভালো বিভাজন হয় ডেটার মালিকানা দেখে, প্রযুক্তির নাম দেখে নয়। আগে জিজ্ঞেস করুন “এই ডেটার আসল কপি কার হাতে?” উত্তরই বলে দেবে কোন টুল মানায়।

প্র ০২“Cache invalidation কঠিন” কথাটি এত প্রচলিত কেন? staleTime কত দেবেন, তা কীভাবে ঠিক করবেন?

Phil Karlton-এর নামে প্রচলিত একটি রসিকতা বলে, কম্পিউটার বিজ্ঞানের দুটি কঠিন কাজের একটি হলো cache invalidation. কঠিন হওয়ার মূল কারণ জানা না থাকা: browser-এর পক্ষে জানা সম্ভব নয় সার্ভারে কখন কী বদলাল। তাই প্রতিটি কৌশল আসলে একটি অনুমান।

  • সময়ভিত্তিক (staleTime): সহজ, কিন্তু জানালার মধ্যে পুরোনো ডেটা দেখা যায়।
  • ঘটনাভিত্তিক (invalidate): নিজের করা বদলে নিখুঁত, কিন্তু অন্যের বদলের খবর আসে না।
  • সার্ভারের ধাক্কা: পর্ব ৬০-এর WebSocket বা পর্ব ৬১-এর SSE সার্ভার থেকে “এই key বদলেছে” খবর পাঠাতে পারে। সবচেয়ে সতেজ, তবে অবকাঠামোর খরচ আছে।

staleTime ঠিক করার প্রশ্ন একটি: এই ডেটা পুরোনো দেখলে ক্ষতি কত, আর বারবার আনলে খরচ কত? বাজারঘরের জন্য একটি যুক্তিসঙ্গত শুরু: পণ্যের ক্যাটাগরি কয়েক ঘণ্টা, পণ্যের বিবরণ কয়েক মিনিট, দাম এক মিনিটের মতো, স্টক খুব কম বা ০, আর নিজের অর্ডারের তালিকা ০ সাথে invalidation. এগুলো অনুমান, মাপ নয়। পর্ব ৪৪-এর Redis আর পর্ব ৫৩-এর Next.js cache-ও একই প্রশ্ন করে, আর caching strategies পর্বে এর তত্ত্ব আছে।

আরেকটি সূক্ষ্ম কথা: staleTime বড় হলে user পুরোনো ডেটা দেখেন, কিন্তু 0 হলেও সমস্যা আছে। প্রতিটি tab-বদলে অনুরোধ, আর লাখো ব্যবহারকারীতে সেটি সত্যিকারের লোড। অনেক দল তাই global default-এ অল্প একটি মান দেয়।

একটি ব্যবহারিক অভ্যাস হলো মান ঠিক করার আগে Network ট্যাবে দেখা, একটি সাধারণ ব্যবহারে কোন query কতবার যায়। তারপর যে query বারবার একই ডেটা আনছে, শুধু সেটির মান বাড়ান।

মূল উপলব্ধি: নিখুঁত সতেজতা বিনা খরচে পাওয়া যায় না। ভালো ইঞ্জিনিয়ার ডেটা ধরে ধরে ঠিক করেন কতটা পুরোনো সহ্য করা যাবে, আর চূড়ান্ত সত্য যাচাই রাখেন সার্ভারে।

প্র ০৩সার্ভার বিপর্যয়ের পরে ফিরে এলে হাজার হাজার browser একসাথে retry করলে কী হয়? Library-র default কি যথেষ্ট?

একে বলে thundering herd: একটি ঘটনায় সবাই একসাথে দৌড়ানো। ধরুন বাজারঘরের সার্ভার ৩ মিনিট বন্ধ ছিল। লোড-শেডিং শেষে হাজারো ফোন একসাথে অনলাইন, refetchOnReconnect আর refetchOnWindowFocus চালু, আর প্রতিটি পাতায় কয়েকটি query. সার্ভার উঠতে না উঠতেই আবার পড়ে যায়।

Exponential backoff একজন client-এর চাপ কমায়, কিন্তু সবার চেষ্টাকে আলাদা করে না। সবাই একই মুহূর্তে শুরু করলে সবাই 1 s, 3 s, 7 s-এ আবার আসে। Library-র default সূত্র min(1000 × 2n, 30000)-এ কোনো দৈব অংশ নেই, যা উৎস কোডে দেখা যায়। আমাদের run-এ অনুরোধগুলো ঠিক 1, 3, 7 সেকেন্ডের কাছেই এসেছে।

এর প্রচলিত ওষুধ jitter, অর্থাৎ অপেক্ষায় দৈব অংশ যোগ করা। Marc Brooker-এর ২০১৫ সালের বহুল পঠিত একটি প্রকৌশল লেখায় backoff-এর সাথে jitter কত অনুরোধ কমায় তা simulation দিয়ে দেখানো হয়েছে। TanStack Query-তে retryDelay ফাংশন নিজে দিয়ে এটি যোগ করা যায়:

retryDelay: (n) => Math.min(1000 * 2 ** n, 30000) * (0.5 + Math.random() / 2)

তবে client-এর ভালো ব্যবহার যথেষ্ট নয়, কারণ সব client আপনার নিয়ন্ত্রণে নেই। সার্ভারকে নিজেকে বাঁচাতে হয়: rate limit, 503-এর সাথে Retry-After, আর লোড কমাতে অপ্রয়োজনীয় কাজ বাদ দেওয়া। এসব আছে resilience patterns পর্বে। আর status >= 500-এ retry সীমিত রাখা, যা আগে দেখেছেন, এই চাপ কমানোর সহজ উপায়।

হিসাবটি একবার করে দেখুন। ধরে নেওয়া ১০,০০০ জন একসাথে ফিরলেন, প্রতি পাতায় ৩টি query. প্রথম ধাক্কাতেই ৩০,০০০ অনুরোধ। সার্ভার না পারলে সবাই ১ সেকেন্ড পরে আবার একই সংখ্যা পাঠাবে, তারপর ৩ সেকেন্ডে, তারপর ৭ সেকেন্ডে। Jitter দিলে একই অনুরোধগুলো কয়েক সেকেন্ড জুড়ে ছড়িয়ে যায়, ফলে মোট সংখ্যা এক থাকলেও এক মুহূর্তের চূড়া অনেক নিচে নামে।

মূল উপলব্ধি: একজন ব্যবহারকারীর জন্য যুক্তিসঙ্গত আচরণ লাখো ব্যবহারকারীতে একসাথে ঘটলে বিপদ হতে পারে। Frontend-এর default-ও capacity-র হিসাবের অংশ।

প্র ০৪Optimistic update করলে পর্দা আর সার্ভারের মধ্যে সাময়িক মিথ্যা থাকে। এটি কি নৈতিকভাবে আর প্রযুক্তিগতভাবে ঠিক?

দুটি প্রশ্নই গুরুত্বপূর্ণ। প্রথমটির উত্তর নির্ভর করে ভুল হলে ক্ষতি কত, তার ওপর। Like বোতাম সাময়িকভাবে ভুল দেখালে কারও ক্ষতি হয় না, আর প্রায় সবসময়ই সফল হয়। কিন্তু মোবাইল ব্যাংকিং অ্যাপ যদি “টাকা পাঠানো হয়েছে” দেখিয়ে পরে ফিরিয়ে নেয়, মানুষ সেই বার্তায় ভরসা করে দোকানি ছেড়ে চলে যেতে পারেন। তাই আর্থিক কাজে অ্যাপগুলো সাধারণত উল্টো পথে হাঁটে: স্পষ্ট “প্রক্রিয়াধীন” অবস্থা।

একটি মাঝের পথ হলো UI-তে সততা। বার্তা পাঠানোর অ্যাপ বার্তা সাথে সাথে দেখায়, কিন্তু পাশে ঘড়ির চিহ্ন, পৌঁছালে টিক। ডেটা optimistic, কিন্তু অনিশ্চয়তা লুকানো নয়। এটিই variables দিয়ে আবছা করে দেখানোর পথ।

প্রযুক্তিগত ঝুঁকিগুলো:

  • Race: চলমান refetch-এর পুরোনো উত্তর optimistic মান মুছতে পারে, তাই cancelQueries.
  • একাধিক mutation: দ্রুত দুটি চাপে পরের snapshot-এ আগেরটির অনিশ্চিত মান থাকে, আর আগেভাগে invalidate করলে পর্দা কেঁপে ওঠে। সমাধান: mutation key দিয়ে isMutating গুনে শেষটিতে invalidate.
  • অফলাইন: অনুরোধ paused থাকলে optimistic মান অনেকক্ষণ টিকে থাকে।
  • পুনরায় পাঠানো: mutation retry করলে সার্ভারকে idempotent হতে হয়, নইলে একই কাজ দুবার। এই কারণেই default retry ০।

আর optimistic UI কখনো অনুমতির যাচাই নয়। Client-এ চিহ্ন বদলানো সহজ; সার্ভারকে প্রতিটি অনুরোধে দেখতে হবে এই ব্যবহারকারীর অধিকার আছে কি না, যা পর্ব ৪৮-এ করবেন।

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

মূল উপলব্ধি: Optimistic update গতি নয়, অনুভূত গতি বাড়ায়, আর তার বদলে আপনি সততার একটি ঋণ নেন। ঋণটি শোধ হয় rollback, স্পষ্ট বার্তা আর সার্ভারের যাচাই দিয়ে।

প্র ০৫Server Component আর route loader এলে TanStack Query কি অপ্রয়োজনীয় হয়ে যাবে?

এই বিতর্ক এখনো চলছে, তাই দুই পক্ষই শোনা ভালো। React Server Component ডেটা আনে সার্ভারে, প্রথম HTML-এই ডেটা থাকে, আর client-এ library-র JavaScript লাগে না। React Router-এর loader পাতা আসার আগে ডেটা আনে, আর Next.js-এ Server Action-এর পরে revalidate করা যায়। ফলে “পাতা খুলে ডেটা দেখা” ধরনের অনেক কাজে client-এর cache library-র দরকার কমে গেছে। পর্ব ০৩-এর SSR-এর সুবিধাও এখানে মেলে।

কিন্তু কিছু কাজ স্বভাবতই client-এর:

  • Infinite scroll, যেখানে ব্যবহারকারীর সাথে ডেটা বাড়ে
  • Polling আর window focus-এ নতুন করা
  • Optimistic update-এর জটিল ক্রম
  • Mobile app-এর মতো অনুভূতির dashboard, যেখানে পাতা না বদলেই ডেটা বদলায়
  • React Native-এর মতো পরিবেশ, যেখানে server component-এর পথ সহজলভ্য নয়

তাই দুটি শত্রু নয়। TanStack Query-র নিজের নথিতেই server থেকে prefetch করে client-এর cache-এ hydrate করার পথ আছে: প্রথম পাতা সার্ভার থেকে, পরের স্ক্রল client থেকে। SSR-এ একটি গুরুত্বপূর্ণ নিয়ম: QueryClient প্রতিটি অনুরোধে নতুন হতে হবে, নইলে এক ব্যবহারকারীর ডেটা অন্যের কাছে যেতে পারে। পর্ব ৫৬-এ hydration বিস্তারিত দেখবেন।

একটি ব্যবহারিক কথা: টুল যত বদলাক, ধারণা থেকে যায়। Key, stale, invalidate, rollback: Next.js-এর tag আর revalidate একই ধারণার ভিন্ন নাম। এখানে যা শিখলেন, পর্ব ৫৩-এ তা নতুন পোশাকে দেখবেন।

শুরু করার একটি ছোট নিয়ম: পাতা খোলার ডেটা সার্ভারে আনার চেষ্টা করুন, ব্যবহারকারীর কাজের পরে আসা ডেটা client-এ। সার্চ ইঞ্জিনে দেখা দরকার এমন পাতায় প্রথমটি, লগইন করা dashboard-এ দ্বিতীয়টি প্রায়ই মানায়। তবে এটি শুরু করার নিয়ম, আইন নয়; মেপে দেখলে ভিন্ন ফলও পেতে পারেন।

মূল উপলব্ধি: প্রশ্নটি “কোন library জিতবে” নয়, প্রশ্ন “এই ডেটা কোথায় আনা সবচেয়ে ভালো: সার্ভারে, না client-এ?” উত্তর এক পাতার ভেতরেই দুটি হতে পারে।

অনুশীলন

প্রথমে নিজে করুন, তারপর উত্তর মিলিয়ে নিন।

  1. হিসাব: (ক) একজন ক্রেতা ২ মিনিটে একটি পাতায় ১২ বার আসেন, প্রতি ১০ সেকেন্ডে একবার (t = 0 থেকে 110 s). staleTime: 25_000, উত্তরের সময় প্রায় 0.2 s. কয়টি অনুরোধ, hit rate কত? (খ) ২০,০০০ ক্রেতা একই আচরণ করলে staleTime: 0 আর 25_000-এ req/s কত? (গ) retry: 4 আর default retryDelay-এ সার্ভার বন্ধ থাকলে কয়টি অনুরোধ, আর error কত পরে আসে (প্রতি অনুরোধ 0.2 s)?

    (ক) t=0-তে আনা ডেটা t=20-তে 19.8 s পুরোনো, fresh. t=30-এ 29.8 s, stale. অনুরোধ যায় t = 0, 30, 60, 90-এ, মোট ৪টি। বাকি ৮ বার cache থেকে। Hit rate = ৮/১২ = ৬৬.৭%.

    (খ) staleTime: 0: ২০,০০০ × ১২ = ২,৪০,০০০ অনুরোধ, ১২০ সেকেন্ডে, অর্থাৎ ২,০০০ req/s. 25_000: ২০,০০০ × ৪ = ৮০,০০০ অনুরোধ, অর্থাৎ প্রায় ৬৬৭ req/s.

    (গ) মোট ১ + ৪ = ৫টি অনুরোধ। অপেক্ষা ১ + ২ + ৪ + ৮ = ১৫ s, সাথে ৫ × 0.2 = ১ s. Error আসে প্রায় ১৬ সেকেন্ড পরে।

  2. ফল অনুমান করুন: Cache-এ তিনটি ঘর আছে: ['products', 'list', { page: 1 }] (পর্দায় আছে), ['products', 'detail', 7] (inactive, ডেটা আছে), ['cart'] (পর্দায় আছে)। সবগুলো fresh, staleTime ৫ মিনিট। আপনি (১) invalidateQueries({ queryKey: ['products'] }) ডাকলেন, (২) উত্তর আসার পরে detail পাতায় গেলেন। প্রতিটি ধাপে কয়টি অনুরোধ যাবে, আর detail পাতায় প্রথমে পর্দায় কী থাকবে?

    ধাপ ১: ১টি অনুরোধ, তালিকার। Detail ঘর মিলেছে আর stale দাগ পেয়েছে, কিন্তু observer নেই, তাই অনুরোধ নেই। ['cart'] মেলেনি, কিছু হয় না।

    ধাপ ২: ১টি অনুরোধ, detail-এর। পর্দায় প্রথমে পুরোনো ডেটা দেখা যায় (isPending false, isFetching true), Skeleton নয়, তারপর নতুন ডেটা বসে। মোট ২টি। এই পর্বের run-এও ঠিক এই ক্রম এসেছে।

  3. Code review: AI নিচের কোড দিয়েছে, লাইব্রেরি v5. কমপক্ষে পাঁচটি সমস্যা ধরুন: useQuery(['products'], () => fetch(`/api/products?page=${page}`).then(r => r.json()), { cacheTime: 60000, keepPreviousData: true, onSuccess: (d) => setCount(d.length) })
    • Array আর ফাংশন আলাদা argument: v5 শুধু একটি object নেয়।
    • Key-তে page নেই: সব পাতা একই ঘরে, পাতা বদলালে নতুন অনুরোধই যাবে না।
    • res.ok যাচাই নেই: 500-এর JSON data হয়ে বসবে, retry-ও হবে না।
    • cacheTime এখন gcTime.
    • keepPreviousData: true এখন placeholderData: keepPreviousData.
    • useQuery-র onSuccess v5-এ নেই। আর count তো হিসাব করে বের করা যায়: data?.length. এটি derived state, আলাদা state-এর দরকার নেই।
    • signal ব্যবহার করা হয়নি, তাই বাতিল হয় না।
  4. নকশা ও হাতে পরীক্ষা: বাজারঘরের জন্য একটি ছক বানান: ক্যাটাগরি, পণ্যের বিবরণ, স্টক, “আমার অর্ডার”। প্রতিটির query key, staleTime আর কোন mutation-এর পরে invalidate হবে লিখুন। কোন কাজগুলো optimistic হবে? তারপর একটি Vite প্রজেক্টে একটি query বসিয়ে DevTools-এর Network-এ Slow 3G দিয়ে দেখুন isPending আর isFetching-এর পার্থক্য।

    একটি যুক্তিসঙ্গত উত্তর (একমাত্র নয়):

    • ক্যাটাগরি: ['categories'], কয়েক ঘণ্টা; অ্যাডমিন বদলালে invalidate.
    • বিবরণ: ['products', 'detail', id], কয়েক মিনিট; পণ্য সম্পাদনার পরে ['products'].
    • স্টক: ['stock', id], ০ বা কয়েক সেকেন্ড; অর্ডারের পরে invalidate. চূড়ান্ত যাচাই সার্ভারে।
    • আমার অর্ডার: ['orders', 'mine'], ০; অর্ডারের পরে invalidate. Logout-এ queryClient.clear(), নইলে পরের ব্যবহারকারী আগের জনের ডেটা দেখতে পারেন।

    Optimistic: পছন্দের তালিকা, কার্টে পরিমাণ। Optimistic নয়: অর্ডার নিশ্চিত, পেমেন্ট। ডেমোতে প্রথমবার Skeleton, পরেরবার ডেটা সহ ছোট চিহ্ন দেখা যাবে।

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

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

ক্রমএকটি ব্যর্থ optimistic mutation-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. চলমান query বাতিল করা (cancelQueries)
  2. Cache-এর বর্তমান মানের snapshot রাখা
  3. Cache-এ নতুন মান বসিয়ে পর্দা বদলানো
  4. সার্ভার থেকে error আসা, snapshot ফেরানো
  5. onSettled-এ invalidate করে সার্ভারের সাথে মেলানো

TanStack Query v5-এ staleTime default-এ 0. একটি component-এর ডেটা আসার ১০ সেকেন্ড পরে কী ঘটবে, যদি পাতায় কিছুই না ঘটে?

  • ডেটা cache থেকে মুছে যাবে
  • কিছুই না: ডেটা stale, কিন্তু কোনো ঘটনা ছাড়া অনুরোধ যায় না
  • প্রতি ১০ সেকেন্ডে নতুন অনুরোধ যাবে
  • Query error অবস্থায় চলে যাবে

একটি পাতায় দুটি component useQuery দিয়ে একই key চাইলে production build-এ পাতা খোলার সময় কী হয়?

  • দুটি আলাদা অনুরোধ যায়
  • দ্বিতীয়টি error দেয়, কারণ key ডুপ্লিকেট
  • একটি অনুরোধ যায়, দুজনই একই উত্তর পায়
  • প্রথমটি শেষ হলে দ্বিতীয় অনুরোধ যায়

useQuery-এর ফলে isPending false আর isFetching true. এর অর্থ কী?

  • ডেটা আছে, পেছনে নতুন করে আনা হচ্ছে
  • এখনো কোনো ডেটা নেই
  • অনুরোধ ব্যর্থ হয়েছে
  • Network বন্ধ বলে অনুরোধ থেমে আছে

Default বিন্যাসে সার্ভার সবসময় 503 দিলে একটি useQuery কয়টি অনুরোধ পাঠায়, আর network-এর সময় বাদে কত সেকেন্ড অপেক্ষার পরে error হয়?

  • ১টি অনুরোধ, ০ সেকেন্ড
  • ৩টি অনুরোধ, ৩ সেকেন্ড
  • ৪টি অনুরোধ, ৩ সেকেন্ড
  • ৪টি অনুরোধ, ৭ সেকেন্ড

invalidateQueries({ queryKey: ['products'] }) ডাকলে ['products', 'detail', 7] ঘরে, যার কোনো observer নেই, কী হয়?

  • ঘরটি সাথে সাথে মুছে যায়
  • Stale দাগ পড়ে, কিন্তু অনুরোধ যায় না
  • সাথে সাথে refetch হয়
  • কিছুই হয় না, কারণ key হুবহু মেলেনি

অনলাইন শপে কোন কাজে optimistic update সবচেয়ে কম উপযুক্ত?

  • পণ্যকে পছন্দের তালিকায় যোগ করা
  • কার্টে পরিমাণ এক বাড়ানো
  • পেমেন্ট সম্পন্ন হয়েছে বলে দেখানো
  • পাতার রঙের পছন্দ সেভ করা

শব্দকোষ

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

মনে রাখুন

  1. Server state ধার করা কপি: দূরের, ভাগ করা, পুরোনো হতে পারে।
  2. Query key cache-এর ঠিকানা; query function যা ব্যবহার করে, সব key-তে। একই key মানে একটি অনুরোধ।
  3. staleTime কতক্ষণ বিশ্বাস, gcTime কতক্ষণ রাখা। Refetch হয় ঘটনায়, ঘড়িতে নয়।
  4. Mutation-এর পরে invalidate. Optimistic মানে cancel, snapshot, বদল, rollback, আবার মেলানো।
  5. isPending বনাম isFetching আলাদা; অনুরোধ গুনুন production build-এ; চূড়ান্ত যাচাই সার্ভারে।

পরের পর্বে: এতক্ষণ এক পাতার ভেতরে ডেটা দেখেছেন। এবার অনেক পাতা: React Router-এ route, nested layout, loader আর URL-কে state হিসেবে ব্যবহার করবেন।

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

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