دليل React و Apollo الشامل لعام 2020: أمثلة واقعية لتطوير تطبيقات GraphQL

دقائق القراءة: 18

دليل React و Apollo الشامل لعام 2020: أمثلة واقعية لتطوير تطبيقات GraphQL

إذا كنت تتطلع إلى بناء تطبيقات ويب متطورة باستخدام React و GraphQL، فإن مكتبة Apollo هي الأداة المثالية التي يجب عليك استخدامها. لقد قمت بتجميع هذا الدليل الشامل الذي يستعرض جميع المفاهيم الأساسية في مكتبة Apollo، موضحًا لك كيفية استخدامها مع React من البداية إلى النهاية، مع التركيز على الأمثلة الواقعية والتطبيقات العملية.

جدول المحتويات

ما هو Apollo ولماذا نحتاجه؟

Apollo هي مكتبة تجمع بين تقنيتين مفيدتين للغاية تُستخدمان لبناء تطبيقات الويب والجوال: React و GraphQL. صُممت React لإنشاء تجارب مستخدم رائعة باستخدام JavaScript، بينما تُعد GraphQL لغة جديدة ومباشرة وتصريحية لجلب وتغيير البيانات بسهولة وفعالية أكبر، سواء كانت من قاعدة بيانات أو حتى من ملفات ثابتة.

يعمل Apollo كـ"لاصق" يربط هاتين الأداتين معًا. بالإضافة إلى ذلك، فإنه يسهل العمل مع React و GraphQL بشكل كبير من خلال توفير العديد من خطافات React (React hooks) والميزات المخصصة التي تمكننا من كتابة عمليات GraphQL وتنفيذها باستخدام كود JavaScript. سنغطي هذه الميزات بعمق على مدار هذا الدليل.

إعداد Apollo Client

الإعداد الأساسي لـ Apollo Client

إذا كنت تبدأ مشروعًا باستخدام قالب React مثل Create React App، فستحتاج إلى تثبيت التبعيات التالية كقاعدة أساسية للبدء في العمل مع Apollo Client:

// باستخدام npm:
npm i @apollo/react-hooks apollo-boost graphql

// باستخدام yarn:
yarn add @apollo/react-hooks apollo-boost graphql
  • @apollo/react-hooks: يوفر لنا خطافات React التي تجعل أداء عملياتنا والعمل مع Apollo Client أفضل وأكثر سلاسة.
  • apollo-boost: يساعدنا في إعداد Client بالإضافة إلى تحليل عمليات GraphQL التي نكتبها.
  • graphql: يتولى أيضًا مهمة تحليل عمليات GraphQL (بالإضافة إلى gql).

إعداد Apollo Client مع الاشتراكات (Subscriptions)

لاستخدام جميع أنواع عمليات GraphQL (الاستعلامات queries، التعديلات mutations، والاشتراكات subscriptions)، نحتاج إلى تثبيت تبعيات أكثر تحديدًا مقارنة بـ apollo-boost فقط:

// باستخدام npm:
npm i @apollo/react-hooks apollo-client graphql graphql-tag apollo-cache-inmemory apollo-link-ws

// باستخدام yarn:
yarn add @apollo/react-hooks apollo-client graphql graphql-tag apollo-cache-inmemory apollo-link-ws
  • apollo-client: يمنحنا Client مباشرة، بدلاً من الحصول عليه من apollo-boost.
  • graphql-tag: مدمج في apollo-boost، ولكنه غير متضمن في apollo-client بشكل افتراضي.
  • apollo-cache-inmemory: ضروري لإعداد الذاكرة المؤقتة (cache) الخاصة بنا (والتي يقوم apollo-boost، بالمقارنة، بإعدادها تلقائيًا).
  • apollo-link-ws: ضروري للتواصل عبر مآخذ الويب (websockets)، والتي تتطلبها الاشتراكات.

إنشاء Apollo Client جديد

إنشاء Apollo Client (الإعداد الأساسي)

أبسط إعداد لإنشاء Apollo Client هو عن طريق إنشاء مثيل جديد للعميل وتوفير خاصية uri فقط، والتي ستكون نقطة نهاية GraphQL الخاصة بك:

import ApolloClient from "apollo-boost";

const client = new ApolloClient({
  uri: "https://your-graphql-endpoint.com/api/graphql",
});

تم تطوير apollo-boost لجعل عمليات مثل إنشاء Apollo Client سهلة قدر الإمكان. ومع ذلك، ما يفتقر إليه في الوقت الحالي هو دعم اشتراكات GraphQL عبر اتصال websocket. بشكل افتراضي، يقوم بتنفيذ العمليات عبر اتصال http (كما ترى من خلال uri المقدمة أعلاه).

باختصار، استخدم apollo-boost لإنشاء عميلك إذا كنت تحتاج فقط إلى تنفيذ الاستعلامات (queries) والتعديلات (mutations) في تطبيقك. يقوم بإعداد ذاكرة مؤقتة داخل الذاكرة (in-memory cache) بشكل افتراضي، وهو أمر مفيد لتخزين بيانات تطبيقنا محليًا. يمكننا القراءة من الذاكرة المؤقتة والكتابة إليها لمنع الحاجة إلى تنفيذ استعلاماتنا بعد تحديث بياناتنا. سنغطي كيفية القيام بذلك لاحقًا.

إنشاء Apollo Client (إعداد الاشتراكات)

تُعد الاشتراكات مفيدة لعرض نتائج تغييرات البيانات (من خلال التعديلات mutations) في تطبيقنا بسهولة أكبر. بشكل عام، نستخدم الاشتراكات كنوع محسن من الاستعلامات. تستخدم الاشتراكات اتصال websocket "للاشتراك" في التحديثات والبيانات، مما يتيح عرض البيانات الجديدة أو المحدثة للمستخدمين على الفور دون الحاجة إلى إعادة تنفيذ الاستعلامات أو تحديث الذاكرة المؤقتة.

import ApolloClient from "apollo-client";
import { WebSocketLink } from "apollo-link-ws";
import { InMemoryCache } from "apollo-cache-inmemory";

const client = new ApolloClient({
  link: new WebSocketLink({
    uri: "wss://your-graphql-endpoint.com/v1/graphql",
    options: {
      reconnect: true,
      connectionParams: {
        headers: {
          Authorization: "Bearer yourauthtoken",
        },
      },
    },
  }),
  cache: new InMemoryCache(),
});

توفير Client لمكونات React

بعد إنشاء Client جديد، يعد تمريره إلى جميع المكونات أمرًا ضروريًا لتتمكن من استخدامه داخل مكوناتنا لأداء جميع عمليات GraphQL المتاحة. يتم توفير Client لشجرة المكونات بأكملها باستخدام سياق React (React Context)، ولكن بدلاً من إنشاء سياقنا الخاص، نقوم باستيراد موفر سياق خاص من @apollo/react-hooks يسمى ApolloProvider.

يمكننا أن نرى كيف يختلف عن سياق React العادي بسبب وجود خاصية خاصة، client، مصممة خصيصًا لقبول Client الذي تم إنشاؤه. لاحظ أن كل هذا الإعداد يجب أن يتم في ملف index.js أو App.js (حيثما يتم تعريف مساراتك) بحيث يمكن تغليف الموفر (Provider) حول جميع مكوناتك.

import { ApolloProvider } from "@apollo/react-hooks";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <ApolloProvider client={client}>
      <BrowserRouter>
        <Switch>
          <Route exact path="/" component={App} />
          <Route exact path="/new" component={NewPost} />
          <Route exact path="/edit/:id" component={EditPost} />
        </Switch>
      </BrowserRouter>
    </ApolloProvider>
  </React.StrictMode>,
  rootElement
);

استخدام Client مباشرة

يُعد Apollo Client الجزء الأكثر أهمية في المكتبة نظرًا لأنه مسؤول عن تنفيذ جميع عمليات GraphQL التي نريد أداءها باستخدام React. يمكننا استخدام Client الذي تم إنشاؤه مباشرة لأداء أي عملية نريدها. يحتوي على طرق تتوافق مع الاستعلامات (client.query())، والتعديلات (client.mutate())، والاشتراكات (client.subscribe()). تقبل كل طريقة كائنًا وخصائصها المقابلة:

// تنفيذ الاستعلامات
client
  .query({
    query: GET_POSTS,
    variables: { limit: 5 },
  })
  .then((response) => console.log(response.data))
  .catch((err) => console.error(err));

// تنفيذ التعديلات
client
  .mutate({
    mutation: CREATE_POST,
    variables: { title: "Hello", body: "World" },
  })
  .then((response) => console.log(response.data))
  .catch((err) => console.error(err));

// تنفيذ الاشتراكات
client
  .subscribe({
    subscription: GET_POST,
    variables: { id: "8883346c-6dc3-4753-95da-0cc0df750721" },
  })
  .then((response) => console.log(response.data))
  .catch((err) => console.error(err));

ومع ذلك، قد يكون استخدام Client مباشرة أمرًا معقدًا بعض الشيء، حيث أنه عند إجراء طلب، فإنه يعيد وعدًا (promise). لحل كل وعد، نحتاج إما إلى دوال رد النداء (callbacks) مثل .then() و .catch() كما هو موضح أعلاه، أو استخدام الكلمة المفتاحية await لكل وعد داخل دالة معلنة بالكلمة المفتاحية async.

كتابة GraphQL في ملفات .js باستخدام gql

لاحظ أعلاه أنني لم أحدد محتويات المتغيرات GET_POSTS و CREATE_POST و GET_POST. إنها العمليات المكتوبة في صيغة GraphQL التي تحدد كيفية تنفيذ الاستعلام والتعديل والاشتراك على التوالي. إنها ما نكتبه في أي وحدة تحكم GraphiQL للحصول على البيانات وتغييرها.

المشكلة هنا، ومع ذلك، هي أننا لا نستطيع كتابة وتنفيذ تعليمات GraphQL في ملفات JavaScript (.js)، مثلما يجب أن يكون كود React الخاص بنا. لتحليل عمليات GraphQL، نستخدم دالة خاصة تسمى "قالب حرفي موسوم" (tagged template literal) للسماح لنا بالتعبير عنها كسلاسل JavaScript. هذه الدالة تُسمى gql.

// إذا كنت تستخدم apollo-boost
import { gql } from "apollo-boost";

// وإلا، يمكنك استخدام حزمة مخصصة graphql-tag
import gql from "graphql-tag";

// استعلام
const GET_POSTS = gql`
  query GetPosts($limit: Int) {
    posts(limit: $limit) {
      id
      body
      title
      createdAt
    }
  }
`;

// تعديل
const CREATE_POST = gql`
  mutation CreatePost($title: String!, $body: String!) {
    insert_posts(objects: { title: $title, body: $body }) {
      affected_rows
    }
  }
`;

// اشتراك
const GET_POST = gql`
  subscription GetPost($id: uuid!) {
    posts(where: { id: { _eq: $id } }) {
      id
      body
      title
      createdAt
    }
  }
`;

خطافات React الأساسية في Apollo

خطاف useQuery

يُعد خطاف useQuery بلا شك الطريقة الأكثر ملاءمة لأداء استعلام GraphQL، مع الأخذ في الاعتبار أنه لا يعيد وعدًا (promise) يحتاج إلى حل. يتم استدعاؤه في الجزء العلوي من أي مكون دالة (كما يجب أن تكون جميع الخطافات) ويتلقى كحجة أولى مطلوبة استعلامًا تم تحليله بواسطة gql.

يُفضل استخدامه عندما تكون لديك استعلامات يجب تنفيذها فورًا، عند عرض المكون، مثل قائمة بيانات يرغب المستخدم في رؤيتها فور تحميل الصفحة. يعيد useQuery كائنًا يمكننا من خلاله تفكيك القيم التي نحتاجها بسهولة. عند تنفيذ استعلام، هناك ثلاث قيم أساسية سنحتاج إلى استخدامها داخل كل مكون نجلب فيه البيانات. وهي loading و error و data.

const GET_POSTS = gql`
  query GetPosts($limit: Int) {
    posts(limit: $limit) {
      id
      body
      title
      createdAt
    }
  }
`;

function App() {
  const { loading, error, data } = useQuery(GET_POSTS, {
    variables: { limit: 5 },
  });

  if (loading) return <div> Loading... </div>;
  if (error) return <div> Error! </div>;

  return data.posts.map((post) => <Post key={post.id} post={post} />);
}

قبل أن نتمكن من عرض البيانات التي نجلبها، نحتاج إلى التعامل مع حالة التحميل (عندما تكون loading مضبوطة على true) وحالة محاولة جلب البيانات. في تلك المرحلة، نعرض div بنص "Loading" أو مؤشر تحميل. نحتاج أيضًا إلى التعامل مع احتمال وجود خطأ في جلب استعلامنا، مثل وجود خطأ في الشبكة أو إذا ارتكبنا خطأ في كتابة استعلامنا (خطأ في الصيغة).

بمجرد الانتهاء من التحميل وعدم وجود خطأ، يمكننا استخدام بياناتنا في مكوننا، عادةً لعرضها لمستخدمينا (كما هو الحال في المثال أعلاه). هناك قيم أخرى يمكننا تفكيكها من الكائن الذي يعيده useQuery، ولكنك ستحتاج إلى loading و error و data في كل مكون تقريبًا حيث تقوم بتنفيذ useQuery.

خطاف useLazyQuery

يوفر خطاف useLazyQuery طريقة أخرى لأداء استعلام، والذي يهدف إلى التنفيذ في وقت ما بعد عرض المكون أو استجابة لتغيير معين في البيانات. يُعد useLazyQuery مفيدًا جدًا للأشياء التي تحدث في أي نقطة زمنية غير معروفة، مثل الاستجابة لعملية بحث المستخدم.

function Search() {
  const [query, setQuery] = React.useState("");
  const [searchPosts, { data, loading, called }] = useLazyQuery(SEARCH_POSTS, {
    variables: { query: `%${query}%` },
  });
  const [results, setResults] = React.useState([]);

  React.useEffect(() => {
    if (!query) return;
    // دالة تنفيذ الاستعلام لا تعيد وعدًا
    searchPosts();
    if (data) {
      setResults(data.posts);
    }
  }, [query, data, searchPosts]);

  if (called && loading) return <div> Loading... </div>;

  return results.map((result) => (
    <SearchResult key={result.id} result={result} />
  ));
}

يختلف useLazyQuery عن useQuery، أولاً وقبل كل شيء، فيما يتم إرجاعه من الخطاف. فهو يعيد مصفوفة يمكننا تفكيكها، بدلاً من كائن. نظرًا لأننا نريد تنفيذ هذا الاستعلام في وقت ما بعد تحميل المكون، فإن العنصر الأول الذي يمكننا تفكيكه هو دالة يمكنك استدعاؤها لأداء هذا الاستعلام عندما تختار. تُسمى دالة الاستعلام هذه searchPosts في المثال أعلاه.

القيمة الثانية المفككة في المصفوفة هي كائن، يمكننا استخدام تفكيك الكائنات عليه ومن خلاله يمكننا الحصول على جميع الخصائص نفسها التي حصلنا عليها من useQuery، مثل loading و error و data. نحصل أيضًا على خاصية مهمة تسمى called، والتي تخبرنا ما إذا كنا قد استدعينا هذه الدالة بالفعل لأداء استعلامنا. في هذه الحالة، إذا كانت called صحيحة و loading صحيحة، فإننا نريد إرجاع "Loading…" بدلاً من بياناتنا الفعلية، لأننا ننتظر إرجاع البيانات. هذه هي الطريقة التي يتعامل بها useLazyQuery مع جلب البيانات بطريقة متزامنة دون أي وعود.

لاحظ أننا نمرر مرة أخرى أي متغيرات مطلوبة لعملية الاستعلام كخاصية، variables، إلى الحجة الثانية. ومع ذلك، إذا احتجنا، يمكننا تمرير تلك المتغيرات على كائن يتم توفيره لدالة الاستعلام نفسها.

خطاف useMutation

الآن بعد أن عرفنا كيفية تنفيذ الاستعلامات البطيئة (lazy queries)، نعرف بالضبط كيفية العمل مع خطاف useMutation. مثل خطاف useLazyQuery، فإنه يعيد مصفوفة يمكننا تفكيكها إلى عنصريها. في العنصر الأول، نحصل على دالة، والتي في هذه الحالة، يمكننا استدعاؤها لأداء عملية التعديل (mutation) الخاصة بنا. بالنسبة للعنصر التالي، يمكننا مرة أخرى تفكيك كائن يعيد لنا loading و error و data.

import { useMutation } from "@apollo/react-hooks";
import { gql } from "apollo-boost";

const CREATE_POST = gql`
  mutation CreatePost($title: String!, $body: String!) {
    insert_posts(objects: { body: $body, title: $title }) {
      affected_rows
    }
  }
`;

function NewPost() {
  const [title, setTitle] = React.useState("");
  const [body, setBody] = React.useState("");
  const [createPost, { loading, error }] = useMutation(CREATE_POST);

  function handleCreatePost(event) {
    event.preventDefault();
    // دالة التعديل لا تعيد وعدًا أيضًا
    createPost({ variables: { title, body } });
  }

  return (
    <div>
      <h1> New Post </h1>
      <form onSubmit={handleCreatePost}>
        <input onChange={(event) => setTitle(event.target.value)} />
        <textarea onChange={(event) => setBody(event.target.value)} />
        <button disabled={loading} type="submit"> Submit </button>
        {error && <p>{error.message}</p>}
      </form>
    </div>
  );
}

على عكس الاستعلامات، لا نستخدم loading أو error لعرض شيء بشكل شرطي. نستخدم loading بشكل عام في حالات مثل عندما نرسل نموذجًا لمنع إرساله عدة مرات، لتجنب تنفيذ نفس التعديل دون داع (كما ترى في المثال أعلاه). نستخدم error لعرض ما يحدث خطأ في التعديل الخاص بنا للمستخدمين. على سبيل المثال، إذا لم يتم توفير بعض القيم المطلوبة لتعديلنا، يمكننا بسهولة استخدام بيانات الخطأ هذه لعرض رسالة خطأ بشكل شرطي داخل الصفحة حتى يتمكن المستخدم من إصلاح ما يحدث خطأ.

بالمقارنة مع تمرير المتغيرات إلى الحجة الثانية لـ useMutation، يمكننا الوصول إلى بعض دوال رد النداء المفيدة عندما تحدث أشياء معينة، مثل عند اكتمال التعديل وعند وجود خطأ. تُسمى دوال رد النداء هذه onCompleted و onError. تمنحنا دالة رد النداء onCompleted الوصول إلى بيانات التعديل المعادة وهي مفيدة جدًا للقيام بشيء ما عند الانتهاء من التعديل، مثل الانتقال إلى صفحة مختلفة. تمنحنا دالة رد النداء onError الخطأ المعادل عندما تكون هناك مشكلة في التعديل وتمنحنا أنماطًا أخرى للتعامل مع أخطائنا.

const [createPost, { loading, error }] = useMutation(CREATE_POST, {
  onCompleted: (data) => console.log("Data from mutation", data),
  onError: (error) => console.error("Error creating a post", error),
});

خطاف useSubscription

يعمل خطاف useSubscription تمامًا مثل خطاف useQuery. يعيد useSubscription كائنًا يمكننا تفكيكه، يتضمن نفس الخصائص: loading و data و error. يقوم بتنفيذ اشتراكنا فورًا عند عرض المكون. هذا يعني أننا بحاجة إلى التعامل مع حالات التحميل والأخطاء، وبعد ذلك فقط نعرض/نستخدم بياناتنا.

import { useSubscription } from "@apollo/react-hooks";
import gql from "graphql-tag";

const GET_POST = gql`
  subscription GetPost($id: uuid!) {
    posts(where: { id: { _eq: $id } }) {
      id
      body
      title
      createdAt
    }
  }
`;

// حيث يأتي الـ id من معلمات المسار -> /post/:id
function PostPage({ id }) {
  const { loading, error, data } = useSubscription(GET_POST, {
    variables: { id },
    // shouldResubscribe: true (افتراضي: false)
    // onSubscriptionData: data => console.log('new data', data)
    // fetchPolicy: 'network-only' (افتراضي: 'cache-first')
  });

  if (loading) return <div> Loading... </div>;
  if (error) return <div> Error! </div>;

  const post = data.posts[0];
  return (
    <div>
      <h1> {post.title} </h1>
      <p> {post.body} </p>
    </div>
  );
}

تمامًا مثل useQuery و useLazyQuery و useMutation، يقبل useSubscription خاصية variables المقدمة كحجة ثانية. ومع ذلك، فإنه يقبل أيضًا بعض الخصائص المفيدة مثل shouldResubscribe. هذه قيمة منطقية (boolean)، والتي ستسمح لاشتراكنا بإعادة الاشتراك تلقائيًا عند تغيير خصائصنا. هذا مفيد عندما نمرر متغيرات إلى خصائص مركز الاشتراك الخاص بنا والتي نعلم أنها ستتغير. بالإضافة إلى ذلك، لدينا دالة رد نداء تسمى onSubscriptionData، والتي تمكننا من استدعاء دالة كلما تلقى خطاف الاشتراك بيانات جديدة. أخيرًا، يمكننا تعيين fetchPolicy، والذي يكون افتراضيًا "cache-first".

وصفات أساسية

تحديد سياسة الجلب (Fetch Policy) يدويًا

من الميزات المفيدة جدًا في Apollo أنه يأتي بذاكرته المؤقتة (cache) الخاصة به، والتي يستخدمها لإدارة البيانات التي نستعلم عنها من نقطة نهاية GraphQL الخاصة بنا. ومع ذلك، في بعض الأحيان نجد أنه بسبب هذه الذاكرة المؤقتة، لا يتم تحديث الأشياء في واجهة المستخدم بالطريقة التي نريدها.

في كثير من الحالات، لا يحدث ذلك، كما في المثال أدناه، حيث نقوم بتحرير منشور في صفحة التعديل، ثم بعد تحرير منشورنا، ننتقل إلى الصفحة الرئيسية لرؤيته في قائمة جميع المنشورات، ولكننا نرى البيانات القديمة بدلاً من ذلك:

// المسار: /edit/:postId
function EditPost({ id }) {
  const { loading, data } = useQuery(GET_POST, { variables: { id } });
  const [title, setTitle] = React.useState(loading ? data?.posts[0].title : "");
  const [body, setBody] = React.useState(loading ? data?.posts[0].body : "");

  const [updatePost] = useMutation(UPDATE_POST, {
    // بعد تحديث المنشور، نذهب إلى الصفحة الرئيسية
    onCompleted: () => history.push("/"),
  });

  function handleUpdatePost(event) {
    event.preventDefault();
    updatePost({ variables: { title, body, id } });
  }

  return (
    <form onSubmit={handleUpdatePost}>
      <input onChange={(event) => setTitle(event.target.value)} defaultValue={title} />
      <input onChange={(event) => setBody(event.target.value)} defaultValue={body} />
      <button type="submit"> Submit </button>
    </form>
  );
}

// المسار: / (الصفحة الرئيسية)
function App() {
  const { loading, error, data } = useQuery(GET_POSTS, {
    variables: { limit: 5 },
  });

  if (loading) return <div> Loading... </div>;
  if (error) return <div> Error! </div>;

  // المنشور المحدث لا يُعرض، ما زلنا نرى البيانات القديمة
  return data.posts.map((post) => <Post key={post.id} post={post} />);
}

هذا لا يرجع فقط إلى ذاكرة Apollo cache، ولكن أيضًا إلى التعليمات الخاصة بالبيانات التي يجب أن يجلبها الاستعلام. يمكننا تغيير كيفية جلب الاستعلام باستخدام خاصية fetchPolicy. بشكل افتراضي، يتم تعيين fetchPolicy على "cache-first". سيحاول البحث في الذاكرة المؤقتة للحصول على بياناتنا بدلاً من الحصول عليها من الشبكة.

تتمثل إحدى الطرق السهلة لإصلاح هذه المشكلة المتمثلة في عدم رؤية البيانات الجديدة في تغيير سياسة الجلب. ومع ذلك، فإن هذا النهج ليس مثاليًا من منظور الأداء، لأنه يتطلب إجراء طلب إضافي (استخدام الذاكرة المؤقتة مباشرة لا يتطلب ذلك، لأنها بيانات محلية). هناك العديد من الخيارات المختلفة لسياسة الجلب المدرجة أدناه:

{
  fetchPolicy: "cache-first"; // افتراضي
  /*
  cache-and-network
  cache-first
  cache-only
  network-only
  no-cache
  standby
  */
}

لن أخوض في تفاصيل ما تفعله كل سياسة بالضبط، ولكن لحل مشكلتنا الفورية، إذا كنت تريد دائمًا أن يحصل الاستعلام على أحدث البيانات عن طريق طلبها من الشبكة، فإننا نضبط fetchPolicy على "network-first".

const { loading, error, data } = useQuery(GET_POSTS, {
  variables: { limit: 5 },
  fetchPolicy: "network-first",
});

تحديث الذاكرة المؤقتة (Cache) بعد عملية Mutation

بدلاً من تجاوز الذاكرة المؤقتة عن طريق تغيير سياسة الجلب لـ useQuery، دعنا نحاول إصلاح هذه المشكلة عن طريق تحديث الذاكرة المؤقتة يدويًا. عند إجراء تعديل (mutation) باستخدام useMutation، لدينا وصول إلى دالة رد نداء أخرى، تُعرف باسم update. تمنحنا update وصولاً مباشرًا إلى الذاكرة المؤقتة بالإضافة إلى البيانات التي يتم إرجاعها من تعديل ناجح.

يمكّننا هذا من قراءة استعلام معين من الذاكرة المؤقتة، وأخذ تلك البيانات الجديدة وكتابة البيانات الجديدة إلى الاستعلام، مما سيؤدي بعد ذلك إلى تحديث ما يراه المستخدم. يُعد العمل مع الذاكرة المؤقتة يدويًا عملية معقدة يميل الكثير من الناس إلى تجنبها، ولكنه مفيد جدًا لأنه يوفر بعض الوقت والموارد من خلال عدم الاضطرار إلى إجراء نفس الطلب عدة مرات لتحديث الذاكرة المؤقتة يدويًا.

function EditPost({ id }) {
  const [updatePost] = useMutation(UPDATE_POST, {
    update: (cache, { data }) => {
      const { posts } = cache.readQuery({ query: GET_POSTS });
      const newPost = data.update_posts.returning[0]; // تأكد من الحصول على الكائن الصحيح
      const updatedPosts = posts.map((post) => (post.id === id ? newPost : post));
      cache.writeQuery({
        query: GET_POSTS,
        data: { posts: updatedPosts },
      });
    },
    onCompleted: () => history.push("/"),
  });
  // ...
}

نريد أولاً قراءة الاستعلام والحصول على البيانات السابقة منه. ثم نحتاج إلى أخذ البيانات الجديدة. في هذه الحالة، للعثور على المنشور بمعرف معين واستبداله ببيانات newPost، وإلا فليكن البيانات السابقة، ثم نكتب تلك البيانات مرة أخرى إلى نفس الاستعلام، مع التأكد من أن لها نفس بنية البيانات كما كان من قبل. بعد كل هذا، كلما قمنا بتحرير منشور وتم توجيهنا مرة أخرى إلى الصفحة الرئيسية، يجب أن نرى بيانات المنشور الجديدة.

إعادة جلب الاستعلامات باستخدام useQuery

لنفترض أننا نعرض قائمة من المنشورات باستخدام استعلام GET_POSTS ونقوم بحذف أحدها باستخدام تعديل DELETE_POST. عندما يحذف المستخدم منشورًا، ماذا نريد أن يحدث؟ بطبيعة الحال، نريد إزالته من القائمة، سواء البيانات أو ما يتم عرضه للمستخدمين. ومع ذلك، عندما يتم تنفيذ تعديل، لا يعرف الاستعلام أن البيانات قد تغيرت.

هناك بعض الطرق لتحديث ما نراه، ولكن أحد الأساليب هو إعادة تنفيذ الاستعلام. يمكننا القيام بذلك عن طريق الحصول على دالة refetch التي يمكننا تفكيكها من الكائن الذي يعيده خطاف useQuery وتمريرها إلى التعديل ليتم تنفيذها عند اكتماله، باستخدام دالة رد النداء onCompleted:

function Posts() {
  const { loading, data, refetch } = useQuery(GET_POSTS);

  if (loading) return <div> Loading... </div>;

  return data.posts.map((post) => (
    <Post key={post.id} post={post} refetch={refetch} />
  ));
}

function Post({ post, refetch }) {
  const [deletePost] = useMutation(DELETE_POST, {
    onCompleted: () => refetch(),
  });

  function handleDeletePost(id) {
    if (window.confirm("Are you sure you want to delete this post?")) {
      deletePost({ variables: { id } });
    }
  }

  return (
    <div>
      <h1> {post.title} </h1>
      <p> {post.body} </p>
      <button onClick={() => handleDeletePost(post.id)}>Delete </button>
    </div>
  );
}

إعادة جلب الاستعلامات باستخدام useMutation

لاحظ أنه يمكننا أيضًا استخدام خطاف useMutation لإعادة تنفيذ استعلاماتنا من خلال حجة مقدمة لدالة التعديل (mutate function)، تسمى refetchQueries. تقبل مصفوفة من الاستعلامات التي نريد إعادة جلبها بعد تنفيذ تعديل. يتم توفير كل استعلام داخل كائن، تمامًا كما نوفرها لـ client.query()، وتتكون من خاصية query وخاصية variables. إليك مثال بسيط لإعادة جلب استعلام GET_POSTS الخاص بنا بعد إنشاء منشور جديد:

function NewPost() {
  const [createPost] = useMutation(CREATE_POST, {
    refetchQueries: [
      {
        query: GET_POSTS,
        variables: { limit: 5 },
      },
    ],
  });
  // ...
}

الوصول إلى Client باستخدام useApolloClient

يمكننا الوصول إلى Client عبر مكوناتنا بمساعدة خطاف خاص يسمى useApolloClient. نقوم بتنفيذ هذا الخطاف في الجزء العلوي من مكون الدالة الخاص بنا ونستعيد Client نفسه.

function Logout() {
  const client = useApolloClient();
  // client هو نفسه الذي أنشأناه باستخدام new ApolloClient()
  function handleLogout() {
    // التعامل مع تسجيل خروج المستخدم، ثم مسح البيانات المخزنة
    logoutUser();
    client.resetStore().then(() => console.log("logged out!"));
    /* كن على علم بأن .resetStore() غير متزامن */
  }

  return <button onClick={handleLogout}> Logout </button>;
}

ومن هناك يمكننا تنفيذ جميع الاستعلامات والتعديلات والاشتراكات نفسها. لاحظ أن هناك العديد من الميزات الأخرى التي تأتي مع الطرق التي تأتي مع Client. باستخدام Client، يمكننا أيضًا كتابة وقراءة البيانات من الذاكرة المؤقتة التي يقوم Apollo بإعدادها (باستخدام client.readData() و client.writeData()). يستحق العمل مع ذاكرة Apollo cache دورة تدريبية مكثفة خاصة به.

من الفوائد العظيمة للعمل مع Apollo أنه يمكننا أيضًا استخدامه كنظام لإدارة الحالة (state management system) ليحل محل حلول مثل Redux لحالتنا العامة. إذا كنت ترغب في معرفة المزيد حول استخدام Apollo لإدارة حالة التطبيق العامة، يمكنك مراجعة الروابط والمصادر الرسمية.

لقد حاولت جعل هذا الدليل شاملاً قدر الإمكان، على الرغم من أنه لا يزال يترك العديد من ميزات Apollo التي تستحق الاستكشاف. إذا كنت ترغب في معرفة المزيد عن Apollo، فتأكد من مراجعة وثائق Apollo الرسمية.

الخلاصة التقنية

يمثل Apollo جسرًا حيويًا بين قوة React في بناء واجهات المستخدم التفاعلية ومرونة GraphQL في إدارة البيانات. من خلال مجموعة غنية من خطافات React والقدرة على التفاعل المباشر مع Apollo Client، يوفر Apollo للمطورين أدوات قوية لإنشاء تطبيقات ويب سريعة الاستجابة وفعالة. سواء كان الأمر يتعلق بجلب البيانات الفوري باستخدام useQuery، أو التحكم في توقيت الاستعلامات مع useLazyQuery، أو تنفيذ التعديلات المعقدة عبر useMutation، أو حتى التعامل مع البيانات في الوقت الفعلي باستخدام useSubscription، فإن Apollo يبسط هذه العمليات بشكل كبير.

علاوة على ذلك، فإن إدارة الذاكرة المؤقتة (cache) وتحديثها بذكاء يقلل من الحاجة إلى طلبات الشبكة المتكررة، مما يحسن الأداء العام للتطبيق. إن فهم هذه المفاهيم الأساسية والوصفات المتقدمة يمكّن المطورين من بناء تطبيقات GraphQL قوية وقابلة للتوسع بكفاءة عالية، مما يجعل Apollo خيارًا لا غنى عنه في بيئة تطوير الويب الحديثة.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *