Coding

Costruire un configuratore complesso con DnD-kit

Costruire un configuratore per una workstation robotica da banco per laboratori clinici ha significato affrontare sfide complesse di drag-and-drop. DnD-kit è stata la nostra arma di scelta: dal setup di base al mix di pattern diversi in un'unica interfaccia.

Pedro Di Martino Pedro Di Martino
12 marzo 2026 — 10 min

Di recente abbiamo costruito un configuratore web-based per un sistema di workstation robotica da banco, del tipo che si trova nei laboratori clinici. Gli utenti dovevano trascinare moduli rack dentro i tray, vedere in anteprima come si incastravano, riordinarli e gestire più profili di configurazione. Facile, no? Non proprio.

La sfida non era solo rendere gli oggetti trascinabili. Dovevamo gestire i vincoli degli slot (i rack hanno dimensioni diverse e devono starci nello spazio disponibile), la compatibilità delle fence (rack adiacenti devono avere connettori combacianti), la validazione in real-time (gli utenti non dovrebbero poter rilasciare un rack dove non ci sta) e pattern di interazione misti (alcune aree permettono il riordino, altre solo lo scambio). Il tutto in un’applicazione React che doveva risultare responsiva e intuitiva.

Abbiamo scelto DnD-kit per questo lavoro e, pur essendosi rivelata un’ottima scelta, il percorso ha richiesto alcune scoperte interessanti e qualche lezione imparata a caro prezzo. Ecco cosa abbiamo imparato costruendo questo configuratore.

Nota: questo articolo copre @dnd-kit/core (v6.x) e @dnd-kit/sortable, che sono ora considerati la versione legacy di DnD-kit. Un nuovo rewrite framework-agnostic è in sviluppo attivo — con adapter per React, Vue, Svelte e Solid — ma non ha ancora raggiunto una release 1.0 stabile (attualmente v0.3.x). I pattern descritti qui restano l’approccio standard per l’uso in produzione.

Primo tentativo: drag and drop di base

Abbiamo iniziato con @dnd-kit/core, il pacchetto fondamentale che fornisce i building block per le interazioni drag-and-drop. L’approccio iniziale era diretto: usare useDraggable per gli item rack nel drawer delle location e useDroppable per gli slot nei tray in cui si potevano piazzare i rack.

Abbiamo creato un componente DroppableSlot tra ogni lane del tray, trasformando di fatto ogni spazio vuoto in un drop target. Quando trascinavi un rack dal drawer, lo rilasciavi in uno di questi slot e lo inserivamo in quella posizione. Il codice assomigliava a questo:

// Simplified version of our first approach
function Tray({ lanes, onAddLane }) {
  return (
    <div className="tray-grid">
      {lanes.map((lane, index) => (
        <>
          <DroppableSlot 
            index={index} 
            onDrop={(item) => onAddLane(item, index)} 
          />
          <Lane lane={lane} />
        </>
      ))}
      <DroppableSlot 
        index={lanes.length} 
        onDrop={(item) => onAddLane(item, lanes.length)} 
      />
    </div>
  );
}

function DroppableSlot({ index, onDrop }) {
  const { isOver, setNodeRef } = useDroppable({
    id: `slot-${index}`,
    data: { index }
  });
  
  return <div ref={setNodeRef} className={isOver ? 'highlight' : ''} />;
}

Funzionava, ma era goffo. Ogni volta che volevamo riordinare un rack esistente, dovevamo calcolare manualmente gli indici, gestire la rimozione dalla vecchia posizione e inserirlo in quella nuova. Non c’erano animazioni fluide quando gli item si spostavano, e il codice stava diventando verboso con tutta la gestione manuale degli slot.

Scoperta del preset Sortable

Dopo l’implementazione iniziale, sapevamo che doveva esserci un modo migliore. Ci siamo tuffati negli esempi di DnD-kit per vedere come altri gestivano problemi simili. Un esempio in particolare è stato illuminante: usava @dnd-kit/sortable, un preset progettato specificamente per le liste ordinabili.

Studiando gli esempi, abbiamo capito che questo preset astrae gran parte della complessità che stavamo gestendo a mano. Invece di gestire gli slot di drop e calcolare gli indici da noi, potevamo wrappare le nostre lane in un SortableContext e usare l’hook useSortable. La libreria gestisce le animazioni, i calcoli degli indici e fornisce persino utility per strategie di ordinamento diverse.

Ecco come abbiamo rifatto il componente tray:

import { SortableContext, horizontalListSortingStrategy } from '@dnd-kit/sortable';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';

function Tray({ lanes, onMoveLane }) {
  return (
    <SortableContext 
      items={lanes.map(lane => lane.id)} 
      strategy={horizontalListSortingStrategy}
    >
      <div className="tray-grid">
        {lanes.map(lane => (
          <SortableRack key={lane.id} lane={lane} />
        ))}
      </div>
    </SortableContext>
  );
}

function SortableRack({ lane }) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging
  } = useSortable({ 
    id: lane.id,
    data: { lane }
  });

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? 0.5 : 1
  };

  return (
    <div ref={setNodeRef} style={style} {...attributes} {...listeners}>
      <Rack lane={lane} />
    </div>
  );
}

La differenza era notte e giorno. I rack ora si animavano fluidamente durante il riordino, il codice era significativamente più pulito e abbiamo ottenuto l’accessibilità da tastiera gratis grazie a sortableKeyboardCoordinates. Il preset sortable ha trasformato quelle che erano dozzine di righe di manipolazione degli indici in un’interfaccia pulita e dichiarativa.

Preview della configurazione in real-time

Con il sortable funzionante, abbiamo affrontato la sfida successiva: il feedback visivo durante le operazioni di drag. Gli utenti dovevano sapere se un rack sarebbe entrato in un tray prima di rilasciarlo. Il nostro sistema di rack ha vincoli fisici — ogni rack occupa un certo numero di slot, e i tray hanno capacità limitata. Rilasciare un rack da 10 slot in un tray con solo 5 slot disponibili doveva essere impedito e, idealmente, l’utente doveva vederlo prima di tentare il drop.

Anteprima della configurazione in real-time con la validazione degli slot

All’inizio validavamo solo su onDragEnd. L’utente trascinava un rack, lo rilasciava, e poi vedeva un errore se non ci stava. Sembrava rotto: perché permettergli di tentare un drop invalido in primo luogo?

La soluzione è stata ricalcolare la configurazione in real-time usando l’evento onDragOver. Mentre l’utente trascina sopra un tray, calcoliamo come sarebbe la configurazione se rilasciasse adesso, e aggiorniamo la UI di conseguenza. Gli slot vuoti si illuminano per mostrare dove andrebbe il rack e, se il rack non ci sta, quegli slot diventano rossi.

Ecco la logica centrale:

function ConfiguratorRoute() {
  const [configuration, setConfiguration] = useState(initialConfig);
  const [tempConfiguration, setTempConfiguration] = useState(null);

  function onDragOver(event) {
    const { active, over } = event;
    
    if (!over) {
      setTempConfiguration(null);
      return;
    }

    // Compute what the configuration would look like
    const source = active.data.current;
    const target = over.data.current;
    
    const preview = computeConfigurationPreview(
      configuration, 
      source, 
      target
    );
    
    // Check if the rack fits
    const { totalSlots, availableSlots } = getTrayCapacity(preview);
    const rackSlots = source.rack.number_of_slots;
    const fenceSlots = computeFenceSlots(preview);
    
    if (rackSlots + fenceSlots <= availableSlots) {
      setTempConfiguration(preview);
    } else {
      setTempConfiguration({ ...preview, overflow: true });
    }
  }

  function onDragEnd(event) {
    if (tempConfiguration && !tempConfiguration.overflow) {
      setConfiguration(tempConfiguration);
    }
    setTempConfiguration(null);
  }

  const displayConfig = tempConfiguration || configuration;

  return (
    <DndContext onDragOver={onDragOver} onDragEnd={onDragEnd}>
      <DeviceList devices={displayConfig.devices} />
    </DndContext>
  );
}

Il trucco è stato mantenere due stati: la configuration committata e la tempConfiguration temporanea che si aggiorna a ogni spostamento del drag. In fase di render, usiamo la configurazione temporanea se esiste, dando all’utente un’anteprima istantanea. Al drop, committiamo la configurazione temporanea oppure la scartiamo se non è valida.

Questo approccio ha avuto implicazioni di performance. Ricalcolare fence e posizioni degli slot a ogni evento di drag non è gratis, specialmente con configurazioni complesse. Abbiamo ottimizzato usando gli attributi CSS style direttamente, invece della prop sx di MUI, per transform e transition, cosa che ha ridotto significativamente i re-render. Abbiamo anche memoizzato la logica di calcolo delle fence ed evitato clonazioni non necessarie dell’oggetto configurazione.

Mescolare i pattern di drag

Proprio quando pensavamo di aver finito, i requisiti sono evoluti. Gli utenti dovevano poter creare più profili di configurazione basati su un layout di default. I tray del layout di default dovevano essere sortable (gli utenti possono riordinare liberamente i rack), ma i tray dei profili dovevano permettere solo lo swap — puoi sostituire un rack con uno diverso dal drawer, ma non puoi riordinare o rimuovere rack perché romperebbe la struttura del layout.

Significava che avevamo bisogno di entrambi i pattern, sortable e droppable, nella stessa interfaccia, e dovevano coesistere senza interferire tra loro.

La nostra soluzione è stata rendere il componente lane adattivo in base al contesto:

function Lane({ lane, isSortable = true }) {
  if (isSortable) {
    return <SortableRack lane={lane} />;
  }
  return <DroppableRack lane={lane} />;
}

function DroppableRack({ lane }) {
  const { isOver, setNodeRef } = useDroppable({ 
    id: `lane-${lane.id}`,
    data: { type: 'lane', lane }
  });
  
  // Show preview of what rack would be here
  return (
    <div ref={setNodeRef}>
      {isOver ? <RackPreview /> : <Rack lane={lane} />}
    </div>
  );
}

La sfida era che le lane droppable dovevano sapere cosa stesse venendo trascinato per mostrare un’anteprima adeguata. Il context di DnD-kit non espone questa informazione direttamente a componenti arbitrari nell’albero. Abbiamo risolto creando un DraggingContext custom:

const DraggingContext = createContext(undefined);

function useDraggingContext() {
  return useContext(DraggingContext);
}

function ConfiguratorRoute() {
  const [dragSource, setDragSource] = useState(undefined);

  function onDragStart(event) {
    setDragSource(event.active.data.current);
  }

  function onDragEnd() {
    setDragSource(undefined);
  }

  return (
    <DndContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
      <DraggingContext.Provider value={dragSource}>
        <DeviceList />
      </DraggingContext.Provider>
    </DndContext>
  );
}

Ora le lane droppable potevano accedere a useDraggingContext() per vedere quale rack veniva trascinato e mostrare un’anteprima accurata, inclusa l’evidenziazione degli slot in rosso quando il rack non ci sarebbe entrato per via di un numero di slot diverso o fence incompatibili.

Abbiamo anche aggiunto una differenziazione visiva: le lane sortable mostrano fence solide, mentre le lane droppable mostrano bordi tratteggiati, rendendo chiaro quali aree permettono il riordino rispetto a quelle che permettono lo swap.

Cosa abbiamo imparato

Dopo diverse iterazioni, ci siamo trovati a desiderare di aver saputo alcune cose fin dall’inizio. Il risparmio di tempo più grande sarebbe stato partire subito dal preset sortable. Abbiamo costruito la gestione manuale degli slot con core, cose che sortable gestisce in automatico — una settimana di lavoro diventata inutile una volta scoperto il preset. Anche se il tuo caso d’uso ti sembra “troppo custom”, probabilmente sortable lo supporta.

La performance è diventata un problema man mano che le configurazioni crescevano in complessità. Le operazioni di drag devono sembrare istantanee, il che significa essere intenzionali su dove avvengono i calcoli. Passare da sx a style inline per le proprietà animate, memorizzare i calcoli ed evitare clonazioni superflue di oggetti hanno fatto la differenza tra lento e reattivo.

Il sistema di preview in real-time ha richiesto uno sforzo importante da implementare, ma vedere gli utenti usarlo ha validato ogni ora spesa. C’è qualcosa di profondamente soddisfacente nel vedere feedback immediato — slot vuoti che si illuminano per mostrare dove atterrerà il tuo rack, o che diventano rossi quando non ci sta. L’approccio con onDragOver aggiunge complessità, ma il miglioramento di UX è innegabile.

Abbiamo anche imparato ad appoggiarci molto al sistema di dati di DnD-kit. La possibilità di attaccare dati arbitrari agli elementi draggable e droppable tramite la prop data è diventata centrale per la nostra architettura. Abbiamo passato così configurazioni delle lane, riferimenti ai device e metadati di validazione. Usare discriminated union di TypeScript per strutturare questi dati si è rivelato essenziale per catturare bug prima che arrivassero in produzione.

Conclusione

Costruire questo configuratore ci ha insegnato che DnD-kit è impressionantemente flessibile, ma richiede di capirne i diversi layer e pattern. Partire da @dnd-kit/core ci ha dato alta capacità di controllo, ma scoprire @dnd-kit/sortable ci ha mostrato quanto la libreria sappia fare quando usi il giusto livello di astrazione.

L’insight chiave è che DnD-kit fornisce building block, non una soluzione completa. Dovrai progettare la tua gestione dello stato, la logica di validazione e il sistema di preview. Ma questi building block sono solidi, ben progettati e componibili in modi che ti permettono di gestire requisiti complessi come mescolare pattern sortable e droppable.

Il configuratore è ora in produzione, aiuta i tecnici di laboratorio a pianificare e validare le configurazioni dei rack prima di ordinare l’hardware fisico — esattamente il tipo di problema in cui azzeccare il drag-and-drop fa tutta la differenza. Se ti interessa affrontare sfide di questo tipo, che tu stia costruendo qualcosa di simile o voglia lavorare su progetti come questo, ci farebbe piacere sentirti.

Risorse

Tap by Designer Zepeto from Noun Project (CC BY 3.0)

Pedro Di Martino
Pedro Di Martino Software Engineer

Pedro is a full-stack Software Engineer at Buildo, with a strong inclination toward frontend development and complex UI challenges. He combines technical precision with a curiosity for new tools and approaches, always looking for ways to raise the bar on code quality and developer experience.

Vuoi saperne di più?
NEWSLETTER

Ehi, c'è anche una Newslettero!
Sì, proprio con la o.

Ogni mese scegliamo articoli utili ma anche idee fresche e stimolanti. Proprio quello che vorremmo trovare noi in una newsletter!