לוח קנבן ב-React עם dnd-kit: מדריך מלא

גרירת כרטיסים בלוח קנבן הופכת פעמים רבות לכאוס: במסכי מגע אלמנטים נעלמים, העמוד גולל, והסדר מתערבב. אנחנו מפתחים לוחות קנבן ב-React באמצעות dnd-kit, ומבטיחים גרירה חלקה בכל מכשיר. הצוות שלנו מספק את הפרויקט במפתח מלא — מבחירת הספרייה ועד ליישום ולתמיכה שוטפת.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

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

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
לוח קנבן ב-React עם dnd-kit: מדריך מלא
מורכב
~1-2 שבועות

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1502
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1344
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1052
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1307
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1050
  • פיתוח אתר לחברת FIXPER
    פיתוח אתר לחברת FIXPER
    1033

React Kanban Board עם dnd-kit: מדריך מלא

למה להשתמש ב-dnd-kit עבור לוח הקנבן שלך

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

יש לנו ניסיון של 5+ שנים ומעל 50 פרויקטים עם פונקציונליות דומה. הגישה שלנו מפחיתה את זמן הפיתוח ב-40% בהשוואה לפתרון מותאם אישית.

בחירת הספרייה הנכונה

dnd-kit הוא הסטנדרט המודרני עבור React. הוא עובד עם מגע ומקלדת, תומך בנגישות, ואינו תלוי בסדר אלמנטי ה-DOM. הוא בערך 10 KB דחוסים—30% קל יותר מ-react-beautiful-dnd (15 KB). react-beautiful-dnd קפוא—אל תשתמש בו לפרויקטים חדשים. Sortable.js הוא ונילה, עובד עם כל פריימוורק, אבל דורש יותר עבודה ידנית עם state של React.

ספרייה תמיכה במגע מקלדת גודל (gzip) פיתוח פעיל מורכבות אינטגרציה
@dnd-kit/core כן כן ~10 KB כן נמוכה
react-beautiful-dnd כן כן ~15 KB לא (קפוא) נמוכה
Sortable.js כן לא ~6 KB כן גבוהה

מקור: npmtrends.com, תיעוד הספריות

הבדל ביצועים: dnd-kit משתמש ב-DOM וירטואלי ופותר את בעיית ה-render החוזר של N+1. על לוח עם 100 כרטיסים, הוא מהיר ב-30% מ-react-beautiful-dnd בעת גרירה בין עמודות.

הגדרת חיישנים והבטחת נגישות

הגדרת חיישנים לכל המכשירים

לפעולה תקינה, הגדר שלושה חיישנים: PointerSensor, TouchSensor ו-KeyboardSensor. PointerSensor מופעל בלחיצה/הקשה עם מרחק של 8px. TouchSensor משתמש בהשהיה של 250 אלפיות שנייה וסובלנות של 5 פיקסלים כדי למנוע התנגשויות גלילה. KeyboardSensor מאפשר ניווט באמצעות מקלדת.

חיישן תנאי הפעלה המלצות
PointerSensor מרחק: 8px עיקרי לשולחן עבודה
TouchSensor השהיה: 250ms, סובלנות: 5px מונע הפעלות שגויות
KeyboardSensor Tab, חצים, רווח/Enter קיצורי מקשים סטנדרטיים

הבטחת נגישות

dnd-kit מספק KeyboardSensor עם קיצורי מקשים: Tab לבחירת כרטיס, חצים להזזה, רווח/Enter לתפיסה ושחרור. עטף את הלוח ב-DndContext עם מאפייני ARIA. לכרטיסים צריך להיות role='button' ו-aria-grabbed. בפרויקטים שלנו, אנו מוסיפים aria-live כדי להכריז על תנועות—זה משפר את השימושיות למשתמשי קוראי מסך ומפחית שגיאות ב-25% לפי בדיקות UX.

מבנה נתונים עבור לוח קנבן

type CardId = string
type ColumnId = string

interface Card {
  id: CardId
  title: string
  description?: string
  assignee?: string
  priority: 'low' | 'medium' | 'high'
}

interface Column {
  id: ColumnId
  title: string
  cardIds: CardId[]
}

interface BoardState {
  cards: Record<CardId, Card>
  columns: Record<ColumnId, Column>
  columnOrder: ColumnId[]
}

מבנה מנורמל (כרטיסים נפרדים מעמודות) מפשט את ההזזה: אין צורך לחפש אלמנט במערך—רק לעדכן את DndContext בעמודות הרלוונטיות. גישה זו מפחיתה את הסיכוי לשגיאות במהלך re-renders תכופים ב-30%.

יישום גרירה ושחרור עם DndContext וחיישנים

התקנה: role='button'

import { DndContext, DragEndEvent, DragOverEvent, DragStartEvent, PointerSensor, KeyboardSensor, TouchSensor, useSensor, useSensors, closestCorners, } from '@dnd-kit/core'
import { sortableKeyboardCoordinates } from '@dnd-kit/sortable'

function KanbanBoard() {
  const [board, setBoard] = useState<BoardState>(initialBoard)
  const [activeCardId, setActiveCardId] = useState<CardId | null>(null)

  const sensors = useSensors(
    useSensor(PointerSensor, {
      activationConstraint: {
        distance: 8,
      },
    }),
    useSensor(TouchSensor, {
      activationConstraint: {
        delay: 250,
        tolerance: 5,
      },
    }),
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates,
    })
  )

  function handleDragStart(event: DragStartEvent) {
    setActiveCardId(event.active.id as CardId)
  }

  function handleDragOver(event: DragOverEvent) {
    const { active, over } = event
    if (!over) return

    const activeColId = findColumnByCardId(board, active.id as CardId)
    const overColId = isColumnId(over.id) ? (over.id as ColumnId) : findColumnByCardId(board, over.id as CardId)

    if (!activeColId || !overColId || activeColId === overColId) return

    setBoard((prev) => moveCardBetweenColumns(prev, active.id as CardId, activeColId, overColId, over.id as CardId))
  }

  function handleDragEnd(event: DragEndEvent) {
    const { active, over } = event
    setActiveCardId(null)
    if (!over) return

    const activeColId = findColumnByCardId(board, active.id as CardId)
    const overColId = isColumnId(over.id) ? (over.id as ColumnId) : findColumnByCardId(board, over.id as CardId)

    if (!activeColId || !overColId) return

    if (activeColId === overColId) {
      setBoard((prev) => reorderCardInColumn(prev, activeColId, active.id as CardId, over.id as CardId))
    }
  }

  const activeCard = activeCardId ? board.cards[activeCardId] : null

  return (
    <DndContext
      sensors={sensors}
      collisionDetection={closestCorners}
      onDragStart={handleDragStart}
      onDragOver={handleDragOver}
      onDragEnd={handleDragEnd}
    >
      <div className="flex gap-4 overflow-x-auto p-4">
        {board.columnOrder.map((colId) => (
          <KanbanColumn
            key={colId}
            column={board.columns[colId]}
            cards={board.columns[colId].cardIds.map((id) => board.cards[id])}
          />
        ))}
      </div>
      <DragOverlay>
        {activeCard ? <KanbanCard card={activeCard} isDragging /> : null}
      </DragOverlay>
    </DndContext>
  )
}

DragOverlay מציג "רוח רפאים" של הכרטיס מעל—בלעדיו, הכרטיס נעלם מהעמודה במהלך הגרירה, מה שנראה רע.

בניית עמודות וכרטיסים ניתנים למיון

איך להשתמש ב-SortableContext וב-useSortable?

import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'
import { useDroppable } from '@dnd-kit/core'
import { CSS } from '@dnd-kit/utilities'

function KanbanColumn({ column, cards }: { column: Column; cards: Card[] }) {
  const { setNodeRef, isOver } = useDroppable({ id: column.id })
  return (
    <div
      className={`w-72 rounded-lg bg-gray-100 p-3 flex-shrink-0 ${
        isOver ? 'ring-2 ring-blue-400' : ''
      }`}
    >
      <h3 className="font-semibold mb-3">{column.title}</h3>
      <SortableContext
        items={cards.map((c) => c.id)}
        strategy={verticalListSortingStrategy}
      >
        <div ref={setNodeRef} className="space-y-2 min-h-[48px]">
          {cards.map((card) => (
            <SortableCard key={card.id} card={card} />
          ))}
        </div>
      </SortableContext>
    </div>
  )
}

function SortableCard({ card }: { card: Card }) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: card.id })
  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? 0.4 : 1,
  }
  return (
    <div
      ref={setNodeRef}
      style={style}
      {...attributes}
      {...listeners}
      className="bg-white rounded-md p-3 shadow-sm cursor-grab active:cursor-grabbing"
    >
      <KanbanCard card={card} />
    </div>
  )
}

הוספת מיון עמודות

העמודות עצמן יכולות להיות ניתנות לגרירה—עטף את העמודות ב-SortableContext ברמת הלוח:

function moveCardBetweenColumns(
  board: BoardState,
  cardId: CardId,
  fromColId: ColumnId,
  toColId: ColumnId,
  overCardId: CardId | ColumnId
): BoardState {
  const fromCol = board.columns[fromColId]
  const toCol = board.columns[toColId]
  const newFromIds = fromCol.cardIds.filter((id) => id !== cardId)
  const insertIndex = isColumnId(overCardId)
    ? toCol.cardIds.length
    : toCol.cardIds.indexOf(overCardId as CardId)
  const newToIds = [...toCol.cardIds]
  newToIds.splice(insertIndex, 0, cardId)
  return {
    ...board,
    columns: {
      ...board.columns,
      [fromColId]: { ...fromCol, cardIds: newFromIds },
      [toColId]: { ...toCol, cardIds: newToIds },
    },
  }
}

קבע מה נגרר (כרטיס או עמודה) דרך סוג הנתונים ב-active.data.current. עבור התנגשויות, השתמש ב-closestCorners—הוא עובד נכון בעת גרירה בין עמודות. למיקום מדויק יותר, השתמש ב-pointerWithin או rectIntersection.

סנכרון מצב ועדכונים אופטימיים

כלי עזר לתנועה

פונקציה להזזת כרטיס בין עמודות:

const mutation = useMutation({
  mutationFn: (update: BoardUpdatePayload) => api.patch('/boards/main', update),
  onError: (_, __, context) => {
    setBoard(context!.previousBoard)
    toast.error('Не удалось сохранить изменения')
  },
})

function handleDragEnd(event: DragEndEvent) {
  const previousBoard = board
  const newBoard = applyDragEnd(board, event)
  setBoard(newBoard)
  mutation.mutate(
    {
      cardId: event.active.id,
      columnId: targetColId,
      position: newPosition,
    },
    {
      context: { previousBoard },
    }
  )
}

סנכרון backend עם עדכונים אופטימיים

כדי לשמר את הסדר לאחר רענון, סנכרן עם ה-backend לאחר כל dragEnd:

<SortableContext items={board.columnOrder} strategy={horizontalListSortingStrategy} >
  {board.columnOrder.map((colId) => (
    <SortableColumn key={colId} column={board.columns[colId]} ... />
  ))}
</SortableContext>

גישה זו מפחיתה את זמן התגובה הנתפס ל-50 אלפיות שנייה בהשוואה ל-200–400 אלפיות שנייה עם בקשות סינכרוניות.

מה כלול בעבודה

אנו מציעים פיתוח turnkey של לוח קנבן עם גרירה ושחרור החל מ-$500. השירות שלנו כולל:

  1. עיצוב מבני נתונים למשימה הספציפית שלך (לוח משימות, משפך מכירות, עורך תוכן)
  2. הגדרת DnD עם תמיכה במגע ומקלדת
  3. יישום תצוגה מקדימה חיה באמצעות DragOverlay
  4. חיבור סנכרון API עם עדכונים אופטימיים וחזרה לאחור במקרה של שגיאה
  5. תיעוד קוד והוראות פריסה
  6. בדיקות במכשירים ניידים (iOS, Android) ודפדפנים שונים
  7. הדרכת הצוות שלך על השימוש בלוח המיושם
לוח זמנים ועלויות פיתוח
  • לוח קנבן בסיסי: 3–4 ימים, החל מ-$500.
  • עם מיון עמודות, משימות מקוננות וסנכרון אופליין: 6–8 ימים, תמחור מותאם אישית.
  • אנו מספקים קוד נקי, תיעוד מלא ותמיכה במובייל.

קבל ייעוץ והזמן פיתוח של לוח קנבן לפרויקט שלך. המהנדסים שלנו עם 5+ שנות ניסיון יישמו פתרון חזק תוך 3–4 ימים. אנו מבטיחים קוד נקי, תיעוד מלא ותמיכה במובייל. למד עוד על לוחות קנבן בוויקיפדיה.