کامپوننت های مرتبه بالاتر در React – بخش ۲

در این مقاله به ادامه مبحث Higher-Order Components در React خواهیم پرداخت. مشاهده بخش ۱

کامپوننت اصلی را تغییر ندهید. از ترکیب‌بندی استفاده کنید

در برابر این وسوسه که نمونه اولیه و اصلی کامپوننت را در داخل یک HOC وایرایش کنید یا تغییرش دهید، مقاومت کنید.

function logProps(InputComponent) {
  InputComponent.prototype.componentWillReceiveProps = function(nextProps) {
    console.log('Current props: ', this.props);
    console.log('Next props: ', nextProps);
  };
  // The fact that we're returning the original input is a hint that it has
  // been mutated.
  return InputComponent;
}

// EnhancedComponent will log whenever props are received
const EnhancedComponent = logProps(InputComponent);

این عمل چند مشکل ایجاد می‌کند. یکی این است که کامپوننت ورودی نمی‌تواند به طور مستقل قابل استفادۀ مجدد باشد. مهم‌تر این که، اگر شما HOC دیگری را در EnhancedComponent اجرا کنید که componentWillReceiveProps را نیز تغییر می‌دهد، HOC نخستین، از نظر عملکردر باطل خواهد شد. همچنین این HOC دیگر با کامپوننت‌های تابعی که متدهای چرخۀ زندگی را ندارند، کار نخواهد کرد.

تغییر HOC ها یک leaky abstraction است. مصرف کننده (consumer) باید بداند چگونه آنها پیاده سازی شده‌اند تا از ناسازگاری با HOC های دیگر جلوگیری شود.

به جای تغییر دادن، HOC ها باید از ترکیب‌بندی (composition) استفاده کنند، یعنی با بسته بندی کامپوننت ورودی در یک کامپوننت نگه دارنده (container):

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentWillReceiveProps(nextProps) {
      console.log('Current props: ', this.props);
      console.log('Next props: ', nextProps);
    }
    render() {
      // Wraps the input component in a container, without mutating it. Good!
      return <WrappedComponent {...this.props} />;
    }
  }
}

این HOC همان عملکرد نسخۀ تغییر داده شده را دارد، با این تفاوت که از ناسازگاری‌ها جلوگیری می‌کند. همچنین با کامپوننت‌های تابعی و class نیز به خوبی کار می‌کند. و چون تابعی خالص است، قابل خواندن با HOC های دیگر و حتی با خودش است.

ممکن است شما متوجه شباهت‌های بین HOC ها و الگویی به نام کامپوننت‌های نگه دارنده (container components) شده باشید. کامپوننت‌های نگه دارنده بخشی از استراتژی تقسیم مسئولیت‌ها بین نگرانی‌های سطح بالا و سطح پایین هستند. نگه دارنده‌ها چیزها را مانند حالت (state) مدیریت می‌کنند، و prop ها را منتقل می‌کنند به کامپوننت‌هایی که چیزها را مدیریت می‌کنند مانند رندر رابط کاربری. HOC ها از نگه دارنده‌ها به عنوان بخشی از پیاده سازی خود استفاده می‌کنند. شما می‌توانید به HOC ها به عنوان تعریف کامپوننت نگه دارندۀ پارامتریک فکر کنید.

قرارداد: prop های غیرمرتبط را به کامپوننت‌های بسته بندی شده منتقل کنید

HOC ها feature ها را به یک کامپوننت اضافه می‌کنند. آنها نباید به شدت قرارداشان را عوض کنند. انتظار می‌رود که کامپوننتِ برگردانده شده توسط HOC خط اتصال مشابهی با کامپوننت بسته بندی شده داشته باشد.

HOC ها باید prop هایی را که با موضوع خاص آنها غیرمرتبط است را منتقل کنند. اکثر HOC ها یک متد رندر دارند که چیزی شبیه این است:

render() {
  // Filter out extra props that are specific to this HOC and shouldn't be
  // passed through
  const { extraProp, ...passThroughProps } = this.props;

  // Inject props into the wrapped component. These are usually state values or
  // instance methods.
  const injectedProp = someStateOrInstanceMethod;

  // Pass props to wrapped component
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}

این قرارداد کمک می‌کند مطمئن شویم که HOC ها به اندازۀ ممکن انعطاف پذیر هستند.

قرارداد: حداکثر سازی سازگاری

همۀ HOC ها مانند یکدیگر نیستند. گاهی اوقات آنها فقط یک آرگومان را می‌پذیرند، یعنی کامپوننت بسته بندی شده:

const NavbarWithRouter = withRouter(Navbar);

معمولا HOC ها آرگومان‌های بیشتری را می‌پذیرند. در این مثال از Relay از یک آبجکت config برای مشخص کردن وابستگی‌های دیتای کامپوننت استفاده شده است:

const CommentWithRelay = Relay.createContainer(Comment, config);

امضای مشترک اکثر HOC ها شبیه این است:

// React Redux's `connect`
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);

چی شده؟ اگر آن را تجزیه کنید، راحت‌تر می‌توانید درک کنید که چه چیزی رخ داده است.

// connect is a function that returns another function
const enhance = connect(commentListSelector, commentListActions);
// The returned function is a HOC, which returns a component that is connected
// to the Redux store
const ConnectedComment = enhance(CommentList);

به عبارت دیگر، connect یک تابع مرتبۀ بالاتر است که یک کامپوننت مرتبۀ بالاتر را برمی‌گرداند.

این فرم ممکن است گیج کننده و یا غیرضروری باشد، اما یک ویژگی کارآمد دارد. HOC های تک آرگومانی مانند آن یکی که توسط تابع connect بازگردانده شد، امضایی به صورت Component => Component دارند. توابعی که نوع خروجی آنها مشابه نوع ورودی آنهاست به راحتی قابل ترکیب با یکدیگر هستند.

// Instead of doing this...
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))

// ... you can use a function composition utility
// compose(f, g, h) is the same as (...args) => f(g(h(...args)))
const enhance = compose(
  // These are both single-argument HOCs
  withRouter,
  connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)

( این مشخصه همچنین اجازه می‌دهد connect و HOC های تقویت کنندۀ (enhancer) دیگر به عنوان دکوراتور، یک طرح پیشنهادی آزمایشی جاوا اسکریپت، استفاده شوند. )

تابع ابزار compose به وسیلۀ کتابخانه‌های جانبی تامین می‌شود، شامل lodash و Redux و Ramda.

قرارداد: نمایش نام را برای اشکال زدایی راحت، بسته بندی کنید

کامپوننت‌های نگه دارنده که توسط HOC ساخته شده‌اند، در ابزار برنامه نویسی React مانند هر کامپوننت دیگری نمایش داده می‌شوند. جهت تسهیل اشکال زدایی، یک نام نمایشی (display name) انتخاب کنید . این نام نمایشی این موضوع را مکاتبه می‌کند که آن نتیجۀ یک HOC است.

رایج‌ترین تکنیک، بسته بندی نام نمایشیِ کامپوننت بسته بنده شده است. در نتیجه اگر کامپوننت مرتبۀ بالاتر شما withSubscription نامگذاری شده است، و نام نمایشی کامپوننت بسته بندی شده CommentList است، از نام نمایشی WithSubscription(CommentList) استفاده کنید:

function withSubscription(WrappedComponent) {
  class WithSubscription extends React.Component {/* ... */}
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
  return WithSubscription;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

هشدارها

کامپوننت‌های مرتبۀ بالاتر همراه با هشدارهایی هستند که اگر شما در کار کردن با React تازه‌کار هستید، برایتان چندان واضح نخواهند بود.

از HOC ها در داخل یک متد رندر استفاده نکنید

الگوریتم تفاوت React (یا الگوریتم تلفیق، diffing algorithm) از مشخصۀ کامپوننت برای تعیین این که آیا باید زیردرخت (subtree) را آپدیت کند و یا این که آن را کنار بگذارد و زیردرخت جدیدی نصب کند، استفاده می‌کند. اگر کامپوننت بازگشت داده شده از رندر با کامپوننت رندر قبلی یکسان (===) باشد، React به طور بازگشتی زیردرخت را با الگوریتم تفاوت با زیردرخت جدید آپدیت می‌کند. اگر یکسان نباشند، زیردرخت قبلی کاملا منحل(unmount) خواهد شد.

به طور معمول شما نیاز به تفکر دربارۀ این موضوع ندارید. ولی در HOC ها این موضوع مهم می‌شود، چون شما نمی‌توانید یک HOC را به کامپوننتی درون متد رندر یک کامپوننت، اعمال کنید:

render() {
  // A new version of EnhancedComponent is created on every render
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // That causes the entire subtree to unmount/remount each time!
  return <EnhancedComponent />;
}

مشکل در اینجا فقط در مورد عملکرد نیست؛ انحلال یک کامپوننت باعث می‌شود حالت آن کامپوننت و تمام child های آن از دست بروند.

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

در آن موارد نادر که شما نیاز به اعمال دینامیک HOC دارید، می‌توانید آن را در داخل متد چرخۀ زندگی کامپوننت یا constructor آن، انجام دهید.

متدهای استاتیک باید دوباره نوشته شوند

گاهی اوقات تعریف یک متد استاتیک در کامپوننت React مفید است. برای مثال نگه دارنده‌های Relay متد استاتیک getFragment را در معرض می‌گذارند تا ترکیب قطعه‌های GraphQL را تسهیل کنند.

وقتی شما یک HOC را به یک کامپوننت اعمال می‌کنید، کامپوننت اصلی با یک کامپوننت نگه دارنده بسته بندی می‌شود. این یعنی کامپوننت جدید هیچ یک از متدهای استاتیک کامپوننت اصلی را ندارد.

برای حل این موضوع، شما می‌توانید متدها را داخل نگه دارنده، قبل از بازگشت دادن آن، کپی کنید:

// Define a static method
WrappedComponent.staticMethod = function() {/*...*/}
// Now apply a HOC
const EnhancedComponent = enhance(WrappedComponent);

// The enhanced component has no static method
typeof EnhancedComponent.staticMethod === 'undefined' // true

البته شما باید بدانید دقیقا کدام متد باید کپی گردد. شما می‌توانید از  hoist-non-react-statics برای کپی خودکار تمام متدهای استاتیک غیر ری‌اکت استفاده کنید:

function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // Must know exactly which method(s) to copy 🙁
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}

یک راه حل دیگر، export کردن متد استاتیک به طور جداگانه از کامپوننت است.

// Instead of...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...export the method separately...
export { someFunction };

// ...and in the consuming module, import both
import MyComponent, { someFunction } from './MyComponent.js';

Ref ها منتقل نمی‌شوند

در حالیکه قرارداد کامپوننت‌های مرتبۀ بالاتر همۀ prop ها را به کامپوننت بسته بندی شده منتقل می‌کند، این عمل برای ref ها انجام نمی‌شود. دلیل آن این است که ref مانند key واقعا یک prop نیست، و به طور خاص توسط React مدیریت می‌شود. اگر ref را به المانی که کامپوننت آن از یک HOC حاصل شده است اضافه کنید، ref به نمونۀ بیرونی‌ترین کامپوننت نگه دارنده ارجاع می‌دهد، نه کامپوننت بسته بندی شده.

راه حل این مشکل، استفاده از React.forwardRef API است که در نسخۀ ۱۶.۳ ری‌اکت تعریف شد. اطلاعات بیشتر در مورد آن را می‌توانید در بخش انتقال ref ها ببینید.

1 Star2 Stars3 Stars4 Stars5 Stars (3 votes, average: 5٫00 out of 5)
Loading...
counter customizable free hit