Frontend Development

تسلط بر همگام‌سازی داده‌های بلادرنگ: یکپارچه‌سازی WebSockets با React Query

در توسعه فرانت‌اند مدرن، توانایی نمایش داده‌های زنده و به‌روز، دیگر یک لوکس نیست بلکه انتظاری استاندارد است. از داشبوردهای مالی تا ابزارهای ویرایش مشارکتی، کاربران بازخورد فوری را طلب می‌کنند. در حالی که پالینگ (Polling) روشی منسوخ و ناکارآمد است، WebSockets یک کانال ارتباطی پایدار و دوطرفه ارائه می‌دهند که این مشکل را به زیبایی حل می‌کند. با این حال، مدیریت وضعیت WebSocket در یک برنامه React می‌تواند به سرعت پیچیده شود. اینجاست که React Query (که اکنون با نام TanStack Query شناخته می‌شود) درخشش می‌کند و راه‌حلی قدرتمند برای دریافت و کش کردن وضعیت سرور ارائه می‌دهد. در این پست، بررسی خواهیم کرد که چگونه می‌توان این دو فناوری را به هم پیوند داد تا تجربه‌ای بلادرنگ و روان ایجاد شود.

چالش همگام‌سازی وضعیت

React Query در مدیریت دریافت داده‌های مبتنی بر درخواست (HTTP/REST) عالی عمل می‌کند. این کتابخانه هوک‌هایی مانند useQuery برای کش کردن و useMutation برای اثرات جانبی ارائه می‌دهد. با این حال، WebSockets بر اساس یک مدل رویداد-محور (Event-driven) عمل می‌کنند، نه یک چرخه درخواست-پاسخ. این ناسازگاری معماری اغلب باعث می‌شود توسعه‌دهندگان به طور کامل از React Query صرف‌نظر کرده و اشتراک‌گذاری‌های WebSocket را به صورت دستی در هوک‌های useEffect مدیریت کنند. این رویکرد به سرعت با مقیاس‌پذیری برنامه غیرقابل مدیریت می‌شود و منجر به شرایط رقابتی (Race conditions)، نشت حافظه و وضعیت‌های ناسازگار در رابط کاربری می‌گردد.

هدف این است که از مکانیزم‌های قدرتمند کش کردن و نامعتبرسازی (Invalidation) React Query بهره ببریم و در عین حال از WebSockets به عنوان لایه انتقال برای به‌روزرسانی‌ها استفاده کنیم. ما به استراتژی‌ای نیاز داریم که در آن WebSocket به عنوان یک اعلان‌دهنده عمل کند و React Query را برای دریافت مجدد داده‌ها یا به‌روزرسانی مستقیم کش خود تحریک نماید.

راه‌اندازی کلاینت WebSocket

در ابتدا، بیایید یک کلاینت WebSocket ساده ایجاد کنیم. ما یک هوک سفارشی ایجاد می‌کنیم که چرخه حیات اتصال را مدیریت می‌کند. این هوک باید باز کردن اتصال، ارسال پیام‌ها و گوش دادن به رویدادهای ورودی را مدیریت کند.

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

الگوی مؤثرترین روش، استفاده از WebSocket برای تحریک نامعتبرسازی کش است. وقتی پیام WebSocket نشان می‌دهد که داده‌ها تغییر کرده‌اند، می‌توانیم queryClient.invalidateQueries() را فراخوانی کنیم. این کار به React Query می‌گوید که داده‌های کش شده منسوخ شده‌اند و باید از طریق endpoint استاندارد 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,
  });

  // استفاده از WebSocket برای گوش دادن به پیام‌های جدید
  const sendMessage = useWebSocket('wss://api.example.com/chat', (msg) => {
    if (msg.type === 'NEW_MESSAGE') {
      // نامعتبرسازی کوئری برای دریافت مجدد آخرین داده‌ها
      queryClient.invalidateQueries({ queryKey: ['messages'] });
      
      // به عنوان جایگزین، برای تجربه کاربری بهتر، می‌توانید کش را به صورت خوش‌بینانه به‌روزرسانی کنید:
      // 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>
  );
};

به‌روزرسانی‌های خوش‌بینانه (Optimistic Updates) برای تجربه کاربری بهتر

اگرچه نامعتبرسازی کار می‌کند، اما هنگام دریافت مجدد داده‌ها، یک لرزش کوتاه ایجاد می‌کند. برای تجربه‌ای روان‌تر، در نظر بگیرید که از queryClient.setQueryData() برای به‌روزرسانی‌های خوش‌بینانه استفاده کنید. وقتی پیامی دریافت می‌شود، می‌توانید کش را به صورت دستی با آیتم جدید به‌روزرسانی کنید. این کار بازخورد فوری به کاربر بدون انتظار برای درخواست شبکه ارائه می‌دهد. به یاد داشته باشید که همیشه سرور را به عنوان منبع حقیقت نگه دارید و به صورت دوره‌ای داده‌ها را مجدداً دریافت کنید تا هرگونه تعارض احتمالی را حل نمایید.

نتیجه‌گیری

یکپارچه‌سازی WebSockets با React Query لزوماً دردناک نیست. با در نظر گرفتن WebSockets به عنوان یک لایه اعلان‌دهنده به جای لایه دریافت داده، می‌توانید سادگی و قدرت مدیریت کش React Query را حفظ کنید. این رویکرد ترکیبی تضمین می‌کند که برنامه‌های شما هم از نظر عملکردی کارآمد و هم پاسخگو باشند، قابلیت‌های بلادرنگی را که کاربران انتظار دارند ارائه دهند و در عین حال کدبیس شما را قابل نگهداری نگه دارند. هنگام پذیرش این الگو، همیشه به قابلیت اطمینان شبکه توجه کنید و منطق اتصال مجدد (Reconnection) را پیاده‌سازی کنید تا تجربه کاربری مستحکمی را تضمین نمایید.

Share: