توسعه فرانت‌اند

GraphQL با React: Apollo Client راهنمای عملی

Provider و useQuery با Link محلی مستقل، وابستگی‌ها و سه فایل کامل؛ هویت cache، تازگی، mutation و مرز Optimistic UI.

1404/12/13 3 دقیقه مطالعه 1,597 بازدید

دو مؤلفه اطلاعات یک مقاله را می‌خواهند. یکی عنوان را دریافت می‌کند و دیگری عنوان و وضعیت را. اگر برنامه آن‌ها را دو نسخه بی‌ارتباط نگه دارد، تغییر داده می‌تواند نمایش ناهماهنگ بسازد. Apollo Client ابزارهایی برای اجرای عملیات GraphQL و نگهداری cache فراهم می‌کند، اما هویت موجودیت و سیاست تازگی باید روشن باشند. عبارت «Caching خودکار» همه تصمیم‌های داده را توضیح نمی‌دهد.

انتخاب client به API، پیچیدگی داده و نیاز تیم مربوط است. محبوب‌ترین بودن بدون معیار و منبع، دلیل مناسبی برای انتخاب نیست. GraphQL با schema و عملیات مشخص کار می‌کند؛ Apollo هم رفتار و نسخه API خودش را دارد. برای راهنمای عملی، وابستگی‌ها، importها و شکل داده باید با یک نسخه هماهنگ باشند. کد نسخه‌های مختلف را بدون بررسی در یک مثال ترکیب نکنید.

نسخه و پیش‌نیاز

راهنمای شروع Apollo Client نصب بسته‌های لازم و اتصال Provider را توضیح می‌دهد. این نمونه برای Apollo Client 4 نوشته شده و ابزارهای React از مسیر @apollo/client/react وارد می‌شوند. GraphQL و RxJS نیز در وابستگی نمونه هستند. کد در پروژه React با Vite اجرا می‌شود؛ برای محیط می‌توانید Node.js 24 سازگار با ابزارهای انتخاب‌شده داشته باشید.

با npm create vite@latest apollo-demo -- --template react پروژه تازه بسازید، وارد پوشه آن شوید و npm install را اجرا کنید. سپس npm install @apollo/client@4 graphql rxjs را اجرا کنید. فایل index.html ریشه و src/main.jsx را با نمونه‌های زیر جایگزین و src/main.css را اضافه کنید. lockfile را نگه دارید تا نسخه دقیق مشخص باشد. هیچ سرویس بیرونی یا token برای این نمونه لازم نیست.

Link محلی برای مثال مستقل

راهنمای Apollo Link مسیر پردازش عملیات را توضیح می‌دهد. نمونه از یک link محلی استفاده می‌کند که پس از تأخیر کوتاه، دو رکورد آموزشی برمی‌گرداند. این مسیر schema و سرور واقعی GraphQL نیست و فقط قرارداد پاسخ operation معرفی‌شده را شبیه‌سازی می‌کند. هدف، اتصال client، Provider و useQuery بدون endpoint ناپایدار است.

برای تولید، link شبکه‌ای باید به سرویس واقعی با قرارداد دسترسی و خطا وصل شود. آدرس API، مجوز و شکل schema باید از پروژه خودتان بیایند. اگر operation ناشناخته به link نمونه برسد، خطا می‌دهد تا داده ساختگی به هر درخواست نسبت داده نشود. لغو subscription نیز timer محلی را پاک می‌کند. این رفتار محدود را با مدیریت کامل شبکه و retry یکی ندانید.

<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Apollo محلی آموزشی</title></head><body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>

root در HTML با همان id در main.jsx مصرف می‌شود و مسیر script به فایل موجود اشاره دارد. main.jsx همه تعریف query، link، client و مؤلفه نمایش را دارد. فهرست دو عنوان آموزشی پس از پاسخ محلی نشان داده می‌شود. دکمه دریافت دوباره refetch را فراخوانی می‌کند و هنگام loading غیرفعال است. خطای Promise فراخوانی‌شده مدیریت می‌شود و وضعیت خطا در رابط مسیر جدا دارد.

import React from 'react';
import { createRoot } from 'react-dom/client';
import { ApolloClient, ApolloLink, InMemoryCache, gql } from '@apollo/client';
import { ApolloProvider, useQuery } from '@apollo/client/react';
import { Observable } from 'rxjs';
import './main.css';
const ARTICLES = gql`query EducationalArticles { articles { id title __typename } }`;
const link = new ApolloLink(operation => new Observable(observer => {
  const timer = setTimeout(() => {
    if (operation.operationName !== 'EducationalArticles') {
      observer.error(new Error('Unsupported educational operation.')); return;
    }
    observer.next({ data: { articles: [
      { __typename: 'Article', id: 'demo-1', title: 'عنوان آموزشی اول' },
      { __typename: 'Article', id: 'demo-2', title: 'عنوان آموزشی دوم' }
    ] } });
    observer.complete();
  }, 200);
  return () => clearTimeout(timer);
}));
const client = new ApolloClient({ link, cache: new InMemoryCache() });
function ArticleList() {
  const { data, loading, error, refetch } = useQuery(ARTICLES, { notifyOnNetworkStatusChange: true });
  const articles = data?.articles || [];
  return <main><h1>مقاله‌های آموزشی محلی</h1><p>داده این نمونه از Link محلی می‌آید و سرور بیرونی ندارد.</p>
    <button type="button" disabled={loading} onClick={() => { refetch().catch(() => {}); }}>دریافت دوباره نمونه</button>
    <p role="status">{loading ? 'در حال دریافت نمونه.' : error ? 'دریافت نمونه ناموفق بود.' : `${articles.length} مورد دریافت شد.`}</p>
    {error ? <p>برای ادامه، دریافت دوباره را انتخاب کنید.</p> : <ul>{articles.map(article => <li key={article.id}>{article.title}</li>)}</ul>}
    {!loading && !error && articles.length === 0 && <p>موردی در این مجموعه نیست.</p>}
  </main>;
}
createRoot(document.getElementById('root')).render(<ApolloProvider client={client}><ArticleList /></ApolloProvider>);

CSS با import همان فایل وارد می‌شود و selectorها به عناصر واقعی مثال مربوط‌اند. داده حساس، تصویر و فایل معرفی‌نشده وجود ندارد. با npm run dev نشانی توسعه‌ای که Vite اعلام می‌کند در دسترس است. مثال HTTP به سرور GraphQL بیرونی ارسال نمی‌کند. این راهنما کد و روش اجرا را می‌دهد و ادعای نتیجه یک اجرای شبکه یا آزمون انجام‌شده ندارد.

body{margin:0;background:#f2f4f7;color:#17243b;font-family:Tahoma,Arial,sans-serif;line-height:1.9}main{width:min(38rem,calc(100% - 2rem));margin:auto;padding-block:2rem}button{font:inherit;padding:.7rem 1rem;border:1px solid #64758e;background:white;color:#17243b;border-radius:.4rem}button:focus-visible{outline:3px solid #946000;outline-offset:3px}button:disabled{color:#526176;background:#e2e7ef}li{margin-block:.6rem}

Query و وضعیت قابل مشاهده

مستندات Queries درباره اجرای query و وضعیت آن توضیح می‌دهد. loading، error و data نباید فقط به سه ظاهر بدون قرارداد تبدیل شوند. در دریافت دوباره، ممکن است داده قبلی باقی بماند و همزمان انتظار وجود داشته باشد. رابط باید تفاوت نتیجه موجود و تازه‌سازی جاری را روشن کند. خالی بودن مجموعه با شکست دریافت یکسان نیست.

در نمونه، داده قبلی هنگام refetch می‌تواند دیده شود و پیام status دریافت دوباره را اعلام می‌کند. تعداد رکوردها از داده همان پاسخ می‌آید. اگر هیچ رکورد نباشد، پیام خالی جدا وجود دارد. برای محصول واقعی، errorPolicy و نوع خطا باید با قرارداد schema هماهنگ شوند؛ partial data ممکن است برای برخی صفحه‌ها قابل استفاده باشد و برای برخی تصمیم‌ها کافی نباشد. نیاز مخاطب تعیین‌کننده است.

Cache با هویت موجودیت کار می‌کند

راهنمای cache normalization و هویت بر اساس نوع و شناسه را توضیح می‌دهد. در داده نمونه، هر Article دارای id و __typename است. این اطلاعات کمک می‌کنند cache موجودیت را تشخیص دهد. یکسان بودن title به معنی یکسان بودن رکورد نیست. اگر API شناسه پایدار ندارد، باید درباره keyFields و مدل هویت تصمیم مناسب گرفته شود.

InMemoryCache در حافظه client است و به‌خودی‌خود ذخیره دائمی میان دستگاه‌ها نمی‌سازد. ثبت مقدار در آن به معنی تأیید داده سرور نیست. پس از reload یا ساخت client تازه، عمر داده متفاوت می‌شود مگر مسیر persistence جدا طراحی شده باشد. برای نگهداری محلی، راهنمای storage مرورگر محدودیت‌ها را توضیح می‌دهد. دو cache مختلف را بدون شناخت عمر و دسترسی به هم وصل نکنید.

Fetch Policy تصمیم تازگی است

درخواست از cache یا شبکه به policy وابسته است. انتخاب باید به نوع داده مربوط باشد: عنوان مقاله عمومی با ظرفیت رزرو و مبلغ پرداخت شرایط یکسان ندارد. cache-first می‌تواند برای داده موجود مناسب باشد، اما تازگی تضمین‌شده ایجاد نمی‌کند. refetch یک ابزار برای دریافت دوباره است، نه درمان همه ناسازگاری‌ها. مشخص کنید کدام اتفاق باید داده را تازه کند و چه چیزی در زمان انتظار دیده می‌شود.

اگر داده از چند query آمده، update باید با هویت و fieldهای مربوط سازگار باشد. ناقص بودن برخی fieldها ممکن است به درخواست جدید منتهی شود. برای رفتار field، راهنمای Cache Field Behavior درباره read و merge توضیح دارد. این قواعد باید با schema و آرگومان‌ها هماهنگ باشند؛ ترکیب بی‌دلیل مجموعه‌ها می‌تواند نتایج فیلتر متفاوت را با هم مخلوط کند.

Mutation و به‌روزرسانی فهرست

مستندات Mutations اجرای useMutation و راه‌های هماهنگ کردن داده محلی را شرح می‌دهد. تغییر یک موجودیت با افزودن آن به یک فهرست یکسان نیست. پاسخ mutation ممکن است fieldهای همان id را تازه کند، اما عضویت در فهرست نیاز به قرارداد update یا دریافت دوباره داشته باشد. «خودکار بودن» را به معنی حل همه رابطه‌های داده معرفی نکنید.

بعد از شکست، ورودی کاربر حفظ شود و نتیجه موفق پیش از تأیید گزارش نشود. عملیات تکراری می‌تواند به سیاست idempotency در سرور نیاز داشته باشد. Cache مسئول مجوز نیست و سرویس باید دسترسی را بررسی کند. راهنمای REST API درباره قرارداد خطا و نتیجه توضیح دارد؛ GraphQL هم به همین وضوح در سطح schema و resolver نیاز دارد، هرچند شکل پروتکل متفاوت است.

Optimistic UI نتیجه موقت است

راهنمای Optimistic Mutation Results مسیر ارائه نتیجه خوش‌بینانه و جایگزینی آن با نتیجه واقعی را توضیح می‌دهد. این روش می‌تواند بازخورد فوری بسازد، اما باید شکست و بازگشت حالت را طراحی کرد. برای عمل حساس، نمایش موقت نباید موفقیت قطعی تلقی شود. shape و هویت نتیجه خوش‌بینانه نیز باید با cache هماهنگ بماند.

اگر کاربر پیش از شکست عمل بعدی را انجام دهد، برگشت یک تغییر می‌تواند بر وضعیت تازه اثر داشته باشد. طراحی باید وابستگی‌ها را بشناسد. برای افزودن رکورد، شناسه موقت و نهایی رابطه مشخص می‌خواهند. راهنمای بازخورد کوچک به هماهنگی پیام و state کمک می‌کند. سرعت ظاهری وقتی قابل اعتماد است که موقت بودن و نتیجه واقعی قابل فهم باشند.

Pagination و حریم خصوصی

در صفحه‌بندی، آرگومان فیلتر، ترتیب و cursor باید در قرارداد cache دیده شوند. merge ساده همه پاسخ‌ها می‌تواند رکورد تکراری یا مجموعه نامرتبط بسازد. پایان فهرست و دریافت بیشتر را روشن نمایش دهید. برای SSR، client و cache مشترک میان درخواست کاربران می‌تواند اطلاعات را مخلوط کند؛ عمر نمونه باید با معماری سرور هماهنگ باشد. این مثال صرفاً در مرورگر و داده عمومی محلی است.

پس از خروج یا تغییر حساب، داده خصوصی قبلی نباید در UI باقی بماند. سیاست reset و دریافت دوباره باید با سرویس هویت هماهنگ شود. در تحویل، نسخه، queryها، هویت cache و رفتار خطا را ثبت کنید. Apollo Client زمانی مدیریت داده را روشن‌تر می‌کند که قراردادها مشخص باشند؛ Provider و hookها ابزار اجرای آن قراردادند و جای تصمیم درباره اعتبار، تازگی و دسترسی را نمی‌گیرند.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!