• בלוג
  • מדריך Next.JS חלק 6 - שילוב קומפוננטות צד-לקוח וצד-שרת

מדריך Next.JS חלק 6 - שילוב קומפוננטות צד-לקוח וצד-שרת

15/12/2023

בואו נחזור לתפריט העליון שכתבנו באפליקציה. אם עקבתם אחרי המדריך והתרגילים צריכות להיות לכם שתי גירסאות לקומפוננטה זו - גירסת קומפוננטת צד-שרת של התפריט העליון, שמחפשת בצורה דינמית את כל התיקיות שמייצגות "דפים" ומציגה תפריט שמיוצר אוטומטית מהדפים הקיימים, וגירסת קומפוננטת צד-לקוח שמשתמשת ברשימה קבועה של דפים אבל כוללת עיגול קטן ליד הדף הפעיל.

בחלק זה נראה איך לשלב את שתי הקומפוננטות כדי ליהנות מהטוב משני העולמות - גם לקבל תפריט שנוצר דינמית מתוך התיקיות בשרת, וגם לקבל סימון לנתיב הפעיל בעזרת JavaScript בצד לקוח.

1. מה בעצם הבעיה

בואו ניזכר בשתי קומפוננטות התפריט ובבעיות של כל אחת מהן. התפריט בקומפוננטת צד לקוח נראה כך:

"use client";
import Link from 'next/link';
import { usePathname } from 'next/navigation'

export default () => {
  const pathname = usePathname()
  return (
    <nav className='flex my-4 border-4 border-indigo-200 border-l-indigo-500'>
      <div className='flex-1 px-2'>
        <Link href="/" >Home</Link>
        {pathname == "/" &&
        <span className="ml-2 w-2 h-2 bg-blue-500 rounded-full inline-block"></span>
        }

      </div>
      <div className='flex-1 px-2'>
        <Link href="/about" >About</Link>
        {pathname == "/about" &&
        <span className="ml-2 w-2 h-2 bg-blue-500 rounded-full inline-block"></span>
        }
      </div>
    </nav>
  )
}

בגלל שזו קומפוננטת צד לקוח אני לא יכול להשתמש בפונקציות של מערכת הקבצים של השרת כמו readdir כדי לקבל את רשימת כל התיקיות על השרת, ולכן אני משתמש ברשימה קבועה.

בצד השרת שלחתי אתכם באחד התרגילים לכתוב קומפוננטת צד-שרת שמוצאת את כל הדפים בצורה דינמית ויוצרת מהם תפריט, ואולי כתבתם קומפוננטה שנראית כך:

import { readdir } from 'node:fs/promises';
import Link from 'next/link';
import { existsSync } from 'node:fs';

export default async () => {  
  const pages = [
    {href: '/', text: 'home'},
    ...(await readdir('./src/app', { withFileTypes: true }))
    .filter(f => f.isDirectory)
    .filter(f => existsSync(`${f.path}/${f.name}/page.tsx`))
    .map(f => (
      {href: '/' + f.name, text: f.name}
    ))
  ]

  return (
    <nav className='flex my-4 border-4 border-indigo-200 border-l-indigo-500'>
      {pages.map(page => (
        <div className='flex-1 px-2'>
          <Link href={page.href} >{page.text}</Link>
        </div>))}
    </nav>
  )
}

בקומפוננטה זו אני לא יכול להשתמש ב usePathname() מאחר והגישה ל pathName מוגבלת לקומפוננטות צד-לקוח בלבד.

2. הפיתרון - שילוב כוחות

המבנה של קומפוננטת צד שרת שיודעת לגשת למידע וקומפוננטת צד לקוח שיודעת להציג את המידע בצורה אינטרקטיבית הוא בעצם תבנית די נפוצה באפליקציות ווב ובאפליקציות next.js במיוחד. הפיתרון לאתגר הזה הוא פשוט לחלק את העבודה לשתי קומפוננטות, כך שקומפוננטת צד שרת מרנדרת קומפוננטת צד לקוח ומעבירה לה את המידע שהיא צריכה בתור props.

בקובץ אחד בשם menu.tsx אני כותב את התוכן הבא לקומפוננטת צד השרת:

import Menu from './components/menu';
import { readdir } from 'node:fs/promises';
import { existsSync } from 'node:fs';

export default async () => {  
  const pages = (await readdir('./src/app', { withFileTypes: true }))
  .filter(f => f.isDirectory)
  .filter(f => existsSync(`${f.path}/${f.name}/page.tsx`))
  .map(f => (
    {href: '/' + f.name, text: f.name}
  ))

  return <Menu items={[
    {href: "/", text: "home"},
    ...pages
  ]} />
}

ובקובץ אחר בשם components/menu.tsx אני כותב את התוכן הבא:

"use client";
import Link from 'next/link';
import { usePathname } from 'next/navigation'


export default ({ items }: {
  items: Array<{href: String, text: String}>
}) => {
  const pathname = usePathname()
  return (
    <nav className='flex my-4 border-4 border-indigo-200 border-l-indigo-500'>
      {items.map(item => (
      <div className='flex-1 px-2'>
      <Link href={item.href} >{item.text}</Link>
      {pathname == item.href &&
      <span className="ml-2 w-2 h-2 bg-blue-500 rounded-full inline-block"></span>
      }
    </div>
      ))}
    </nav>
  )
}

עכשיו קומפוננטת צד הלקוח עושה שני דברים: היא גם מקבלת בצורה דינמית את רשימת הדפים מהשרת, וגם משתמשת ב usePathname כדי להציג את העיגול ליד הפריט הפעיל בתפריט. ובאופן כללי נרצה לבנות יישומי Next.js במבנה של שיתוף פעולה בין קומפוננטות צד שרת לקומפוננטות צד לקוח. קומפוננטת צד שרת תמשוך את המידע, למשל דרך פנייה לבסיס נתונים, ל API מרוחק או למערכת קבצים, תעביר את המידע לקומפוננטת צד לקוח דרך props וקומפוננטת צד לקוח תוכל להציג את המידע בצורה אינטרקטיבית תוך שימוש בסטייט. כשאותה קומפוננטת צד לקוח תצטרך לעדכן חזרה את השרת נשתמש במנגנון שראינו בפרק הקודם של המדריך וכך העדכון יגרום לריענון של קומפוננטות צד השרת שבעמוד ובמידה והמידע ישתנה גם קומפוננטות צד הלקוח יקבלו פרופס חדשים ויציגו את התוכן המעודכן על המסך.