7 نکته مربوط به کامپوننت های React
ﺯﻣﺎﻥ ﻣﻄﺎﻟﻌﻪ: 10 دقیقه

7 نکته مربوط به کامپوننت های React

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

Prop های بسیار زیاد

عبور بیش از حد propها به یک کامپوننت واحد ممکن است نشانه تقسیم آن باشد.

ممکن است در موقعیتی قرار بگیرید که یک کامپوننت دارای 20 قطعه یا بیشتر باشد و همچنان راضی باشید که فقط یک کار انجام می‌دهد. اما هنگامی که به کامپوننتی برخورد می‌کنید که دارای propهای بسیار زیادی است و می‌خواهید فقط یک مورد دیگر به لیست طولانی propها اضافه کنید، باید چند مورد را در نظر بگیرید:

آیا این کامپوننت کارهای مختلفی انجام می‌دهد؟

مانند توابع، کامپوننت ها نیز باید یک کار را به درستی انجام دهند. بنابراین همیشه خوب است که بررسی کنید آیا امکان تقسیم کامپوننت به چندین کامپوننت کوچکتر وجود دارد یا نه. به عنوان مثال اگر این کامپوننت دارای propهای ناسازگار باشد یا JSX را از توابع برگرداند.

آیا می‌توانم از ترکیب استفاده کنم؟

الگویی که بسیار خوب است اما غالبا نادیده گرفته می‌شود، ترکیب کامپوننت های سازنده به جای کنترل تمام منطق‌های موجود تنها در یک مورد است. بیایید بگوییم ما یک کامپوننت داریم که یک برنامه کاربری را برای برخی از سازمان‌ها مدیریت می‌کند:

<ApplicationForm
  user={userData}
  organization={organizationData}
  categories={categoriesData}
  locations={locationsData}
  onSubmit={handleSubmit}
  onCancel={handleCancel}
  ...
/>

با نگاهی به propهای این کامپوننت می‌فهمیم که همه آنها مربوط به کاری است که کامپوننت انجام می‌دهد، اما هنوز راه دیگری برای بهبود این کار وجود دارد و آن این است که بعضی از کامپوننت‌ها را به عهده فرزندان بگذاریم:

<ApplicationForm onSubmit={handleSubmit} onCancel={handleCancel}>
  <ApplicationUserForm user={userData} />
  <ApplicationOrganizationForm organization={organizationData} />
  <ApplicationCategoryForm categories={categoriesData} />
  <ApplicationLocationsForm locations={locationsData} />
</ApplicationForm>

اکنون مطمئن شدیم که ApplicationForm تنها از کمترین مسئولیت خود یعنی ارسال و لغو فرم استفاده می‌کند. کامپوننت‌های فرزند می‌توانند هر چیزی مربوط به بخش بزرگتر تصویر را مدیریت کنند. این همچنین فرصتی عالی برای استفاده از React Context برای برقراری ارتباط بین فرزندان و والدین آنها است.

آیا من بسیاری از prop های پیکربندی را منتقل می‌کنم؟

در بعضی موارد بهتر است برای سهولت در تعویض پیکربندی، propها را در یک شی options گروه بندی کنید. به عنوان مثال یک کامپوننت داشته باشیم که به نوعی grid یا table را نشان می‌دهد:

<Grid
  data={gridData}
  pagination={false}
  autoSize={true}
  enableSort={true}
  sortOrder="desc"
  disableSelection={true}
  infiniteScroll={true}
  ...
/>

همه این propها به جز data را می‌توان پیکربندی در نظر گرفت. در مواردی از این دست، گاهی اوقات ایده خوبی است که Grid را تغییر دهید تا در عوض یک prop options را بپذیرد.

const options = {
  pagination: false,
  autoSize: true,
  enableSort: true,
  sortOrder: 'desc',
  disableSelection: true,
  infiniteScroll: true,
  ...
}

<Grid
  data={gridData}
  options={options}
/>

این بدان معنی است که حذف optionهای پیکربندی که نمی خواهیم استفاده کنیم در صورت جابجایی بین optionهای مختلف آسان‌تر است.

Prop های ناسازگار

از عبور propهای ناسازگار با یکدیگر خودداری کنید.

به عنوان مثال ممکن است با ایجاد یک کامپوننت رایج </ Input> شروع کنیم که فقط برای مدیریت متن باشد، اما بعد از مدتی امکان استفاده از آن را برای شماره تلفن‌ها نیز اضافه می‌کنیم. پیاده سازی آن می‌تواند چیزی شبیه به این باشد:

function Input({ value, isPhoneNumberInput, autoCapitalize }) {
  if (autoCapitalize) capitalize(value)

  return <input value={value} type={isPhoneNumberInput ? 'tel' : 'text'} />
}

مشكل این مسئله آن است كه propهای isPhoneNumberInput و autoCapitalize با هم جور در نمی‌ایند و واقعا نمی‌توانیم شماره تلفن‌ها را بزرگ بنویسیم.

در این حالت، راه حل احتمالا تجزیه کامپوننت اصلی به چند کامپوننت کوچکتر است. اگر هنوز منطقی داریم که می‌خواهیم بین آنها تقسیم کنیم، می‌توانیم آن را به یک custom hook منتقل کنیم:

function TextInput({ value, autoCapitalize }) {
  if (autoCapitalize) capitalize(value)
  useSharedInputLogic()

  return <input value={value} type="text" />
}

function PhoneNumberInput({ value }) {
  useSharedInputLogic()

  return <input value={value} type="tel" />
}

گرچه این مثال کمی ساختگی است، اما یافتن propهای ناسازگار با یکدیگر معمولا نشانه خوبی است که باید بررسی شود آیا کامپوننت از هم جدا شود یا خیر.

کپی کردن propها در state

با کپی کردن propها در state، جریان داده را متوقف نکنید.

این کامپوننت را در نظر بگیرید:

function Button({ text }) {
  const [buttonText] = useState(text)

  return <button>{buttonText}</button>
}

با عبور دادن text prop به عنوان مقدار اولیه useState، کامپوننت در حال حاضر تمام مقادیر به روز شده text را نادیده می‌گیرد. اگر text به روز شود، این کامپوننت هنوز اولین مقدار خود را رندر می‌کند. برای اکثر propها این رفتار غیرمنتظره است که به نوبه خود باعث به وجود آمدن مشکل در کامپوننت می‌گردد.

یک مثال عملی‌تر از این اتفاق زمانی است که ما می‌خواهیم مقدار جدیدی از یک prop را به دست آوریم، به خصوص اگر این کار به محاسبه کندی نیاز دارد. در مثال زیر تابع slowFormatText را برای قالب بندی text-prop خود اجرا می‌کنیم که اجرای آن زمان زیادی می‌برد.

 function Button({ text }) {
  const [formattedText] = useState(() => slowlyFormatText(text))

  return <button>{formattedText}</button>
}

با قرار دادن آن در state این مسئله را حل کردیم که به طور غیر ضروری مجددا تکرار می‌شود اما مانند بالا همچنان از بروزرسانی کامپوننت جلوگیری کردیم. یک روش بهتر برای حل این مسئله استفاده از قلاب useMemo برای به حافظه سپردن نتیجه است:

function Button({ text }) {
  const formattedText = useMemo(() => slowlyFormatText(text), [text])

  return <button>{formattedText}</button>
}

اکنون slowFormatText فقط هنگام تغییر متن اجرا می‌شود و از به روزرسانی کامپوننت جلوگیری نکرده‌ایم.

گاهی اوقات به یک prop نیاز داریم که همه به روزرسانی‌های آن نادیده گرفته شود. به عنوان مثال یک انتخاب کننده رنگ که در آن به گزینه تنظیم رنگ اولیه انتخاب شده نیاز داریم. اما وقتی کاربر رنگی را انتخاب کرد، ما نمی‌خواهیم به روزرسانی انتخاب کاربر را لغو کند. در این حالت کاملا منطقی است که prop را در state کپی کنید، اما برای نشان دادن این رفتار به کاربر، اکثر توسعه دهندگان پیشوند prop را با مقدار اولیه یا پیش فرض (basicColor / defaultColor) نشان می‌دهند.

بازگشت JSX از توابع

JSX را از توابع درون یک کامپوننت برنگردانید.

این الگویی است که با محبوبیت بیشتر کامپوننت‌های تابع تا حد زیادی از بین رفته است، اما هر از چندگاهی با آن روبه رو می‌شویم. برای مثال:

function Component() {
  const topSection = () => {
    return (
      <header>
        <h1>Component header</h1>
      </header>
    )
  }

  const middleSection = () => {
    return (
      <main>
        <p>Some text</p>
      </main>
    )
  }

  const bottomSection = () => {
    return (
      <footer>
        <p>Some footer text</p>
      </footer>
    )
  }

  return (
    <div>
      {topSection()}
      {middleSection()}
      {bottomSection()}
    </div>
  )
}

اگرچه ممکن است در ابتدا اشکالی نداشته باشد اما دلیل آوردن کد را دشوار می‌کند و باید از آنها اجتناب شود. برای حل این مسئله هر کدام از JSX ها را درون خطی می‌کنیم، زیرا بازگشت زیاد مسئله چندان مهمی نیست. اما بیشتر اوقات این دلیل می‌شود که بخش‌ها را به کامپوننت‌های جداگانه تقسیم کنیم.

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

بولین‌های چندگانه برای state

برای state کامپوننت‌ها از استفاده چند بولین خودداری کنید.

هنگام نوشتن یک کامپوننت و متعاقبا افزایش عملکرد آن، به راحتی می‌توانید در موقعیتی ظاهر شوید که چندین بولین برای نشان دادن اینکه کامپوننت در کدام state قرار دارد، داشته باشید. برای کامپوننت کوچکی که با کلیک روی دکمه‌ای درخواست وب را انجام می‌دهد، ممکن است چیزی شبیه به این باشد:

function Component() {
  const [isLoading, setIsLoading] = useState(false)
  const [isFinished, setIsFinished] = useState(false)
  const [hasError, setHasError] = useState(false)

  const fetchSomething = () => {
    setIsLoading(true)

    fetch(url)
      .then(() => {
        setIsLoading(false)
        setIsFinished(true)
      })
      .catch(() => {
        setHasError(true)
      })
  }

  if (isLoading) return <Loader />
  if (hasError) return <Error />
  if (isFinished) return <Success />

  return <button onClick={fetchSomething} />
}

وقتی روی دکمه کلیک می‌شود isLoading را روی true تنظیم می‌کنیم و یک درخواست وب را با واکشی انجام می‌دهیم. اگر درخواست موفقیت آمیز باشد، isLoading را به false و isFinished را به true تنظیم کرده و در صورت وجود خطا، hasError را به true می‌دهیم.

در حالی که این از نظر فنی به درستی کار می‌کند، به سختی می‌توان استدلال کرد که کامپوننت‌های آن در چه state قرار دارند و در مقایسه با جایگزین‌ها بیش از اندازه مستعد خطا هستند. همچنین ممکن است در یک state غیرممکن قرار بگیریم، مانند اینکه به طور تصادفی هر دوی isLoading و isFinished را در یک زمان روی true تنظیم کنیم.

یک روش بهتر برای مقابله با این مسئله مدیریت state با استفاده از "enum" است. در زبان‌های دیگر enum راهی برای تعریف متغیری می‌باشد که فقط مجاز است در یک مجموعه از پیش تعریف شده از مقادیر ثابت تنظیم شود. هرچند enumها از نظر فنی در جاوااسکریپت وجود ندارند، اما می‌توانیم از یک رشته به عنوان enum استفاده کنیم و همچنان مزایای زیادی به دست آوریم:

function Component() {
  const [state, setState] = useState('idle')

  const fetchSomething = () => {
    setState('loading')

    fetch(url)
      .then(() => {
        setState('finished')
      })
      .catch(() => {
        setState('error')
      })
  }

  if (state === 'loading') return <Loader />
  if (state === 'error') return <Error />
  if (state === 'finished') return <Success />

  return <button onClick={fetchSomething} />
}

با این کار امکان stateهای غیرممکن را از بین بردیم و استدلال در مورد این کامپوننت را بسیار آسان‌تر کردیم. سرانجام اگر از سیستم نوع مانند TypeScript استفاده می‌کنید، از آنجایی که می‌توانید stateهای ممکن را مشخص کنید بهتر است:

const [state, setState] = useState<'idle' | 'loading' | 'error' | 'finished'>('idle')

useState بسیار زیاد

از استفاده بیش از حد قلاب‌های useState در یک کامپوننت خودداری کنید.

یک کامپوننت با تعداد زیادی قلاب useState کارهای زیادی را انجام می‌دهد و احتمالا کاندید مناسبی برای تجزیه کامپوننت‌های چندگانه است. اما موارد پیچیده‌ای نیز وجود دارد که ما نیاز به مدیریت برخی از stateهای پیچیده در یک کامپوننت داریم.

در این مثال برخی از stateها و چند تابع در یک کامپوننت ورودی تکمیل خودکار آورده شده است:

function AutocompleteInput() {
  const [isOpen, setIsOpen] = useState(false)
  const [inputValue, setInputValue] = useState('')
  const [items, setItems] = useState([])
  const [selectedItem, setSelectedItem] = useState(null)
  const [activeIndex, setActiveIndex] = useState(-1)

  const reset = () => {
    setIsOpen(false)
    setInputValue('')
    setItems([])
    setSelectedItem(null)
    setActiveIndex(-1)
  }

  const selectItem = (item) => {
    setIsOpen(false)
    setInputValue(item.name)
    setSelectedItem(item)
  }

  ...
}

ما یک تابع reset داریم که تمام state را بازنشانی کرده و یک تابع selectItem که برخی از stateهای ما را به روز می‌کند. این توابع برای انجام وظیفه مورد نظر خود باید از تعداد زیادی تنظیم کننده state از همه useStates ما استفاده کنند. حال تصور کنید که اقدامات بیشتری برای به روزرسانی state انجام شده است و به راحتی می‌توان فهمید که در دراز مدت برای جلوگیری از مشکل این کار دشوار است. در این موارد مدیریت state با استفاده از قلاب useReducer می‌تواند مفید باشد:

const initialState = {
  isOpen: false,
  inputValue: "",
  items: [],
  selectedItem: null,
  activeIndex: -1
}
function reducer(state, action) {
  switch (action.type) {
    case "reset":
      return {
        ...initialState
      }
    case "selectItem":
      return {
        ...state,
        isOpen: false,
        inputValue: action.payload.name,
        selectedItem: action.payload
      }
    default:
      throw Error()
  }
}

function AutocompleteInput() {
  const [state, dispatch] = useReducer(reducer, initialState)

  const reset = () => {
    dispatch({ type: 'reset' })
  }

  const selectItem = (item) => {
    dispatch({ type: 'selectItem', payload: item })
  }

  ...
}

با استفاده از reducer مدیریت state خود را محصور کردیم و پیچیدگی را از کامپوننت خود خارج کردیم. این موضوع درک آنچه اکنون در جریان است را آسان‌تر می‌کند، زیرا ما می‌توانیم به طور جداگانه در مورد state و کامپوننت خود فکر کنیم.

هر دوی useState و useReducer همراه با مزایا و معایبشان و موارد استفاده متفاوت هستند.

useEffect طولانی

از استفاده از useEffect طولانی خودداری کنید. آنها کد شما را مستعد خطا کرده و باعث می‌شوند استدلال در آن دشوارتر شود.

اشتباهی که هنگام آزاد شدن قلاب‌ها بسیار مرتکب می‌شویم، قرار دادن موارد زیادی در یک useEffect ساده است. برای توضیح بیشتر در اینجا یک کامپوننت با یک useEffect واحد وجود دارد:

function Post({ id, unlisted }) {
  ...

  useEffect(() => {
    fetch(`/posts/${id}`).then(/* do something */)

    setVisibility(unlisted)
  }, [id, unlisted])

  ...
}

هرچند که این useEffect آنقدر طولانی نیست، اما چند کار را انجام می‌دهد. هنگامی که prop unlisted تغییر می‌کند، post را میگیریم حتی اگر id تغییر نکرده باشد.

تقسیم این effect به دو مورد، به جای خرد کردن آن:

function Post({ id, unlisted }) {
  ...

  useEffect(() => { // when id changes fetch the post
    fetch(`/posts/${id}`).then(/* ... */)
  }, [id])

  useEffect(() => { // when unlisted changes update visibility
    setVisibility(unlisted)
  }, [unlisted])

  ...
}

با این کار از پیچیدگی کامپوننت خود کم کرده، استدلال در مورد آن را آسان‌تر کرده و خطر ایجاد مشکل را کاهش داده‌ایم.

جمع بندی

به یاد داشته باشید که این موارد به هیچ وجه قانون نیستند، بلکه نکاتی هستند که ممکن است هرکسی سهوا آنها را به اشتباه انجام دهد . مطمئنا با شرایطی روبه رو خواهید شد که بخواهید برخی از کارهای بالا را به خوبی انجام دهید.

منبع

چه امتیازی برای این مقاله میدهید؟

خیلی بد
بد
متوسط
خوب
عالی
3.75 از 4 رای

/@erfanheshmati
عرفان حشمتی
Full-Stack Web Developer

کارشناس معماری سیستم های کامپیوتری، طراح و توسعه دهنده وب سایت، تولیدکننده محتوا

دیدگاه و پرسش

برای ارسال دیدگاه لازم است وارد شده یا ثبت‌نام کنید ورود یا ثبت‌نام

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید