تاريخ النشر:

كيفية استخدام Higher-Order Functions لتنظيم الأكواد المشتركة وإعادة استخدامها

كتب بواسطة:
  • اسم الكاتب
    تويتر الكاتب

بسم الله والحمد لله والصلاة والسلام على سيدنا محمد رسول الله وبعد،

قبل البدء في قراءة هذا المقال، إليك بعض المصطلحات المستخدمة:

  • HOF: الدالة عالية المستوى (Higher-order function)
  • Callback: دالة يتم تمريرها كوسيطة إلى دالة أخرى.
  • RTK: مكتبة جافا سكريبت مفتوحة المصدر لإدارة حالة التطبيق.

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

عندما كنت أعمل على بناء التطبيق الخاص بالمنصات في مساق، كان هناك ميزة أساسية يجب المحافظة عليها وهي أن كل الصفحات يجب أن تكون Server-Side-Rendered من أجل محركات البحث. وأيضًا كنا نستخدم RTK داخل التطبيق من أجل التعامل مع الـ API. كان هناك آلية استخدام خاصة بـ RTK للحصول على معلومات الـ API من على السيرفر وإرسالها إلى باقي الصفحة. فواجهت تحديًا خلال كتابة الحل المناسب لهذه المشكلة.

المشكلة:

عندما كنت أبحث عن حل لهذه المشكلة، لم يخطر ببالي شيء مناسب. ومن باب الفضول كنت أراجع الكود المصدري لتطبيق Cal.com. عندها وجدت أن لديهم نفس المشكلة التي أواجهها ولكن مع الحل المناسب، وهو أنه تم استخدام مفهوم الـ HOF للتعامل مع هذه المشكلة. وعندما جرّبت تطبيق الحل على مثال مصغر داخل التطبيق عندي، الحمد لله أن التجربة قد نجحت وتم حل المشكلة. ووجدت أنه من الجيد أن أكتب عن التجربة وأشاركها؛ ربما تكون مفيدة لأحد الأشخاص الذين يواجهون نفس المشكلة، وأيضًا من أجل التعرف أكثر على حالات استخدام واقعية خاصة باستخدامات الـ HOF.

واجهت مشكلتين رئيسيتين:

  1. نقل الأكواد المشتركة: كان من الضروري توحيد الأكواد المشتركة بين جميع الصفحات لتسهيل صيانتها.
  2. تنفيذ كود خاص بكل صفحة: احتجت لطريقة تسمح بتنفيذ كود مخصص لكل صفحة بجانب الأكواد المشتركة.

كان الكود في البداية بسيطًا جدًا، ولم يكن يحتوي على أي تعقيدات:

export const getServerSideProps = async ({ locale }) => ({
  props: {
    someEnvVar: process.env.SOME_ENV_VAR,
    ...(await serverSideTranslations(locale ?? i18nextConfig.i18n.defaultLocale, ["common", "courses"]))
  }
});

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

export const getServerSideProps = storeWrapper.getServerSideProps((store) => async (context) => {
  const slug = context.params?.slug;

  store.dispatch(fetchPage.initiate(slug as string));

  await Promise.all(store.dispatch(getRunningPageQueries()));

  return {
    props: {}
  };
});

ومع مرور الوقت، أصبحت التعقيدات في ازدياد.

export const getServerSideProps = storeWrapper.getServerSideProps((store) => async (context) => {
  //new code
  store.dispatch(fetchTenant.initiate());
  await Promise.all(store.dispatch(getRunningTenantQueries()));
  const errors = getQueryErrors(fetchTenant, store);

  if (errors) {
    return errors;
  }
  const tenant = fetchTenant.select()(store.getState())?.data;

  if (!tenant) {
    return {
      notFound: true
    };
  }
  try {
    const env = process.env.NODE_ENV;
    const shouldRedirect = true;

    if (env === "production" && shouldRedirect) {
      return {
        redirect: {
          destination: `https://${tenant.domain}${context.resolvedUrl}`,
          statusCode: 301
        }
      };
    }
  } catch (e) {
    // eslint-disable-next-line no-console
    console.log("Error redirecting to tenant domain", e);
  }

  const session = await getServerSession(req, res, authOptions);

  if (!session && tenant.force_login) {
    /*...*/
  }

  if (tenant?.disable_registration) {
    /*...*/
  }

  //... rest of the code
});

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

الحل، الجزء الاول:

كما ذكرت في بداية المقالة، وبعد بحثٍ مطوّل، وجدت أنه يمكنني استخدام HOF، وكان هذا هو الحل الأمثل للمشكلة. حينها، قمت بنقل الكود الذي يجب أن يعمل لكل الصفحات إلى مكان واحد:

function withSharedGetServerSideProps() {
  return storeWrapper.getServerSideProps((store) => async (context) => {
    store.dispatch(fetchTenant.initiate());
    await Promise.all(store.dispatch(getRunningTenantQueries()));
    const errors = getQueryErrors(fetchTenant, store);

    if (errors) {
      return errors;
    }
    const tenant = fetchTenant.select()(store.getState())?.data;

    if (!tenant) {
      return {
        notFound: true
      };
    }
    try {
      const env = process.env.NODE_ENV;
      const shouldRedirect = true;

      if (env === "production" && shouldRedirect) {
        return {
          redirect: {
            destination: `https://${tenant.domain}${context.resolvedUrl}`,
            statusCode: 301
          }
        };
      }
    } catch (e) {
      // eslint-disable-next-line no-console
      console.log("Error redirecting to tenant domain", e);
    }

    const session = await getServerSession(req, res, authOptions);

    if (!session && tenant.force_login) {
      /*...*/
    }

    if (tenant?.disable_registration) {
      /*...*/
    }

    return {
      props: {}
    };
  });
}

وعند استخدامه في أي مكان آخر، مثل داخل صفحة index.tsx:

export const getServerSideProps = withCommonGetServerSideProps();

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

الحل، الجزء الثاني:

من أساسيات الـ HOF أنه يستقبل دالة Callback، مما يتيح استدعاءها واستخدام النتيجة الخاصة بها في أي مكان:

export function withCommonGetServerSideProps(pageGetServerSidePropsFunc) {
  return storeWrapper.getServerSideProps((store) => async (context) => {
    // shared code
    /*....*/

    if (pageGetServerSidePropsFunc) {
      const additionalProps = await pageGetServerSidePropsFunc({
        ...context,
        store
      });

      if (typeof additionalProps === "object" && "props" in additionalProps && additionalProps) {
        return {
          props: {
            ...additionalProps.props
          }
        };
      }

      return additionalProps;
    }

    return {
      props: {}
    };
  });
}

يمكن الآن تنفيذ كود مخصص لكل صفحة باستخدام الدالة:

slug.tsx

export const getServerSideProps = withCommonGetServerSideProps(async ({ store, query }) => {
  const { slug } = query;

  store.dispatch(fetchPage.initiate(slug as string));
  await Promise.all(store.dispatch(getRunningPageQueries()));

  const errors = getQueryErrors(fetchPage, store, slug as string);

  if (errors) {
    return errors;
  }

  return {
    props: {}
  };
});

وفي حال لم يكن هناك كود خاص بالصفحة، يكفي استدعاء الكود المشترك لجميع الصفحات، وعندها ستُحل المشكلة index.tsx:

export const getServerSideProps = withCommonGetServerSideProps();

الختام:

  • عندما تواجه أي مشكلة تقنية وتجد صعوبة في إيجاد الحل المناسب، حاول أن تستلهم من التطبيقات المفتوحة المصدر، ولا توجد أي مشكلة في الاستعانة بأحد تطبيقات الـ GPT.
  • عند تطبيق الحل، قم بتطبيق الجزء الذي يناسب المشكلة أو التحدي الذي لديك فقط، دون زيادة أو نقصان.
  • دائمًا تأكد أن يكون الحل سهل الصيانة، بحيث يستطيع مطور آخر فهم طريقة الحل دون مواجهة مشاكل.

وأنت، ماذا تعتقد؟ هل تستخدم طريقة مختلفة؟ أو لديك حل أفضل من أجل التعامل مع حالات مشابهة؟ تواصل معي على X وأخبرني عن تجربتك ورأيك!