yazn_108 image

يزن احمد(@yazn_108)مطور مواقع فرونت اند

لا تتصل بنا بل تواصل معي

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

اشترك في النشرة البريدية

ما راح أزعجك برسائل كثيرة، كل القصة إنك تحط بريدك عشان يوصلك تنبيه أول ما أنشر مقالات جديدة. بسيطة وسهلة 👌 أهلاً بك.

هدف المنصة/من انا؟شروط الاستخدام

© جميع الحقوق محفوظة لـyazn_108

ماهو React Design Patterns?

طرق أو أساليب احترافية لتنظيم وكتابة الكود في React/Nextjs، بحيث يكون الكود نظيف، قابل لإعادة الاستخدام، وسهل الصيانة مع نمو المشروع.

  1. اهمية استخدام Design Patterns
  2. Compound Components Pattern
  3. Container-Presentational Pattern
  4. Custom Hooks Pattern
  5. Higher-Order Components (HOCs)
  6. Portal Pattern
  7. Variant Props Pattern
  8. المصادر
  9. اشترك في النشرة البريدية

اهمية استخدام Design Patterns

الـDesign Patterns هي طرق أو أساليب احترافية لتنظيم وكتابة الكود في React، بحيث يكون الكود نظيف، قابل لإعادة الاستخدام، وسهل الصيانة مع نمو المشروع. لما مشروعك يبدأ يكبر، الكود بيبدأ "يتعقد" — تلاقي نفسك تكرر منطق، تعمل تغييرات كثيرة في أماكن مختلفة، أو تضيف ميزة فتتكسر مية حاجة بعدها. هنا تأتي الـDesign Patterns كمنقذ، بتعطيك حلول ذكية ومنظّمة للمشاكل هذه؛ فالـDesign Patterns ليست مجرد قواعد أو طريقة كتابة، هي خطة واضحة تساعدك تتعامل مع المشاكل اللي تواجهك يوميًا في تطوير خلينا من الكلام النظري وندخل في الامثلة العملية لأن الدزاين باترنس اما انك تتعلمها بشكل عملي او انك رح تواجه صعوبة في استخدامها فيما بعد او حتى معرفة اهميتها…..

Compound Components Pattern

قبل ما اعرف عن هذا النمط رح اقلك ان هذا النمط هو اللي جذبني لموضوع الـDesign Patterns اما عن تعريفه فهو نمط فكرته تكمن في امكانية استخدام عدة مكونات من من مكون واحد بمعنى لو عندك Card.tsx انت يمكن في مكان ما في الموقع تريد عرض كامل الـCard وفي مكان آخر تريد عرض مثلا الـCard بدون صورة وفي مكان آخر مع فيديو فمن غير الاحترافي انك تمرر prop لكل ميزة وبنفس الوقت من اهم قواعد البرمجة عدم تكرار الكود اللي تسويه فما رح يكون امر احترافي انك تسوي component منفصل لكل ميزة فهنا تأتي جمايلة Compound Components Pattern (وطبعا المثال السابق هو احد استخداماته لأن يمكن استخدامه بفكرة رح اشير لها في الاسفل بعد ما اعرضلك كود عن هذا الـPattern الجميل)

tsx

// app/_components/Card.tsx
export default function Card({ children }: { children: React.ReactNode }) {
  return <div className="card rounded-md">{children}</div>;
}
Card.Title = function Title() {
  return (
    <div>
      <h3>Card Title</h3>
    </div>
  );
};
Card.Content = function Content({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <p>{children}</p>
    </div>
  );
};
Card.Action = function Action() {
  return (
    <div>
      <button>Action</button>
    </div>
  );
};
Card.Footer = function Footer() {
  return (
    <div>
      <p>Card footer</p>
    </div>
  );
};

النتيجة او آلية الاستخدام...

tsx

// app/page.tsx
<Card>
  <Card.Title />
  <Card.Content>123</Card.Content>
  <Card.Action />
  <Card.Footer />
</Card>;

اما الفكرة اللي قلت اني رح اشير لها بخصوص هذا النمط هي استخدام هذا النمط لعرض مثلا رسائل الركويستات او حتى التنبيهات اللي تظهر بشكل عائم امام اليوزر في حال ما حبيت تستخدم مكاتب جاهزة

tsx

// app/_Articles/_components/ArticlesState.tsx
import React from "react";
const ArticlesState = ({ children }: { children: React.ReactNode }) => {
  return <div>{children}</div>;
};
ArticlesState.loading = function Loading() {
  return (
    <div>
      <h2> جار تحميل المقالات</h2>
    </div>
  );
};
ArticlesState.error = function Error() {
  return (
    <div>
      <h2>حدث خطأ في تحميل المقالات</h2>
    </div>
  );
};
ArticlesState.empty = function empty() {
  return (
    <section>
      <h2>لا يوجد مقالات بعد...</h2>
    </section>
  );
};
export default ArticlesState;

احد اشكال الاستدعاء

tsx

// app/page.tsx
const Articles = dynamic(() => import("./_Articles/Articles"), {
  loading: () => (
    <ArticlesState>
      <ArticlesState.loading />
    </ArticlesState>
  ),
});

Container-Presentational Pattern

وهنا جينا لعنوان رنان في عالم البرمجة وهو الـclean code او الكود النظيف لأن هذا النمط او الـPattern رح يساعدك في هذا الامر؛ الـContainer Presentational Pattern هو نمط يقلك تعلم انك تفصل المنطق البرمجي عن اجزاء العرض حتى يكون الكود سهل الفهم والتتبع والتصحيح او التعديل بمعنى تخيل مثلا عندك صفحة تحتوي على ارسال واستقبال وتعديل وحذف بيانات (جميع العمليات التي تندرج تحت مفهوم CRUD) بالاضافة للكود اللي يعرض نتيجة هذه العمليات... هنا هل تشوف ان الكود رح يكون مقبول شكليا او على الاقل سهل التعامل معاه؟ خاصة عند محاولة اضافة او تغيير ميزة هنا رح تشوف المعنى الحرفي لمصطلح الـspaghetti code وهنا يأتي دور الـContainer Presentational Pattern بحيث ينبهك إلى ان تقوم بفصل كود او واجهة العرض عن المنطق البرمجي

المنطق البرمجي

tsx

// app/_Articles/Articles.tsx
"use client";
import axios from "axios";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useEffect } from "react";
import { useInView } from "react-intersection-observer";
import PresentationalArticles from "./PresentationalArticles";
import { ArticlesResponse } from "@/types/Articles";
export default function Articles({
  initialArticles,
  tag,
}: {
  initialArticles: ArticlesResponse;
  tag?: string;
}) {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading,
    isError,
  } = useInfiniteQuery({
    queryKey: ["articles", tag],
    queryFn: ({ pageParam = 1 }) =>
      axios
        .get(
          !tag
            ? `/api/articles?page=${pageParam}&limit=6`
            : `/api/tags/${tag}?page=${pageParam}&limit=6`
        )
        .then((res) => res.data as ArticlesResponse),
    getNextPageParam: (lastPage) =>
      lastPage.pagination?.hasMore ? lastPage.pagination.page + 1 : undefined,
    initialData: {
      pages: [initialArticles!],
      pageParams: [1],
    },
    initialPageParam: 1,
  });
  const { ref, inView } = useInView({
    threshold: 0,
    rootMargin: "1000px",
  });
  useEffect(() => {
    if (inView && hasNextPage && !isFetchingNextPage) {
      fetchNextPage();
    }
  }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
  const articles =
    data?.pages
      .flatMap((page) => page.articles)
      .filter((article) => article?._id && article?.slug)
      .filter(
        (article, index, self) =>
          index === self.findIndex((a) => a?._id === article?._id)
      ) || [];
  return (
    <PresentationalArticles
      isLoading={isLoading}
      isError={isError}
      articles={articles}
      hasNextPage={hasNextPage}
      inViewRef={ref}
    />
  );
}

منطق عرض نتائج محتوى الكود

tsx

// app/_Articles/PresentationalArticles.tsx
import { PresentationalArticlesProps } from "@/types/Articles";
import dynamic from "next/dynamic";
import Link from "next/link";
import React from "react";
import ArticlesState from "./_components/ArticlesState";
const ArticleImage = dynamic(() => import("./_components/ArticleImage"), {
  ssr: false,
  loading: () => (
    <div className=" w-full h-[300px] rounded-2xl aspect-[16/9] bg-gray-600 animate-pulse" />
  ),
});
const PresentationalArticles: React.FC<PresentationalArticlesProps> = ({
  isError,
  isLoading,
  articles,
  hasNextPage,
  inViewRef,
}) => {
  return (
    <>
      {isLoading && (
        <ArticlesState>
          <ArticlesState.loading />
        </ArticlesState>
      )}
      {isError && (
        <ArticlesState>
          <ArticlesState.error />
        </ArticlesState>
      )}
      {articles.length === 0 && !isLoading && (
        <ArticlesState>
          <ArticlesState.empty />
        </ArticlesState>
      )}
      <section className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 px-0 py-5 sm:p-5 gap-x-10 gap-y-16">
        {articles.map((article, i: number) => (
          <div key={article._id} className="space-y-4">
            <Link href={`/article/${article.slug}`} className="block">
              <ArticleImage banner={article.banner} i={i} />
            </Link>
            <p className="flex justify-center items-center gap-5">
              <span className="text-secondary/75">
                {new Date(article.createdAt).toLocaleDateString("en-GB")}
              </span>
              <Link
                href={`/tags/${article.tag}`}
                className="capitalize truncate max-w-32 text-secondary bg-[#1e293999] hover:bg-[#1e2939] py-1 px-4 rounded-2xl transition-all"
              >
                {article.tag}
              </Link>
            </p>
            <Link href={`/article/${article.slug}`}>
              <h2 className="text-xl font-bold mt-2 mb-4 truncate max-w-full">
                {article.title}
              </h2>
              <p className="line-clamp-3 text-secondary max-w-full">
                {article.description}
              </p>
            </Link>
          </div>
        ))}
      </section>
      {hasNextPage && (
        <div ref={inViewRef} className="w-full h-40">
          {/* {isFetchingNextPage ? (
            <div className="flex items-center gap-2">
              <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
              <span className="text-secondary">جاري تحميل المزيد...</span>
            </div>
          ) : (
            <div className="text-secondary">اسحب لأسفل لتحميل المزيد</div>
          )} */}
        </div>
      )}
    </>
  );
};
export default PresentationalArticles;

لاحظ هنا اننا استفدنا من النمط السابق نمط Compound Components في عرض حالات البيانات العائدة من API استدعاء محتوى المقالات

Custom Hooks Pattern

هنا لا تخاف ما رح اتكلم كثير بس رح اشوفك كيف ان ممكن تحويل منطق برمجي على شكل hook او component منفصل امر ممتع.....

الـhook ومنطقها البرمجي

tsx

// app/_Articles/_components/initialArticlesHook.tsx
import { ArticlesResponse } from "@/types/Articles";
import ArticlesState from "./ArticlesState";
const initialArticlesHook = async ({ tag }: { tag?: string } = {}) => {
  const res = await fetch(
    !tag
      ? `${process.env.url}/api/articles?page=1&limit=6`
      : `${process.env.url}/api/tags/${tag}?page=1&limit=6`,
    {
      next: { revalidate: !tag ? 1000 * 60 * 5 : 0 },
    }
  );
  if (!res.ok) {
    <ArticlesState>
      <ArticlesState.error />
    </ArticlesState>;
  }
  const initialArticles: ArticlesResponse = await res.json();
  if (initialArticles.articles?.length === 0) {
    <ArticlesState>
      <ArticlesState.empty />
    </ArticlesState>;
  }
  return { initialArticles };
};
export default initialArticlesHook;

الاستفادة

tsx

// app/page.tsx
import React from "react";
import ArticlesState from "./_Articles/_components/ArticlesState";
import dynamic from "next/dynamic";
import initialArticlesHook from "./_Articles/_components/initialArticlesHook";
const Articles = dynamic(() => import("./_Articles/Articles"), {
  loading: () => (
    <ArticlesState>
      <ArticlesState.loading />
    </ArticlesState>
  ),
});
const page = async () => {
  const { initialArticles } = await initialArticlesHook();
  return (
    <div className="p-5 space-y-4">
      <h1 className="text-3xl md:text-4xl text-center font-bold">مُركَّز</h1>
      <p className="text-center text-xl md:text-2xl text-secondary max-w-4xl m-auto">
        مُركَّز منصة اجمع فيها مقالاتي التي تكون عبارة عن ملخص لمعلومة برمجية
        بدون كثرة مقدمات او تمطيط للكلام....
      </p>
      <Articles initialArticles={initialArticles} />
    </div>
  );
};
export default page;

وبهذا الشكل اصبح بامكانك الاستفادة من هذا المنطق البرمجي في اي مكان في الموقع وبدون تكرار وتكون طبقت مصطلح don't repeat yourself في عالم البرمجة

Higher-Order Components (HOCs)

اذا عندك خواص عامة مشتركة بين عدة مكونات او تريد تنفيذ شرط معين على عدة مكونات فهذا النمط يخليك تحدد هذا المنطق او الخواص من مكان محدد ثم كل عنصر جديد يمر بداخله يحصل على هذه الميزات وهو شبيه بنمط الـCustom Hooks Pattern إلى حد ما، ولكن ركز معي لأني رح اضيف شكلين لطريقة استخدامه والشكل الثاني مهم في عالم الحماية من ناحية الفرونت اند

اولا- اعطاء خواص معينة لعدة عناصر (وهنا كما قلت يشبه موضوع الـCustom Hooks Pattern إلى حد ما)

tsx

import React from "react";
export default function WithStyles(Component: React.ComponentType<any>) {
  return (props: { [key: string]: any }) => {
    const style = { padding: 10, margin: 10 };
    return <Component style={style} {...props} />;
  };
}
// الاستخدام
import { ButtonHTMLAttributes } from "react";
import WithStyles from "./WithStyles";
export default function Button(props: {
  props: ButtonHTMLAttributes<HTMLButtonElement>;
}) {
  return <button {...props}>Click Me!</button>;
}
export const StyledButton = WithStyles(Button);

ثانيا- استخدامه في عمليات التحقق من تسجيل دخول المستخدم على سبيل المثال (Authorization/Authentication)، الفكرة انه قبل ما تعرض الـComponent يتحقق من شرط معين (مثلاً حالة تسجيل الدخول أو صلاحيات المستخدم)

tsx

"use client"
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { ComponentType } from "react";
function withAuth<P>(WrappedComponent: ComponentType<P>) {
  const ComponentWithAuth = (props: P) => {
    const router = useRouter();
    const [isAuthorized, setIsAuthorized] = useState<boolean | null>(null);
    useEffect(() => {
      const token = localStorage.getItem("token");
      if (token) setIsAuthorized(true);
    }, [router]);
    if (isAuthorized === null) return null;
    return <WrappedComponent {...props} />;
  };
  return ComponentWithAuth;
}

لن يتم عرض العناصر المررة إلى "withAuth()" في حال كان المستخدم لم يسجل الدخول بعد

tsx

const DeleteItem = () => <div>🗑️ حذف العنصر</div>;
const ProtectedDeleteItem = withAuth(DeleteItem);
export default function Page() {
  return <ProtectedDeleteItem />;
}

اعلم انه في nextjs وما شابه يمكن منع المستخد من الوصول لبعض الصفحات عبر ملف middleware.ts ولكن تذكر انه للصفحات وليس للمكونات اما الـHOCs هذا النمط يساعدك على التحكم في امكانية الوصول للمكونات وبدون وضع شروط لكل مكون او تكرار نفس الكود (فعاليته تظهر في المشاريع الكبيرة لأنه يختصر امور كثيرة ويقلل الوقت وهذا الهدف من استخدام كل او جل الـPatterns) (تنويه: يتم في كل مرة إنشاء وإرجاع عنصر جديد أو منفصل عن الأصل، بمعنى انه بإمكانك استخدام أكثر من HOC واحد مع نفس المكوّن، لأن التأثير يُطبَّق على النسخة الناتجة من التغليف، وليس على المكوّن الأصلي نفسه)

tsx

const A = withLogger(Button);
const B = withAuth(Button);

Portal Pattern

هو نمط وخاصية بنفس الوقت وفكرة الـPortal Pattern هي وضع بعض العناصر خارج ترتيبها الطبيعي في الـDOM مثلا لو عندك اي شيء يعرض على الشاشة فبطبيعة الحال هو رح يكون داخل الـroot فسيتم تطبيق عليه جميع تأثيرات العنصر الاساسي اللي هو فيه (مثل الـoverflow أو z-index أو position) ولكن احيانا تريد عرض مثلا قائمة منسدلة او نافذة عائمة فتحتاج اولا انها تكون فوق جميع العناصر ثانيا انها ما تتأثر بمكان العنصر الاساسي اللي يحتويه وبنفس الوقت تحتاج ان تأخذ بيانات من هذا العنصر الاساسي كـprops، فهنا يكمن دور هذا النمط حيث يمكنك من انشاء عنصر في اي مكان في الـDOM اين ما كان موضعه الاساسي عبر خاصية createPortal من react-dom ثم تحديد المكان الذي تريد عرضه فيه مع الحفاظ على الاتصال من ناحية المنطق البرمجي مع العنصر الاساسي او كما يطلق عليه العنصر الاب (parent element)

tsx

import React from "react";
import { createPortal } from "react-dom";
interface ModalProps {
  isOpen: boolean;
  handleClose: () => void;
  children: React.ReactNode;
}
export default function Modal({ isOpen, handleClose, children }: ModalProps) {
  if (isOpen === false) return null;
  return createPortal(
    <div
      style={{
        position: "fixed",
        inset: 0,
        background: "aqua",
        display: "flex",
        flexDirection: "column",
        placeItems: "center",
        justifyContent: "center",
      }}
    >
      <div>{children}</div>
      <div>
        <button onClick={handleClose}>close modal</button>
      </div>
    </div>,
    document.body
  );
}

الان اصبح يعرض خارج وفوق الـroot element ومع هذا ما زال يملك صلاحية الوصول للبيانات المشاركة من قبل العنصر الاب والتعديل عليها وارجاعها للاب وكأنه عنصر داخل شجرة العنصر الاب

Variant Props Pattern

احيانا يكون عندك عنصر محدد ومستخدم في عدة امكان في الموقع ولكن له انماط متعددة او تريد انشاء ثييم موحد في الموقع فنمط Variant Props Pattern يتيح لك هذا الامر من خلال اعداد تنسيقات مسبقة ويمكن اختيار تنسيق محدد في كل مرة يتم استدعاء هذا العنصر والميزة في هذا الامر غير انها تساعدك على مشاركة عدة خواص للعنصر او توحيد ثييم الموقع ايضا يسهل التعديل بدل ما تعدل 12 عنصر في 10 ملفات ممكن تعدل كل هذه العناصر من ملف واحد وان تعددت تنسيقاتها

tsx

// app/_components/Button.tsx
interface ButtonProps {
  variant?: "primary" | "secondary" | "danger" | "success" | "warning";
  shadow?: "one" | "two";
  style?: React.CSSProperties;
  children: React.ReactNode;
}
const Button = ({
  variant = "primary",
  shadow = "one",
  style,
  children,
}: ButtonProps) => {
  return (
    <button
      style={{
        ...styles.variants[variant],
        ...styles.boxShadow[shadow],
        ...style,
      }}
    >
      {children}
    </button>
  );
};
const styles = {
  variants: {
    primary: { backgroundColor: "blue", color: "white" },
    secondary: { backgroundColor: "#fff", color: "#000" },
    danger: { backgroundColor: "#f00", color: "#fff" },
    warning: { backgroundColor: "yellow", color: "#000" },
    success: { backgroundColor: "green", color: "#fff" },
  },
  boxShadow: {
    one: { boxShadow: "0 4px 8px rgba(0, 0, 0, 0.2)" },
    two: { boxShadow: "0 8px 16px rgba(0, 0, 0, 0.3)" },
  },
};
export default Button;

النتيجة

tsx

<Button shadow="two" variant="secondary">انقر هنا</Button>
<Button variant="danger" style={{ padding: "10px" }}>حذف</Button>
<Button variant={dark ? "danger" : "primary"}>حذف</Button>

المصادر

1- Shadow Coding بالعربي 2- github.com/Mostafashadow1 3- انا 🙃