الاستماع إلى الأحداث (Events) وتحديث واجهة React لحظياً عند تغير البيانات

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

الاستماع إلى الأحداث (Events) وتحديث واجهة React لحظياً عند تغير البيانات

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

عند دمج React مع Ethers.js، يمكنك الاشتراك في حدث يصدره العقد الذكي، ثم تحديث state داخل الواجهة مباشرة. النتيجة هي تجربة استخدام أقرب إلى التطبيقات التقليدية، لكن فوق بنية لامركزية تعتمد على Blockchain.

هذا المقال يبني عملياً فوق مفاهيم سابقة مثل هندسة الويب اللامركزي (Web3.js & Ethers.js): كيف نربط الواجهات بالعقود الذكية؟، وإعداد واجهة React.js وتثبيت مكتبة Ethers.js للاتصال بالبلوكتشين، وقراءة البيانات من البلوكتشين وعرضها في واجهة الموقع مجاناً. سننتقل هنا من القراءة الساكنة إلى التحديث اللحظي المدفوع بالأحداث.

لماذا نستخدم الأحداث بدلاً من الاستعلام المتكرر؟

الطريقة البدائية لتحديث الواجهة هي تشغيل استعلام كل عدة ثوانٍ باستخدام polling. لكنها تستهلك موارد أكثر، وقد تسبب بطئاً أو بيانات متأخرة، خصوصاً عندما يكبر عدد المستخدمين أو تتعدد الحقول المقروءة من العقد.

أما باستخدام Events، فإن العقد الذكي يكتب سجلاً في logs عند تنفيذ معاملة معينة. مزود الشبكة أو المكتبة العميلية يلتقط هذا الحدث، ومنه تستطيع الواجهة إعادة تحميل جزء محدد فقط من البيانات بدلاً من إعادة بناء كل الشاشة.

  • تقليل الطلبات غير الضرورية على مزود RPC.
  • إعطاء المستخدم إحساساً فورياً بتغير الحالة.
  • فصل منطق الإشعار عن منطق العرض في الواجهة.
  • إمكانية تتبع تاريخ النشاط لاحقاً من خلال السجلات.

تصميم عقد ذكي يطلق حدثاً واضحاً وقابلاً للاستهلاك

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

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

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Counter {
    uint256 public count;

    event CountUpdated(address indexed user, uint256 newCount);

    function increment() external {
        count += 1;
        emit CountUpdated(msg.sender, count);
    }

    function getCount() external view returns (uint256) {
        return count;
    }
}

لاحظ أن الحدث CountUpdated يستخدم الحقل indexed مع عنوان المستخدم. هذا يسهل تصفية السجلات لاحقاً إذا أردت الاستماع لأحداث تخص عنواناً معيناً فقط.

احرص على عدم استخدام الأحداث كبديل عن التخزين الدائم داخل العقد الذكي. الحدث مفيد للإشعارات والتتبع الخارجي، لكنه ليس حالة يمكن للعقد نفسه قراءتها لاحقاً مثل متغيرات storage. لفهم الفرق البنيوي بين أماكن حفظ البيانات، راجع إدارة الذاكرة بذكاء: الفرق الحاسم بين Storage, Memory, و Calldata.

خطوات ربط العقد بواجهة React

إذا كنت قد أنهيت مسبقاً إعداد الاتصال بالمحفظة عبر الاتصال بمحفظة المستخدم: كتابة كود يطلب من الزائر ربط محفظة MetaMask بموقعك، فستكون جاهزاً لإنشاء كائن العقد باستخدام provider وsigner.

1) إنشاء الاتصال بالعقد

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

2) قراءة القيمة الأولية عند تحميل الصفحة

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

3) الاشتراك في الحدث وتحديث الحالة

هنا نستخدم useEffect لإنشاء الاشتراك مرة واحدة، ثم نربط دالة listener بالحدث. عند وصول السجل الجديد، نقوم بتحديث state فوراً.

// React + Ethers.js example (placed here per requested code block format)
import { useEffect, useState } from "react";
import { ethers } from "ethers";
import counterAbi from "./CounterAbi.json";

const contractAddress = "0xYourContractAddress";

export default function CounterApp() {
  const [count, setCount] = useState("0");
  const [account, setAccount] = useState("");

  useEffect(() => {
    let contract;
    let listener;

    async function init() {
      if (!window.ethereum) return;

      const provider = new ethers.BrowserProvider(window.ethereum);
      const signer = await provider.getSigner();
      const address = await signer.getAddress();
      setAccount(address);

      contract = new ethers.Contract(contractAddress, counterAbi, signer);

      const currentCount = await contract.getCount();
      setCount(currentCount.toString());

      listener = (user, newCount) => {
        setCount(newCount.toString());
      };

      contract.on("CountUpdated", listener);
    }

    init();

    return () => {
      if (contract && listener) {
        contract.off("CountUpdated", listener);
      }
    };
  }, []);

  async function incrementCount() {
    if (!window.ethereum) return;

    const provider = new ethers.BrowserProvider(window.ethereum);
    const signer = await provider.getSigner();
    const contract = new ethers.Contract(contractAddress, counterAbi, signer);

    const tx = await contract.increment();
    await tx.wait();
  }

  return (
    <div>
      <h2>Counter DApp</h2>
      <p>Connected account: {account}</p>
      <p>Current count: {count}</p>
      <button onClick={incrementCount}>Increment</button>
    </div>
  );
}

لماذا نزيل الاشتراك عند إلغاء تحميل المكوّن؟

في بيئة React، إذا لم تستخدم cleanup داخل useEffect، فقد تتكرر الاشتراكات عند إعادة التصيير أو التنقل بين الصفحات. هذا يؤدي إلى تنفيذ نفس listener عدة مرات وإظهار سلوك مربك للمستخدم.

لهذا السبب نستخدم contract.off() عند تفكيك المكوّن. هذه خطوة هندسية مهمة جداً في أي واجهة تعتمد على التدفق اللحظي للأحداث.

أفضل ممارسة: لا تثق بالحدث وحده، أعد المزامنة عند الحاجة

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

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

من منظور أمني، لا تجعل واجهة المستخدم تعتمد على افتراض أن كل حدث يعني نجاح التجربة الكاملة للمستخدم في كل السيناريوهات. تأكد من انتظار تأكيد المعاملة عبر tx.wait() عند الكتابة، ثم أعد قراءة الحالة النهائية من العقد إذا كانت الدقة مطلوبة. كما أن اختبار هذه التدفقات مهم ضمن بيئة اختبار العقود الذكية محلياً: كتابة اختبارات الوحدة (Unit Tests) باستخدام Chai & Mocha.

تحسينات عملية للأداء وتجربة المستخدم

استخدام أحداث مفلترة

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

تقليل البيانات داخل الحدث

كل قيمة إضافية في الحدث تعني حجماً أكبر في log. لذلك اجعل البيانات المرسلة مركزة: معرف، عنوان، قيمة نهائية، أو حالة مختصرة فقط.

لخفض كلفة Gas Fees، لا تضع في الأحداث بيانات ضخمة يمكن اشتقاقها لاحقاً من التخزين أو من الواجهة نفسها. وإذا كنت لا تزال تبني أساسك النظري، راجع التكاليف (Gas Fees): كيف يحسب البلوكتشين تكلفة تنفيذ الأكواد؟ وأنواع الدوال : فهم view و pure لتوفير رسوم الـ Gas.

أخطاء شائعة عند الاستماع للأحداث

  • الاشتراك في الحدث قبل التأكد من الشبكة الصحيحة في MetaMask.
  • نسيان إزالة listener عند مغادرة الصفحة.
  • الاعتماد على الحدث فقط دون قراءة الحالة الأولية من العقد.
  • إرسال معاملة وتحديث الواجهة تفاؤلياً دون انتظار التأكيد.
  • الخلط بين بيانات الحدث وبيانات التخزين الفعلية داخل العقد.

خاتمة

الاستماع إلى الأحداث هو أحد أهم الجسور بين منطق العقد الذكي وتجربة الاستخدام الحديثة في الواجهة. عندما تصمم Smart Contracts بعناية، وتربطها عبر Ethers.js مع مكوّنات React، تحصل على واجهة تتفاعل لحظياً مع تغيرات البلوكتشين بدلاً من أن تبقى مجرد شاشة قراءة جامدة.

النهج الاحترافي هنا هو الجمع بين ثلاثة عناصر: قراءة أولية موثوقة، اشتراك لحظي في Events، وإعادة مزامنة مدروسة للحالة عند الضرورة. بهذه البنية تبني DApps أكثر دقة، أسرع استجابة، وأفضل جودة للمستخدم النهائي.

8 comments

اترك تعليقاً

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