فارسی

بررسی عمیق معماری کامپوننت React و مقایسه ترکیب‌بندی و وراثت. بیاموزید چرا React ترکیب‌بندی را ترجیح می‌دهد و الگوهایی مانند HOC، Render Props و Hooks را برای ساخت کامپوننت‌های مقیاس‌پذیر و قابل استفاده مجدد کشف کنید.

معماری کامپوننت در React: چرا ترکیب‌بندی (Composition) بر وراثت (Inheritance) پیروز می‌شود

در دنیای توسعه نرم‌افزار، معماری از اهمیت بالایی برخوردار است. نحوه ساختاردهی کد ما، مقیاس‌پذیری، قابلیت نگهداری و قابلیت استفاده مجدد آن را تعیین می‌کند. برای توسعه‌دهندگانی که با React کار می‌کنند، یکی از اساسی‌ترین تصمیمات معماری حول محور نحوه به اشتراک‌گذاری منطق و UI بین کامپوننت‌ها می‌چرخد. این ما را به یک بحث کلاسیک در برنامه‌نویسی شیءگرا می‌رساند که برای دنیای مبتنی بر کامپوننت React بازآفرینی شده است: ترکیب‌بندی در مقابل وراثت.

اگر پیش‌زمینه‌ای در زبان‌های کلاسیک شیءگرا مانند جاوا یا C++ داشته باشید، وراثت ممکن است اولین انتخاب طبیعی شما باشد. این یک مفهوم قدرتمند برای ایجاد روابط 'is-a' (یک نوعی از) است. با این حال، مستندات رسمی React یک توصیه واضح و قوی ارائه می‌دهد: «در فیس‌بوک، ما از React در هزاران کامپوننت استفاده می‌کنیم و هیچ مورد استفاده‌ای پیدا نکرده‌ایم که در آن ایجاد سلسله‌مراتب وراثت کامپوننت را توصیه کنیم.»

این پست به بررسی جامع این انتخاب معماری می‌پردازد. ما معنای وراثت و ترکیب‌بندی را در زمینه React تشریح می‌کنیم، نشان می‌دهیم که چرا ترکیب‌بندی رویکرد اصولی و برتر است، و الگوهای قدرتمندی را - از کامپوننت‌های رده بالاتر (Higher-Order Components) گرفته تا هوک‌های مدرن - بررسی می‌کنیم که ترکیب‌بندی را به بهترین دوست یک توسعه‌دهنده برای ساخت اپلیکیشن‌های قوی و انعطاف‌پذیر برای مخاطبان جهانی تبدیل می‌کند.

درک رویکرد قدیمی: وراثت چیست؟

وراثت یکی از ستون‌های اصلی برنامه‌نویسی شیءگرا (OOP) است. این امکان را به یک کلاس جدید (فرزند یا زیرکلاس) می‌دهد تا خصوصیات و متدهای یک کلاس موجود (والد یا سوپرکلاس) را به ارث ببرد. این یک رابطه 'is-a' با اتصال محکم ایجاد می‌کند. به عنوان مثال، یک GoldenRetriever یک نوع Dog است که خود یک نوع Animal است.

وراثت در یک زمینه غیرمرتبط با React

بیایید به یک مثال ساده از کلاس جاوااسکریپت نگاه کنیم تا این مفهوم را بهتر درک کنیم:

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // سازنده والد را فراخوانی می‌کند
    this.breed = breed;
  }

  speak() { // متد والد را بازنویسی (override) می‌کند
    console.log(`${this.name} barks.`);
  }

  fetch() {
    console.log(`${this.name} is fetching the ball!`);
  }
}

const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); // Output: "Buddy barks."
myDog.fetch(); // Output: "Buddy is fetching the ball!"

در این مدل، کلاس Dog به طور خودکار خصوصیت name و متد speak را از Animal دریافت می‌کند. همچنین می‌تواند متدهای خود (fetch) را اضافه کرده و متدهای موجود را بازنویسی کند. این یک سلسله‌مراتب سفت و سخت ایجاد می‌کند.

چرا وراثت در React شکست می‌خورد

در حالی که این مدل 'is-a' برای برخی ساختارهای داده‌ای کار می‌کند، هنگام اعمال بر روی کامپوننت‌های UI در React مشکلات قابل توجهی ایجاد می‌کند:

به دلیل این مشکلات، تیم React این کتابخانه را بر اساس یک پارادایم انعطاف‌پذیرتر و قدرتمندتر طراحی کرد: ترکیب‌بندی.

پذیرش روش React: قدرت ترکیب‌بندی

ترکیب‌بندی یک اصل طراحی است که از رابطه 'has-a' (دارای یک) یا 'uses-a' (استفاده می‌کند از) طرفداری می‌کند. به جای اینکه یک کامپوننت نوعی از کامپوننت دیگر باشد، کامپوننت‌های دیگر را دارد یا از عملکرد آن‌ها استفاده می‌کند. کامپوننت‌ها مانند بلوک‌های ساختمانی - شبیه به آجرهای لگو - در نظر گرفته می‌شوند که می‌توانند به روش‌های مختلف برای ایجاد UIهای پیچیده ترکیب شوند بدون اینکه در یک سلسله‌مراتب سفت و سخت قفل شوند.

مدل ترکیبی React فوق‌العاده متنوع است و در چندین الگوی کلیدی خود را نشان می‌دهد. بیایید آن‌ها را از ابتدایی‌ترین تا مدرن‌ترین و قدرتمندترین آن‌ها بررسی کنیم.

تکنیک ۱: دربرگیری (Containment) با `props.children`

ساده‌ترین شکل ترکیب‌بندی، دربرگیری است. در این حالت، یک کامپوننت به عنوان یک کانتینر یا «جعبه» عمومی عمل می‌کند و محتوای آن از یک کامپوننت والد به آن پاس داده می‌شود. React یک پراپ ویژه و داخلی برای این کار دارد: props.children.

تصور کنید به یک کامپوننت `Card` نیاز دارید که بتواند هر محتوایی را با یک حاشیه و سایه ثابت در بر بگیرد. به جای ایجاد انواع `TextCard`، `ImageCard` و `ProfileCard` از طریق وراثت، شما یک کامپوننت `Card` عمومی ایجاد می‌کنید.

// Card.js - یک کامپوننت کانتینر عمومی
function Card(props) {
  return (
    <div className="card">
      {props.children}
    </div>
  );
}

// App.js - استفاده از کامپوننت Card
function App() {
  return (
    <div>
      <Card>
        <h1>خوش آمدید!</h1>
        <p>این محتوا داخل یک کامپوننت Card است.</p>
      </Card>

      <Card>
        <img src="/path/to/image.jpg" alt="یک تصویر نمونه" />
        <p>این یک کارت تصویر است.</p>
      </Card>
    </div>
  );
}

در اینجا، کامپوننت Card نمی‌داند یا اهمیتی نمی‌دهد که چه چیزی را در خود جای داده است. این کامپوننت صرفاً استایل‌دهی پوشش‌دهنده را فراهم می‌کند. محتوای بین تگ‌های باز و بسته <Card> به طور خودکار به عنوان props.children پاس داده می‌شود. این یک مثال زیبا از جداسازی (decoupling) و قابلیت استفاده مجدد است.

تکنیک ۲: تخصصی‌سازی (Specialization) با پراپ‌ها

گاهی اوقات، یک کامپوننت به چندین «فضای خالی» نیاز دارد تا توسط کامپوننت‌های دیگر پر شود. در حالی که می‌توانید از `props.children` استفاده کنید، یک روش صریح‌تر و ساختاریافته‌تر، پاس دادن کامپوننت‌ها به عنوان پراپ‌های معمولی است. این الگو اغلب تخصصی‌سازی نامیده می‌شود.

یک کامپوننت `Modal` را در نظر بگیرید. یک مودال معمولاً دارای یک بخش عنوان، یک بخش محتوا و یک بخش عملیات (با دکمه‌هایی مانند «تایید» یا «لغو») است. ما می‌توانیم `Modal` خود را طوری طراحی کنیم که این بخش‌ها را به عنوان پراپ بپذیرد.

// Modal.js - یک کانتینر تخصصی‌تر
function Modal(props) {
  return (
    <div className="modal-backdrop">
      <div className="modal-content">
        <div className="modal-header">{props.title}</div>
        <div className="modal-body">{props.body}</div>
        <div className="modal-footer">{props.actions}</div>
      </div>
    </div>
  );
}

// App.js - استفاده از Modal با کامپوننت‌های خاص
function App() {
  const confirmationTitle = <h2>تایید عملیات</h2>;
  const confirmationBody = <p>آیا از انجام این عملیات اطمینان دارید؟</p>;
  const confirmationActions = (
    <div>
      <button>تایید</button>
      <button>لغو</button>
    </div>
  );

  return (
    <Modal
      title={confirmationTitle}
      body={confirmationBody}
      actions={confirmationActions}
    />
  );
}

در این مثال، Modal یک کامپوننت طرح‌بندی با قابلیت استفاده مجدد بالا است. ما آن را با پاس دادن عناصر JSX خاص برای `title`، `body` و `actions` تخصصی می‌کنیم. این روش بسیار انعطاف‌پذیرتر از ایجاد زیرکلاس‌های `ConfirmationModal` و `WarningModal` است. ما به سادگی `Modal` را با محتوای مختلف در صورت نیاز ترکیب می‌کنیم.

تکنیک ۳: کامپوننت‌های رده بالاتر (Higher-Order Components - HOCs)

برای اشتراک‌گذاری منطق غیر-UI، مانند واکشی داده، احراز هویت یا لاگ‌گیری، توسعه‌دهندگان React در گذشته به الگویی به نام کامپوننت‌های رده بالاتر (HOCs) روی می‌آوردند. اگرچه در React مدرن تا حد زیادی با هوک‌ها جایگزین شده‌اند، درک آن‌ها بسیار مهم است زیرا یک گام تکاملی کلیدی در داستان ترکیب‌بندی React را نشان می‌دهند و هنوز در بسیاری از کدبیس‌ها وجود دارند.

یک HOC تابعی است که یک کامپوننت را به عنوان آرگومان می‌گیرد و یک کامپوننت جدید و بهبودیافته را برمی‌گرداند.

بیایید یک HOC به نام `withLogger` ایجاد کنیم که هر زمان که پراپ‌های یک کامپوننت به‌روز می‌شوند، آن‌ها را لاگ می‌کند. این برای دیباگ کردن مفید است.

// withLogger.js - The HOC
import React, { useEffect } from 'react';

function withLogger(WrappedComponent) {
  // این یک کامپوننت جدید را برمی‌گرداند...
  return function EnhancedComponent(props) {
    useEffect(() => {
      console.log('کامپوننت با پراپ‌های جدید به‌روز شد:', props);
    }, [props]);

    // ... که کامپوننت اصلی را با پراپ‌های اصلی رندر می‌کند.
    return <WrappedComponent {...props} />;
  };
}

// MyComponent.js - کامپوننتی که قرار است بهبود یابد
function MyComponent({ name, age }) {
  return (
    <div>
      <h1>سلام، {name}!</h1>
      <p>شما {age} سال سن دارید.</p>
    </div>
  );
}

// اکسپورت کردن کامپوننت بهبودیافته
export default withLogger(MyComponent);

تابع `withLogger` کامپوننت `MyComponent` را در بر می‌گیرد و قابلیت‌های لاگ‌گیری جدیدی به آن می‌دهد بدون اینکه کد داخلی `MyComponent` را تغییر دهد. ما می‌توانیم همین HOC را به هر کامپوننت دیگری اعمال کنیم تا همان ویژگی لاگ‌گیری را به آن بدهیم.

چالش‌های HOCs:

تکنیک ۴: رندر پراپ‌ها (Render Props)

الگوی Render Prop به عنوان راه‌حلی برای برخی از کاستی‌های HOCs ظهور کرد. این الگو روشی صریح‌تر برای اشتراک‌گذاری منطق ارائه می‌دهد.

یک کامپوننت با یک رندر پراپ، یک تابع را به عنوان پراپ (معمولاً با نام `render`) می‌گیرد و آن تابع را فراخوانی می‌کند تا مشخص کند چه چیزی باید رندر شود، و هرگونه وضعیت یا منطق را به عنوان آرگومان به آن پاس می‌دهد.

بیایید یک کامپوننت `MouseTracker` ایجاد کنیم که مختصات X و Y ماوس را ردیابی کرده و آن‌ها را در دسترس هر کامپوننتی که بخواهد از آن‌ها استفاده کند قرار دهد.

// MouseTracker.js - کامپوننت با یک رندر پراپ
import React, { useState, useEffect } from 'react';

function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleMouseMove = (event) => {
    setPosition({ x: event.clientX, y: event.clientY });
  };

  useEffect(() => {
    window.addEventListener('mousemove', handleMouseMove);
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []);

  // فراخوانی تابع رندر با وضعیت
  return render(position);
}

// App.js - استفاده از MouseTracker
function App() {
  return (
    <div>
      <h1>ماوس خود را حرکت دهید!</h1>
      <MouseTracker
        render={mousePosition => (
          <p>موقعیت فعلی ماوس ({mousePosition.x}, {mousePosition.y}) است</p>
        )}
      />
    </div>
  );
}

در اینجا، `MouseTracker` تمام منطق ردیابی حرکت ماوس را در خود کپسوله می‌کند. این کامپوننت به تنهایی چیزی را رندر نمی‌کند. در عوض، منطق رندر را به پراپ `render` خود واگذار می‌کند. این روش صریح‌تر از HOCs است زیرا شما می‌توانید دقیقاً ببینید که داده‌های `mousePosition` از کجا می‌آیند، درست در داخل JSX.

پراپ `children` نیز می‌تواند به عنوان یک تابع استفاده شود که یک نوع رایج و زیبای این الگو است:

// استفاده از children به عنوان یک تابع
<MouseTracker>
  {mousePosition => (
    <p>موقعیت فعلی ماوس ({mousePosition.x}, {mousePosition.y}) است</p>
  )}
</MouseTracker>

تکنیک ۵: هوک‌ها (Hooks) (رویکرد مدرن و ترجیحی)

هوک‌ها که در React 16.8 معرفی شدند، نحوه نوشتن کامپوننت‌های React را متحول کردند. آن‌ها به شما اجازه می‌دهند تا از state و سایر ویژگی‌های React در کامپوننت‌های تابعی استفاده کنید. مهمتر از همه، هوک‌های سفارشی (custom Hooks) ظریف‌ترین و مستقیم‌ترین راه‌حل را برای اشتراک‌گذاری منطق دارای وضعیت (stateful) بین کامپوننت‌ها ارائه می‌دهند.

هوک‌ها مشکلات HOCs و Render Props را به روشی بسیار تمیزتر حل می‌کنند. بیایید مثال `MouseTracker` خود را به یک هوک سفارشی به نام `useMousePosition` بازسازی کنیم.

// hooks/useMousePosition.js - یک هوک سفارشی
import { useState, useEffect } from 'react';

export function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (event) => {
      setPosition({ x: event.clientX, y: event.clientY });
    };

    window.addEventListener('mousemove', handleMouseMove);
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []); // آرایه وابستگی خالی به این معنی است که این effect فقط یک بار اجرا می‌شود

  return position;
}

// DisplayMousePosition.js - کامپوننتی که از هوک استفاده می‌کند
import { useMousePosition } from './hooks/useMousePosition';

function DisplayMousePosition() {
  const position = useMousePosition(); // فقط هوک را فراخوانی کنید!

  return (
    <p>
      موقعیت ماوس ({position.x}, {position.y}) است
    </p>
  );
}

// یک کامپوننت دیگر، شاید یک عنصر تعاملی
import { useMousePosition } from './hooks/useMousePosition';

function InteractiveBox() {
  const { x, y } = useMousePosition();

  const style = {
    position: 'absolute',
    top: y - 25, // جعبه را روی نشانگر ماوس وسط‌چین می‌کند
    left: x - 25,
    width: '50px',
    height: '50px',
    backgroundColor: 'lightblue',
  };

  return <div style={style} />;
}

این یک پیشرفت بزرگ است. هیچ «جهنم پوشش‌دهنده‌ای»، هیچ تداخل نام‌گذاری پراپی و هیچ تابع رندر پراپ پیچیده‌ای وجود ندارد. منطق به طور کامل در یک تابع قابل استفاده مجدد (`useMousePosition`) جدا شده است و هر کامپوننتی می‌تواند با یک خط کد واضح، به آن منطق دارای وضعیت «متصل» شود. هوک‌های سفارشی بیان نهایی ترکیب‌بندی در React مدرن هستند که به شما امکان می‌دهند کتابخانه خود را از بلوک‌های منطقی قابل استفاده مجدد بسازید.

مقایسه سریع: ترکیب‌بندی در مقابل وراثت در React

برای خلاصه کردن تفاوت‌های کلیدی در زمینه React، در اینجا یک مقایسه مستقیم آورده شده است:

جنبه وراثت (ضد-الگو در React) ترکیب‌بندی (روش ترجیحی در React)
رابطه رابطه 'is-a' (یک نوعی از). یک کامپوننت تخصصی، نسخه‌ای از یک کامپوننت پایه است. رابطه 'has-a' (دارای) یا 'uses-a' (استفاده می‌کند از). یک کامپوننت پیچیده، کامپوننت‌های کوچکتر را دارد یا از منطق مشترک استفاده می‌کند.
اتصال (Coupling) بالا. کامپوننت‌های فرزند به شدت به پیاده‌سازی والد خود متصل هستند. پایین. کامپوننت‌ها مستقل هستند و می‌توانند بدون تغییر در زمینه‌های مختلف دوباره استفاده شوند.
انعطاف‌پذیری پایین. سلسله‌مراتب‌های سفت و سخت مبتنی بر کلاس، اشتراک‌گذاری منطق را در درخت‌های کامپوننت مختلف دشوار می‌سازد. بالا. منطق و UI می‌توانند به روش‌های بی‌شماری، مانند بلوک‌های ساختمانی، ترکیب و دوباره استفاده شوند.
قابلیت استفاده مجدد کد محدود به سلسله‌مراتب از پیش تعریف شده. شما کل «گوریل» را دریافت می‌کنید در حالی که فقط «موز» را می‌خواستید. عالی. کامپوننت‌ها و هوک‌های کوچک و متمرکز می‌توانند در کل اپلیکیشن استفاده شوند.
اصطلاح رایج در React توسط تیم رسمی React توصیه نمی‌شود. رویکرد توصیه شده و اصولی برای ساخت اپلیکیشن‌های React.

نتیجه‌گیری: با ذهنیت ترکیب‌بندی فکر کنید

بحث بین ترکیب‌بندی و وراثت یک موضوع بنیادین در طراحی نرم‌افزار است. در حالی که وراثت جایگاه خود را در OOP کلاسیک دارد، ماهیت پویا و مبتنی بر کامپوننت توسعه UI آن را به گزینه‌ای نامناسب برای React تبدیل می‌کند. این کتابخانه از اساس برای پذیرش ترکیب‌بندی طراحی شده است.

با ترجیح دادن ترکیب‌بندی، شما به دست می‌آورید:

به عنوان یک توسعه‌دهنده جهانی React، تسلط بر ترکیب‌بندی فقط به معنای پیروی از بهترین شیوه‌ها نیست - بلکه به معنای درک فلسفه اصلی است که React را به ابزاری قدرتمند و کارآمد تبدیل می‌کند. با ایجاد کامپوننت‌های کوچک و متمرکز شروع کنید. از `props.children` برای کانتینرهای عمومی و از پراپ‌ها برای تخصصی‌سازی استفاده کنید. برای اشتراک‌گذاری منطق، در وهله اول به سراغ هوک‌های سفارشی بروید. با فکر کردن به روش ترکیب‌بندی، در مسیر ساخت اپلیکیشن‌های React زیبا، قوی و مقیاس‌پذیر که آزمون زمان را پس می‌دهند، قرار خواهید گرفت.