واجهات GraphQL

Apollo Client المتقدم

20 دقيقة الدرس 20 من 35

تقنيات Apollo Client المتقدمة

بعد الاستعلامات الأساسية، يوفر Apollo Client ميزات قوية للتحويرات وإدارة ذاكرة التخزين المؤقت وواجهة المستخدم المتفائلة والاستعلامات البطيئة والاشتراكات والحالة المحلية. أتقن هذه التقنيات لبناء تطبيقات GraphQL سريعة الاستجابة وفعالة للغاية.

خطاف useMutation

ينفذ خطاف useMutation تحويرات GraphQL لإنشاء البيانات أو تحديثها أو حذفها.

مثال useMutation الأساسي:
import React, { useState } from 'react';
import { useMutation, gql } from '@apollo/client';

const CREATE_POST = gql`
  mutation CreatePost($title: String!, $content: String!) {
    createPost(title: $title, content: $content) {
      id
      title
      content
      createdAt
    }
  }
`;

function CreatePostForm() {
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');

  const [createPost, { data, loading, error }] = useMutation(CREATE_POST);

  const handleSubmit = async (e) => {
    e.preventDefault();

    try {
      await createPost({
        variables: { title, content }
      });

      // مسح النموذج عند النجاح
      setTitle('');
      setContent('');
      alert('تم إنشاء المنشور بنجاح!');
    } catch (err) {
      console.error('خطأ في التحوير:', err);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="العنوان"
        required
      />
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        placeholder="المحتوى"
        required
      />
      <button type="submit" disabled={loading}>
        {loading ? 'جارٍ الإنشاء...' : 'إنشاء منشور'}
      </button>
      {error && <p>خطأ: {error.message}</p>}
    </form>
  );
}

export default CreatePostForm;

تحديثات ذاكرة التخزين المؤقت بعد التحويرات

بعد التحوير، تحتاج إلى تحديث ذاكرة التخزين المؤقت Apollo بحيث تعكس واجهة المستخدم التغييرات دون إعادة الجلب.

تحديث ذاكرة التخزين المؤقت اليدوي:
import { useMutation, gql } from '@apollo/client';

const CREATE_POST = gql`
  mutation CreatePost($title: String!, $content: String!) {
    createPost(title: $title, content: $content) {
      id
      title
      content
    }
  }
`;

const GET_POSTS = gql`
  query GetPosts {
    posts {
      id
      title
      content
    }
  }
`;

function CreatePostForm() {
  const [createPost] = useMutation(CREATE_POST, {
    update(cache, { data: { createPost } }) {
      // قراءة المنشورات الموجودة من ذاكرة التخزين المؤقت
      const existingPosts = cache.readQuery({ query: GET_POSTS });

      // كتابة المنشورات المحدثة مرة أخرى إلى ذاكرة التخزين المؤقت
      cache.writeQuery({
        query: GET_POSTS,
        data: {
          posts: [...existingPosts.posts, createPost]
        }
      });
    }
  });

  // تنفيذ النموذج...
}
إعادة جلب الاستعلامات بعد التحوير:
const [deletePost] = useMutation(DELETE_POST, {
  refetchQueries: [
    { query: GET_POSTS },
    { query: GET_USER_POSTS, variables: { userId: user.id } }
  ]
});

// أو استخدم رد اتصال refetchQueries
const [updatePost] = useMutation(UPDATE_POST, {
  refetchQueries: (result) => [
    { query: GET_POST, variables: { id: result.data.updatePost.id } }
  ]
});

واجهة المستخدم المتفائلة

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

مثال استجابة متفائلة:
import { useMutation, gql } from '@apollo/client';

const LIKE_POST = gql`
  mutation LikePost($postId: ID!) {
    likePost(postId: $postId) {
      id
      likes
      isLiked
    }
  }
`;

function LikeButton({ post }) {
  const [likePost] = useMutation(LIKE_POST, {
    optimisticResponse: {
      likePost: {
        __typename: 'Post',
        id: post.id,
        likes: post.likes + 1,
        isLiked: true
      }
    }
  });

  return (
    <button onClick={() => likePost({ variables: { postId: post.id } })}>
      {post.isLiked ? 'إلغاء الإعجاب' : 'إعجاب'} ({post.likes})
    </button>
  );
}
أفضل ممارسات واجهة المستخدم المتفائلة:
  • استخدم للعمليات السريعة ومنخفضة المخاطر (الإعجابات، المتابعات، التبديلات)
  • اجعل بنية الاستجابة المتفائلة متطابقة مع الاستجابة الفعلية
  • قم بتضمين __typename في الاستجابة المتفائلة لتطبيع ذاكرة التخزين المؤقت
  • تجنب العمليات الحرجة (المدفوعات، حذف البيانات)

خطاف useLazyQuery

بخلاف useQuery الذي ينفذ على الفور، يعيد useLazyQuery دالة يمكنك استدعاؤها يدويًا عند الحاجة.

مثال استعلام بطيء:
import React, { useState } from 'react';
import { useLazyQuery, gql } from '@apollo/client';

const SEARCH_USERS = gql`
  query SearchUsers($query: String!) {
    searchUsers(query: $query) {
      id
      name
      email
      avatar
    }
  }
`;

function UserSearch() {
  const [query, setQuery] = useState('');

  // يعيد [executeQuery, { data, loading, error }]
  const [searchUsers, { data, loading, error }] = useLazyQuery(SEARCH_USERS);

  const handleSearch = (e) => {
    e.preventDefault();
    searchUsers({ variables: { query } });
  };

  return (
    <div>
      <form onSubmit={handleSearch}>
        <input
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="البحث عن المستخدمين..."
        />
        <button type="submit">بحث</button>
      </form>

      {loading && <p>جارٍ البحث...</p>}
      {error && <p>خطأ: {error.message}</p>}

      {data && (
        <ul>
          {data.searchUsers.map(user => (
            <li key={user.id}>
              <img src={user.avatar} alt={user.name} />
              {user.name} - {user.email}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default UserSearch;

خطاف useSubscription

يشترك خطاف useSubscription في التحديثات في الوقت الفعلي عبر WebSocket.

مثال الاشتراك:
import React from 'react';
import { useSubscription, gql } from '@apollo/client';

const MESSAGE_SUBSCRIPTION = gql`
  subscription OnMessageAdded($channelId: ID!) {
    messageAdded(channelId: $channelId) {
      id
      content
      author {
        id
        name
        avatar
      }
      createdAt
    }
  }
`;

function ChatMessages({ channelId, messages }) {
  const { data, loading } = useSubscription(MESSAGE_SUBSCRIPTION, {
    variables: { channelId },
    onData: ({ client, data }) => {
      // معالجة رسالة جديدة
      console.log('رسالة جديدة:', data.data.messageAdded);
    }
  });

  return (
    <div>
      {messages.map(msg => (
        <div key={msg.id}>
          <strong>{msg.author.name}:</strong> {msg.content}
        </div>
      ))}
      {loading && <p>جارٍ الاتصال بالدردشة...</p>}
    </div>
  );
}

export default ChatMessages;

إدارة الحالة المحلية

يمكن لـ Apollo Client إدارة الحالة المحلية جنبًا إلى جنب مع البيانات البعيدة، مما يلغي الحاجة إلى مكتبات إدارة حالة منفصلة.

الحالة المحلية مع المتغيرات التفاعلية:
import { makeVar, useReactiveVar } from '@apollo/client';

// إنشاء متغير تفاعلي
export const isLoggedInVar = makeVar(false);
export const currentUserVar = makeVar(null);
export const themeVar = makeVar('light');

// الاستخدام في المكونات
function Header() {
  const isLoggedIn = useReactiveVar(isLoggedInVar);
  const currentUser = useReactiveVar(currentUserVar);
  const theme = useReactiveVar(themeVar);

  const toggleTheme = () => {
    themeVar(theme === 'light' ? 'dark' : 'light');
  };

  return (
    <header className={theme}>
      {isLoggedIn ? (
        <div>مرحبًا، {currentUser.name}</div>
      ) : (
        <button>تسجيل الدخول</button>
      )}
      <button onClick={toggleTheme}>
        تبديل الوضع
      </button>
    </header>
  );
}
قراءة وكتابة المتغيرات التفاعلية:
import { isLoggedInVar, currentUserVar } from './cache';

// قراءة القيمة الحالية
const isLoggedIn = isLoggedInVar();

// تحديث القيمة
isLoggedInVar(true);
currentUserVar({ id: '1', name: 'جون دو', email: 'john@example.com' });

// في التحويرات
const [login] = useMutation(LOGIN, {
  onCompleted: (data) => {
    isLoggedInVar(true);
    currentUserVar(data.login.user);
  }
});

const [logout] = useMutation(LOGOUT, {
  onCompleted: () => {
    isLoggedInVar(false);
    currentUserVar(null);
  }
});

معالجة ذاكرة التخزين المؤقت

اقرأ واكتب وعدل ذاكرة التخزين المؤقت Apollo مباشرة لحالات الاستخدام المتقدمة.

الوصول المباشر إلى ذاكرة التخزين المؤقت:
import { useApolloClient } from '@apollo/client';

function UserProfile({ userId }) {
  const client = useApolloClient();

  // القراءة من ذاكرة التخزين المؤقت
  const cachedUser = client.readFragment({
    id: `User:${userId}`,
    fragment: gql`
      fragment UserData on User {
        id
        name
        email
      }
    `
  });

  // الكتابة إلى ذاكرة التخزين المؤقت
  const updateUserInCache = (updates) => {
    client.writeFragment({
      id: `User:${userId}`,
      fragment: gql`
        fragment UserData on User {
          id
          name
          email
        }
      `,
      data: {
        ...cachedUser,
        ...updates
      }
    });
  };

  // الإزالة من ذاكرة التخزين المؤقت (الحذف)
  const removeUserFromCache = () => {
    client.cache.evict({ id: `User:${userId}` });
    client.cache.gc(); // جمع القمامة
  };

  return (
    <div>
      {/* واجهة المكون */}
    </div>
  );
}

معالجة الأخطاء في التحويرات

معالجة أخطاء التحوير الشاملة:
const [updateUser, { loading, error }] = useMutation(UPDATE_USER, {
  onError: (error) => {
    if (error.graphQLErrors) {
      error.graphQLErrors.forEach(({ message, extensions }) => {
        if (extensions?.code === 'UNAUTHENTICATED') {
          // إعادة التوجيه إلى تسجيل الدخول
          window.location.href = '/login';
        } else if (extensions?.code === 'BAD_USER_INPUT') {
          // إظهار أخطاء التحقق من الصحة
          alert(`خطأ في التحقق: ${message}`);
        }
      });
    }

    if (error.networkError) {
      alert('خطأ في الشبكة. يرجى المحاولة مرة أخرى.');
    }
  },
  onCompleted: (data) => {
    alert('تم تحديث المستخدم بنجاح!');
  }
});
تحذير: عند استخدام واجهة المستخدم المتفائلة مع تحديثات ذاكرة التخزين المؤقت، تأكد من أن بنية استجابتك المتفائلة تتطابق مع استجابة الخادم الفعلية تمامًا. يمكن أن تتسبب عدم التطابقات في عدم تناسق ذاكرة التخزين المؤقت وأخطاء واجهة المستخدم.
تمرين:
  1. أنشئ تحويرًا لإضافة تعليق إلى منشور باستخدام useMutation
  2. نفذ تحديث ذاكرة تخزين مؤقت يدوي لإضافة التعليق الجديد إلى قائمة تعليقات المنشور
  3. أضف واجهة مستخدم متفائلة لإظهار التعليق على الفور قبل تأكيد الخادم
  4. قم ببناء ميزة بحث باستخدام useLazyQuery الذي ينفذ فقط عندما يقدم المستخدم نموذجًا
  5. أنشئ متغيرات تفاعلية لتفضيل الوضع الداكن واستخدمها عبر مكونات متعددة