اگر اپلیکیشن React شما درخواستهای API زیادی ارسال میکند، همیشه مشکل از HTTP Client نیست. گاهی مشکل اصلی این است که درخواستها چه زمانی و با چه فاصلهای اجرا میشوند.
چند مثال آشنا:
با هر بار تایپ کاربر در Search یک درخواست ارسال میشود.
هنگام ویرایش متن، چند درخواست Autosave همزمان اجرا میشوند.
هنگام اسکرول، محاسبات و بررسیهای تکراری انجام میشوند.
با انتخاب تعداد زیادی فایل، همه آپلودها همزمان شروع میشوند.
هر رویداد Analytics یک درخواست مستقل ایجاد میکند.
در این مقاله، شش الگوی کاربردی را با استفاده از سه کتابخانه بررسی میکنیم:
TanStack Pacer: کنترل زمان اجرا و تعداد دفعات اجرای عملیات
TanStack Query: مدیریت Server State، کش و چرخه عمر درخواستها
tanstack-fetch: مدیریت درخواستهای HTTP و خطاهای تایپشده
من Mohammad Garmabi، سازنده کتابخانه tanstack-fetch هستم. این کتابخانه یک پروژه مستقل جامعه توسعهدهندگان است و پروژه رسمی تیم TanStack نیست.
ابتدا پکیجهای موردنیاز را نصب میکنیم:
npm install @tanstack/react-pacer @tanstack/react-query tanstack-fetch
سپس یک Client مشترک میسازیم:
// src/lib/api.ts import { createFetch } from 'tanstack-fetch' export const api = createFetch({ baseUrl: import.meta.env.VITE_API_URL, timeoutMs: 10_000, })
در این معماری، هر کتابخانه مسئولیت مشخصی دارد. Pacer زمان اجرای عملیات را کنترل میکند، Query مسئول کش و وضعیت دادههاست و tanstack-fetch ارتباط HTTP را مدیریت میکند.
نمونههای این مقاله بر اساس API نسخههای موردنظر نوشته شدهاند. پیش از استفاده در پروژه واقعی، امضای متدها را با نسخه نصبشده و مستندات فعلی بررسی کنید.
فرض کنید کاربر عبارت tanstack را جستوجو میکند. اگر با هر کاراکتر یک درخواست بفرستیم، تعداد زیادی درخواست غیرضروری ایجاد میشود.
با Debounce میتوانیم صبر کنیم تا کاربر برای مدت کوتاهی تایپ را متوقف کند.
import { useState } from 'react' import { useDebouncedValue } from '@tanstack/react-pacer' import { keepPreviousData, useQuery } from '@tanstack/react-query' import { api } from '~/lib/api' type User = { id: number name: string email: string } export function SearchUsers() { const [search, setSearch] = useState('') const [debouncedSearch] = useDebouncedValue(search, { wait: 300, }) const query = useQuery({ queryKey: ['users', 'search', debouncedSearch], queryFn: ({ signal }) => api.get<User[]>('/users', { query: { q: debouncedSearch }, signal, }), enabled: debouncedSearch.trim().length >= 2, placeholderData: keepPreviousData, staleTime: 30_000, }) return ( <> <input value={search} ={(event) => setSearch(event.target.value)} placeholder="جستوجوی کاربران" /> {query.isFetching && <span>در حال جستوجو...</span>} {query.data?.map((user) => ( <div key={user.id}>{user.name}</div> ))} </> ) }
چند نکته مهم در این پیادهسازی وجود دارد:
مقدار Input بلافاصله تغییر میکند و تایپ کاربر کند نمیشود.
فقط مقدار debouncedSearch وارد queryKey میشود.
هر عبارت جستوجو میتواند کش مستقل خودش را داشته باشد.
با keepPreviousData، نتایج قبلی هنگام بارگذاری نتایج جدید باقی میمانند.
با مصرف signal در queryFn، درخواستهای قبلی امکان لغو شدن دارند.
نکته: بهتر است خود queryFn را با یک تابع Debounce معمولی Wrap نکنید. مقدار ورودی به queryKey را Debounce کنید تا قرارداد Promise مورد انتظار TanStack Query حفظ شود.
در قابلیت Autosave، هدف این است که بعد از توقف تایپ، آخرین نسخه متن ذخیره شود. اما فقط تأخیر انداختن درخواست کافی نیست؛ باید به درخواستهایی که قبلاً شروع شدهاند هم توجه کنیم.
یک جریان مناسب شامل این مراحل است:
۱. بعد از توقف تایپ، عملیات ذخیره اجرا شود.
۲. درخواست PATCH همراه با AbortSignal ارسال شود.
۳. در صورت کنار گذاشتن تغییرات، عملیات در انتظار لغو شود و درخواست در حال اجرا نیز در صورت امکان Abort شود.
۴. پس از دریافت پاسخ موفق، کش TanStack Query بهروز شود.
برای مدیریت زمانبندی عملیات غیرهمزمان میتوان از useAsyncDebouncer در TanStack Pacer استفاده کرد و AbortSignal را به درخواست tanstack-fetch داد.
با این حال، یک نکته مهم را نباید فراموش کنیم: لغو درخواست در مرورگر لزوماً به معنی لغو پردازش آن در سرور نیست. اگر صحت داده اهمیت زیادی دارد، بهتر است از مکانیزمهایی مانند Version Check یا Optimistic Concurrency در سمت سرور هم استفاده کنیم.
رویدادهای Scroll میتوانند در هر ثانیه چندین بار اجرا شوند. اگر در هر رویداد موقعیت اسکرول را بررسی کنیم، محاسبات تکراری خواهیم داشت.
با Throttle میتوانیم تعداد دفعات اجرای Callback را محدود کنیم:
const = useThrottledCallback( (element: HTMLElement) => { const nearBottom = element.scrollTop + element.clientHeight > element.scrollHeight - 600 if (nearBottom && hasNextPage && !isFetchingNextPage) { void fetchNextPage() } }, { wait: 200 }, )
در این مثال، تابع در هر بازه زمانی حداکثر با محدودیت تعریفشده اجرا میشود. TanStack Query همچنان مسئول مدیریت صفحهها و وضعیت درخواست است.
برای بعضی رابطهای کاربری، استفاده از IntersectionObserver و یک عنصر مشخص در انتهای لیست نیز انتخاب مناسبی است.
فرض کنید یک API موقعیتیابی، حداکثر پنج درخواست در ثانیه را مجاز میداند. میتوانیم پیش از ارسال درخواست، تعداد عملیات را در سمت کلاینت کنترل کنیم.
const limiter = useAsyncRateLimiter( (address: string) => api.get<Place[]>('/geocode', { query: { address }, }), { limit: 5, window: 1_000, windowType: 'sliding', onReject: (_args, rateLimiter) => { const seconds = Math.ceil( rateLimiter.getMsUntilNextWindow() / 1000, ) toast(`لطفاً ${seconds} ثانیه دیگر تلاش کنید`) }, }, )
این روش به کاهش درخواستهای غیرضروری و ارائه بازخورد سریع به کاربر کمک میکند.
اما Rate Limiting سمت کلاینت، محدودیت امنیتی یا سراسری نیست. کاربر میتواند از تب یا دستگاه دیگری درخواست بفرستد. بنابراین سرور باید محدودیت واقعی را اعمال کند.
برای پاسخ HTTP با کد 429 Too Many Requests نیز میتوان سیاست Retry را در لایه Transport تعریف کرد و در صورت پشتیبانی نسخه نصبشده، مقدار Retry-After را در نظر گرفت.
فرض کنید کاربر ۲۰۰ تصویر را برای آپلود انتخاب کرده است. شروع همزمان تمام درخواستها میتواند فشار زیادی به مرورگر، شبکه و سرور وارد کند.
با Queue میتوان تعداد درخواستهای فعال را محدود کرد:
const queue = useAsyncQueuer( (job: UploadJob) => api.upload<Photo>('/photos', { file: job.file, fields: { albumId: job.albumId }, }), { concurrency: 3, wait: 100, onSuccess: (_photo, job) => queryClient.invalidateQueries({ queryKey: ['albums', job.albumId, 'photos'], }), : (error, job) => console.error(`Upload failed: ${job.file.name}`, error), }, )
در این مثال، حداکثر سه آپلود بهصورت همزمان فعال هستند و سایر فایلها در صف باقی میمانند.
این ساختار میتواند پایهای برای امکانات زیر باشد:
نمایش درصد پیشرفت آپلودها
توقف و ادامه صف
اولویتبندی فایلها
مدیریت خطای هر فایل بهصورت مستقل
به محدود کردن حجم کار همزمان، Backpressure گفته میشود؛ یعنی بهجای شروع تمام عملیات، نرخ ورود کار به سیستم را کنترل میکنیم.
فرض کنید در یک صفحه تعداد زیادی رویداد ثبت میشود. ارسال یک درخواست HTTP برای هر رویداد، سربار غیرضروری ایجاد میکند.
با Batcher میتوان چند رویداد را در یک درخواست ارسال کرد:
const batcher = useAsyncBatcher( (events: TrackEvent[]) => api.post('/events', { body: { events } }), { maxSize: 20, wait: 2_000, : (error, batch) => console.warn(`Batch of ${batch.length} events failed`, error), }, ) batcher.addItem({ name: 'button_clicked', at: Date.now(), props: { page: '/dashboard' }, })
در این مثال، رویدادها با رسیدن به سقف تعیینشده یا بر اساس زمانبندی Batcher ارسال میشوند.
برای استفاده در محیط Production باید درباره Retry، جلوگیری از ثبت تکراری رویدادها و ارسال رویدادهای باقیمانده هنگام مخفی شدن صفحه نیز تصمیم بگیریم.
TanStack Query، Pacer و HTTP Client ممکن است هرکدام امکان Retry داشته باشند.
اگر هر سه لایه بهصورت مستقل یک عملیات را Retry کنند، یک درخواست ناموفق میتواند به تعداد زیادی درخواست تبدیل شود.
بهتر است مسئولیتها را تفکیک کنیم:
لایه HTTP: رفتارهای وابسته به وضعیت پاسخ مانند 429 و Retry-After
TanStack Query: سیاست Retry برای Queryها
TanStack Pacer: Retry برای کارهای صفبندیشده یا Batchهایی که مسئولیت آنها در جای دیگری مدیریت نمیشود
برای مثال، میتوان در Query از Retry کردن بیشتر خطاهای سمت کلاینت جلوگیری کرد:
import { isFetchError } from 'tanstack-fetch' retry: (count, error) => !(isFetchError(error) && error.status < 500) && count < 3
این صرفاً یک سیاست نمونه است و باید با رفتار واقعی API شما تطبیق داده شود.
نیازابزارجستوجو و فیلترهاuseDebouncedValueAutosaveuseAsyncDebouncerرویدادهای Scroll و ResizeuseThrottledCallbackمحدودیت تعداد درخواستهاuseAsyncRateLimiterآپلود گروهیuseAsyncQueuerتجمیع رویدادهای AnalyticsuseAsyncBatcher
برای جزئیات API و گزینههای هر ابزار، مستندات TanStack Pacer را بررسی کنید.
برای ساخت یک لایه ارتباطی قابل نگهداری در React، بهتر است مسئولیتها را از هم جدا کنیم:
TanStack Pacer مشخص میکند عملیات چه زمانی اجرا شود.
TanStack Query وضعیت دادههای سمت سرور، کش و چرخه عمر Queryها را مدیریت میکند.
tanstack-fetch مسئول ارسال درخواستهای HTTP و مدیریت رفتارهای لایه Transport است.
این تفکیک باعث میشود کنترل درخواستها، کش، لغو عملیات و مدیریت خطاها سادهتر و قابلفهمتر باشد.
نویسنده: Mohammad Garmabi، سازنده tanstack-fetch؛ یک کتابخانه مستقل HTTP Client برای اکوسیستم فرانتاند.