در این مقاله قصد داریم تا با مفهوم سرور کامپوننت در React آشنا شویم و مزایا و معایب و همچنین نحوه استفاده از آن را بررسی کنیم.

چند سالی است که React هوک‌ها را منتشر کرده است و در طول این مدت، نظرات از بسیار عالی به نارضایتی از آن‌ها تغییر کرده است. توسعه‌دهندگان روزبه‌روز از هوک‌های React ناامید می‌شوند و این تقریباً به طور کامل به دلیل fetch کردن داده‌ها و به طور خاص‌تر به دلیل هوک useEffect است.

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

سرور کامپوننت چیست؟

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

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

function ClientComponent() {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [errr, setError] = useState(false)

  useEffect(() => {
    setLoading(true)
    setData(undefined)
    setError(false)

    const controller = new AbortController()

    fetch("/api/data", { signal: controller.signal })
      .then(res => res.json())
      .then(data => setData(data))
      .catch(error => setError(true))

    return () => controller.abort()
  }, [])

  if (loading) return <p>Loading...</p>
  if (error) return <p>Error</p>

  return <p>{data}</p>
}
async function ServerComponent() {
  try {
    const data = await fetch("/api/data").then(res => res.json())
  } catch (error) {
    return <p>Error</p>
  }

  return <p>{data}</p>
}

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

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

مزایای سرور کامپوننت‌ها

سرور کامپوننت‌ها ابزاری باورنکردنی هستند که fetch کردن داده‌ها را بسیار آسان‌تر می‌کنند. اما مزایای دیگری نیز دارند که در ادامه آن‌ها را بررسی می‌کنیم.

امنیت

از آنجایی که سرور کامپوننت‌ها به طور کامل بر روی سرور اجرا می‌شوند، می‌توانیم با خیال راحت از secret API keyها استفاده نماییم یا داده‌ها را مستقیماً از پایگاه داده fetch کنیم زیرا کلاینت هرگز این کد را نخواهد دید.

async function ServerComponent() {
  const user = await db.users.find({ name: "John" })

  return <p>{user.name}</p>
}

این موضوع ممکن است یک امتیاز جزئی به نظر برسد، اما در واقع یک مزیت بزرگ است. زیرا به این معنی می‌باشد که ما نیازی به ایجاد یک API عمومی جداگانه نداریم تا برنامه React خود را تقویت کنیم. ما فقط می‌توانیم تمام callهای پایگاه داده خود را مستقیماً از سرور کامپوننت‌های خود انجام دهیم.

کارایی

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

Caching

از آنجایی که تمام کدهای ما بر روی سرور اجرا می‌شود، به این معنی است که می‌توانیم داده‌ها را بین درخواست‌ها و بین کاربران ذخیره کنیم. برای مثال، اگر فهرستی از مقالات وبلاگ داریم که می‌دانیم اغلب تغییر نمی‌کند، می‌توانیم آن فهرست مقالات را در حافظه cache ذخیره کنیم و بدون نیاز به هر بار fetch کردن آن از پایگاه داده، مقالات را به هر کاربر ارائه دهیم.

async function ServerComponent() {
  // Cache for 1 minute across all requests
  const articles = await cache(db.articles.findAll, { timeSec: 60 })

  return <p>{articles.length}</p>
}

حجم بسته نرم افزاری

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

سرعت لود صفحه

از آنجایی که سرور کامپوننت‌ها بر روی سرور رندر می‌شوند، در نتیجه HTML آن کامپوننت‌ها به محض درخواست کاربر برای کلاینت ارسال می‌شود. این بدان معناست که کاربر محتوای صفحه را بسیار سریع‌تر از زمانی که از کلاینت کامپوننت‌ها استفاده می‌کردیم می‌بیند، زیرا کلاینت کامپوننت‌ها باید منتظر لود کدهای جاوااسکریپتی باشند تا بتوانند رندر شوند. این موضوع برای سئو بسیار مهم است زیرا گوگل به سرعت لود صفحه اهمیت زیادی می‌دهد.

سئو

سرور کامپوننت‌ها برای مبحث سئو نیز بسیار بهتر هستند، زیرا HTML برای آن کامپوننت‌ها بلافاصله و بدون نیاز به منتظر ماندن برای لود شدن کدهای جاوااسکریپت، برای کلاینت ارسال می‌شود. این موضوع به موتورهای جستجو کمک می‌کند تا صفحات ما را فهرست کرده و پیدا کردن آن‌ها در گوگل را آسان‌تر کند.

معایب سرور کامپوننت‌ها

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

عدم تعامل

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

این موضوع ممکن است مانند یک deal breaker به نظر برسد. اما از آنجایی که می‌توانیم به راحتی کلاینت کامپوننت معمولی را در سرور کامپوننت‌ها قرار دهیم، نمی‌تواند مشکل‌ساز باشد.

async function ServerComponent() {
  const articles = await db.articles.findAll()

  return articles.map(article => <Article article={article} />)
}
"use client"

// This is a normal client component so you can use hooks and event listeners
function Article() {
  const [likes, setLikes] = useState(0)

  return (
    <div>
      <p>{article.title}</p>
      <p>{article.content}</p>
      <button onClick={() => setLikes(l => l + 1)}>Like</button>
      <p>{likes}</p>
    </div>
  )
}

توجه به این نکته لازم است که دستور “use client”در قسمت بالای فایل کلاینت کامپوننت، یکی از ویژگی‌های Next.js می‌باشد و برای اینکه به Next.js بگویم که این فایل یک کلایت کامپوننت است مورد استفاده قرار می‌گیرد. فریمورک‌های دیگر ممکن است روش‌های مختلفی برای انجام این کار داشته باشند، اما مفهوم یکسان است.

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

No Browser APIs

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

مشکل سرور کامپوننت‌های تودرتو در کلاینت کامپوننت‌ها

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

function ServerComponent() {
  return <p>Hello Server</p>
}
"use client"

function ClientComponent() {
  return (
    <div>
      <p>Hello Client</p>
      {/* This will not work */}
      <ServerComponent />
    </div>
  )
}

البته ما می‌توانیم این مشکل را،حداقل در Next.js تا حدودی برطرف کنیم. به این صورت که سرور کامپوننت را به عنوان یک prop به کلاینت کامپوننت ارسال کرده و سپس آن در کلاینت کامپوننت رندر کنیم.

function ServerComponent() {
  return <p>Hello Server</p>
}
"use client"

function ClientComponent({ children }) {
  return (
    <div>
      <p>Hello Client</p>
      {children}
    </div>
  )
}
function AnotherServerComponent() {
  return (
    <ClientComponent>
      {/* This will work */}
      <ServerComponent />
    </ClientComponent>
  )
}

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

نحوه استفاده از سرور کامپوننت‌ها

سرور کامپوننت‌ها هنوز آزمایشی هستند بنابراین نمی‌توانند مستقیماً در داخل React مورد استفاده قرار بگیرند. اما باید از فریم‌ورک‌هایی استفاده کنیم که سرور کامپوننت‌ها را برای استفاده از آن‌ها پیاده سازی کند. در حال حاضر تنها فریم‌ورکی که سرور کامپوننت‌ها را پیاده‌سازی می‌کند Next.js است. اما بسیاری از فریم‌ورک‌های دیگر در حال آزمایش با پیاده‌سازی سرور کامپوننت‌ها هستند.

در Next.js به طور پیش‌فرض تمام کامپوننت‌هایی که ایجاد می‌کنیم سرور کامپوننت هستند. به منظور استفاده از یک کلاینت، باید عبارت "use Client"را در بالای فایل اضافه کنیم.

"use client"

// This is a client component
function ClientComponent() {
  return <p>Hello Client</p>
}

دلیل این رویکرد server first این است که استفاده از سرور کامپوننت‌ها تقریباً همیشه بهتر است مگر اینکه ما به طور خاص به APIهای تعاملی یا مرورگر نیاز داشته باشیم.

جمع‌بندی

استفاده از سرور کامپوننت روش جدیدی برای نوشتن برنامه‌های React هستند که ایجاد برنامه‌های پیچیده را آسان‌تر از همیشه می‌کند. زیرا بسیاری از پیچیدگی‌های مربوط به fetch کردن داده‌ها و عملکرد را حل می‌کند. اگرچه سرور کامپوننت‌ها هنوز آزمایشی هستند، اما فریمورک‌هایی مانند Next.js باعث می‌شوند تا به روشی پایدار قابل دسترسی باشند.