في تطوير الواجهات الأمامية الحديثة، لم تعد القدرة على عرض البيانات الحية والمحدثة كل ثانية رفاهية، بل أصبحت توقعاً قياسياً. من لوحات المعلومات المالية إلى أدوات التحرير التعاوني، يطلب المستخدمون ردود فعل فورية. بينما يُعد الاستعلام الدوري (Polling) نهجاً قديماً وغير فعال، توفر WebSockets قناة اتصال مستمرة وذات اتجاهين تحل هذه المشكلة بذكاء. ومع ذلك، يمكن أن يصبح إدارة حالة WebSockets داخل تطبيق React معقدة بسرعة. هنا تبرز قوة React Query (المعروف الآن باسم TanStack Query)، حيث يقدم حلاً متيناً لجلب البيانات وتخزينها في ذاكرة التخزين المؤقت. في هذا المنشور، سنستكشف كيفية جسر هذه التقنيات معاً لإنشاء تجربة في الوقت الفعلي سلسة.
تحدي مزامنة الحالة
يتفوق React Query في التعامل مع جلب البيانات القائم على الطلبات (HTTP/REST). فهو يوفر أدوات مثل useQuery للتخزين المؤقت وuseMutation للآثار الجانبية. ومع ذلك، تعمل WebSockets بناءً على نموذج قائم على الأحداث، وليس دورة طلب-استجابة. يؤدي هذا التباين المعماري غالباً إلى قيام المطورين بتجاوز React Query تماماً، وإدارة اشتراكات WebSockets يدوياً داخل أدوات useEffect. يصبح هذا النهج غير قابل للإدارة بسرعة مع توسع التطبيق، مما يؤدي إلى ظروف السباق (race conditions)، وتسرب الذاكرة، وحالات واجهة مستخدم غير متسقة.
الهدف هو الاستفادة من آليات التخزين المؤقت والإبطال القوية في React Query مع استخدام WebSockets كطبقة نقل للتحديثات. نحتاج إلى استراتيجية تعمل فيها WebSockets كمرسل إشارات، مما يحفز React Query لإعادة جلب البيانات أو تحديث ذاكرة التخزين المؤقت مباشرة.
إعداد عميل WebSockets
أولاً، دعونا نؤسس لعميل WebSockets بسيط. سنقوم بإنشاء أداة مخصصة (custom hook) تدير دورة حياة الاتصال. يجب أن تتعامل هذه الأداة مع فتح الاتصال، وإرسال الرسائل، والاستماع إلى الأحداث الواردة.
import { useEffect, useRef } from 'react';
export const useWebSocket = (url, onMessage) => {
const ws = useRef(null);
useEffect(() => {
ws.current = new WebSocket(url);
ws.current.onopen = () => console.log('Connected');
ws.current.onmessage = (event) => {
const data = JSON.parse(event.data);
if (onMessage) onMessage(data);
};
ws.current.onclose = () => console.log('Disconnected');
return () => ws.current.close();
}, [url, onMessage]);
const sendMessage = (message) => {
if (ws.current?.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify(message));
}
};
return sendMessage;
};
جسر WebSockets مع React Query
النمط الأكثر فعالية هو استخدام WebSockets لإطلاق عمليات إبطال ذاكرة التخزين المؤقت. عندما تشير رسالة WebSocket إلى أن البيانات قد تغيرت، يمكننا استدعاء queryClient.invalidateQueries(). يخبر هذا React Query بأن البيانات المخزنة في ذاكرة التخزين المؤقت قديمة وتحتاج إلى إعادة جلبها من الخادم عبر نقطة النهاية القياسية لـ HTTP.
إليك كيفية دمج ذلك في مكون يجلب رسائل الدردشة في الوقت الفعلي:
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useWebSocket } from './useWebSocket';
const fetchMessages = async () => {
const response = await fetch('/api/messages');
return response.json();
};
const RealTimeChat = () => {
const queryClient = useQueryClient();
const { data, isLoading } = useQuery({
queryKey: ['messages'],
queryFn: fetchMessages,
});
// Use WebSocket to listen for new messages
const sendMessage = useWebSocket('wss://api.example.com/chat', (msg) => {
if (msg.type === 'NEW_MESSAGE') {
// Invalidate the query to refetch latest data
queryClient.invalidateQueries({ queryKey: ['messages'] });
// Alternatively, for better UX, you could optimistically update the cache:
// queryClient.setQueryData(['messages'], old => [...old, msg.payload]);
}
});
if (isLoading) return <p>Loading...</p>;
return (
<div>
{data.map(msg => <p key={msg.id}>{msg.text}</p>)}
<button onClick={() => sendMessage({ type: 'SEND', text: 'Hello' })}>
Send
</button>
</div>
);
};
التحديثات التطلعية لتحسين تجربة المستخدم
بينما يعمل الإبطال، فإنه يسبب وميضاً قصيراً أثناء إعادة جلب البيانات. للحصول على تجربة أكثر سلاسة، فكر في استخدام queryClient.setQueryData() للتحديثات التطلعية. عند استلام رسالة، يمكنك تحديث ذاكرة التخزين المؤقت يدوياً بالعنصر الجديد. يوفر هذا ردود فعل فورية للمستخدم دون انتظار طلب الشبكة. تذكر دائماً الحفاظ على الخادم كمصدر للحقيقة من خلال إعادة جلب البيانات بشكل دوري لحل أي تعارضات محتملة.
الخاتمة
لا يجب أن يكون دمج WebSockets مع React Query أمراً مؤلماً. من خلال التعامل مع WebSockets كطبقة إشعارات بدلاً من طبقة جلب البيانات، يمكنك الحفاظ على بساطة وقوة إدارة ذاكرة التخزين المؤقت في React Query. يضمن هذا النهج الهجين أن تكون تطبيقاتك سريعة الاستجابة وقوية الأداء، مما يوفر قدرات الوقت الفعلي التي يتوقعها المستخدمون مع الحفاظ على قابلية صيانة قاعدة الشفرة البرمجية. مع اعتمادك لهذا النمط، فكر دائماً في موثوقية الشبكة ونفذ منطق إعادة الاتصال لضمان تجربة مستخدم قوية.