Skip to main content

DNDProvider


id: dndprovider title: DNDProvider sidebar_label: DNDProvider

DNDProvider es el componente raíz del sistema de Drag & Drop.
Crea un store de Redux aislado, configura el slice de DnD y expone:

  • El contexto para todos los Draggable y Droppable que vivan dentro.
  • Los hooks auxiliares useDndDispatch, useDndSelector y useDndActions.

Todo el sistema se tipa con un genérico T, que representa el tipo de dato que viajan los draggables.


API

Firma

export interface DNDProviderProps<T> {
workSpaceDimensions: {
width: number;
height: number;
};
children: ReactNode;

/** Modo de dropeo global */
mode?:
| "disappear"
| "remplacement-disappear"
| "remplacement-multi"
| "multi"
| "multi-disappear";

/**
* Callback global que se dispara cuando termina un ciclo de drop.
* Recibe:
* - data: último valor no-nulo por droppable
* - history: historial completo por droppable
*/
onDrop?: (data: Record<string, T>, history: Record<string, T[]>) => void;

/**
* (Reservado) Si true, considera las áreas droppable como círculos/óvalos.
* Actualmente **no tiene efecto en la implementación**.
*/
areaDroppableCircle?: boolean;

/**
* (Reservado) Si true, considera las áreas draggable como círculos/óvalos.
* Actualmente **no tiene efecto en la implementación**.
*/
areaDraggingCircle?: boolean;

/**
* Si es true, la selección del mejor droppable se hace usando centroides
* (distancia entre centros), no solo solapamiento de rectángulos.
*/
calcCentroids?: boolean;
}

export const DNDProvider = <T,>(props: DNDProviderProps<T>) => JSX.Element;

Props

workSpaceDimensions

workSpaceDimensions: { width: number; height: number };

Dimensiones lógicas del “workspace” donde se usa el DnD (por ejemplo, el área útil de la escena).

  • Actualmente el DNDProvider no usa este valor internamente, pero:

    • El tipo lo pide (para mantener coherencia con otros componentes como PadDND / PadDroppable).
    • Es buena práctica calcularlo una vez en la escena y reutilizarlo.

Recomendación: pasá las mismas dimensiones que usás para layout de pads/grids, aunque hoy el provider no las consume.


children

children: ReactNode;

El subárbol de React que tendrá acceso al contexto de DnD:

  • Todos tus Draggable, Droppable, MultipleDraggables, PadDND, etc. deben vivir dentro de este provider.
  • Internamente se renderizan envueltos por un <Provider store={store}> de react-redux.

mode

mode?:
| "disappear"
| "remplacement-disappear"
| "remplacement-multi"
| "multi"
| "multi-disappear";

Controla el comportamiento global del sistema; se pasa al slice como opción y se guarda en state.dragDrop.mode.

En la implementación actual:

  • El valor por defecto es "remplacement-multi".

  • El core del slice distingue explícitamente el modo "disappear":

    • Al reasignar un droppable a otro draggable, solo limpia vínculos previos cuando mode !== "disappear".
  • El componente Droppable usa mode para la lógica de historial:

    • En "multi" y "multi-disappear" se permite acumular múltiples entradas en history aunque el valor sea el mismo.
    • En los demás modos, si el nuevo drop trae el mismo dato (lastDrop.data === droppedData), no se agrega una entrada extra al historial.

En otras palabras:

  • "multi" / "multi-disappear" → historial tipo “log”, incluso con repetidos.
  • "remplacement-*" / "disappear" → historial más de “reemplazo”; no duplica entradas iguales.

Varios nombres de modo están pensados para variantes futuras, pero hoy las diferencias prácticas se concentran en:

  • el tratamiento especial de "disappear" en el slice, y
  • el manejo de history en Droppable.

onDrop

onDrop?: (
data: Record<string, T>,
history: Record<string, T[]>
) => void;

Callback global que se dispara desde el listener del slice después de un drop final. Se invoca con:

  • data: mapa droppableId → último valor no nulo dejado en ese droppable.
  • history: mapa droppableId → arreglo de todos los valores (T) que han caído ahí, en orden.

Detalles:

  • El listener filtra dataDroppable para quitar null antes de llamarte.

  • El callback se dispara después de actualizar el estado del slice, usando la versión más reciente de legacyHistory.

  • Es útil para:

    • Evaluar respuestas de toda la escena de una sola vez.
    • Sincronizar el resultado de la actividad con algún estado externo (por ejemplo, Redux global, backend, etc.).

areaDroppableCircle / areaDraggingCircle

areaDroppableCircle?: boolean;
areaDraggingCircle?: boolean;

Flags pensadas para soportar colisiones circulares/ovaladas.

  • Actualmente no se usan en el cálculo de colisión; los rectángulos (Rect) siguen siendo el modelo de colisión.
  • Se dejaron en el tipo como extensiones futuras; por ahora podés ignorarlas (o dejarlas en false).

calcCentroids

calcCentroids?: boolean;

Controla cómo se elige el “mejor” droppable al mover el draggable:

  • Si false (default):

    • Se usa solapamiento de rectángulos (Rect) y un umbral de ≥ 51% del área del draggable para elegir droppable.
  • Si true:

    • Se calcula el centro del draggable y el de cada droppable.
    • Se busca el droppable cuya distancia al centro del draggable sea mínima, siempre que haya “colisión” entre sus cajas.
    • Esto se usa dentro de getBestDroppable del slice.

Independientemente de calcCentroids, el slice calcula una métrica de distancia mínima a los droppables (CalcDistance) y la guarda en state.dragDrop.distance, que luego se expone a los Draggable como distance para animaciones.


Hooks expuestos

Además del componente provider, se exportan tres hooks para consumir el estado/acciones del DnD:

useDndDispatch

export const useDndDispatch = <T,>() =>
useReduxDispatch<DragDropDispatch<T>>();
  • Devuelve el dispatch tipado del store interno de DnD.
  • Usalo solo si querés disparar acciones del slice directamente (por ejemplo, desde un botón custom de “limpiar”).

Ejemplo:

const dispatch = useDndDispatch<MyData>();
const actions = useDndActions<MyData>();

const handleClean = () => {
dispatch(actions.cleanAll());
};

useDndSelector

export const useDndSelector: TypedUseSelectorHook<DragDropRootState<any>> =
useReduxSelector;

Selector tipado sobre el store de DnD:

const draggingData = useDndSelector((s) => s.dragDrop.dragData);
const distance = useDndSelector((s) => s.dragDrop.distance);
  • Ideal si necesitás leer información global del sistema (quién se está arrastrando, distancia, etc.) para mostrar UI adicional.

useDndActions

export const useDndActions = <T,>() =>
useContext(DndActionsContext) as DragDropActions<T>;

Devuelve los creadores de acciones generados por el slice (registerDroppable, moveDrag, cleanAll, resetCleanData, etc.).

Normalmente no los usás directamente porque:

  • Draggable y Droppable ya manejan registro, movimiento, history, etc.
  • Componentes de alto nivel (MultipleDraggables, CleanButton, etc.) encapsulan la mayoría de casos.

Pero están disponibles por si necesitás:

  • Crear botones custom que llamen cleanAll, antiDrop para un id específico, etc.
  • Integrar el sistema en componentes no estándar.

Ejemplo de uso típico

import { DNDProvider, Draggable, Droppable } from "@components/DnDRevolution";

type Option = { id: number; text: string };

export const SimpleDndScreen = () => {
const workspaceDimensions = { width: 360, height: 640 };

return (
<DNDProvider<Option>
workSpaceDimensions={workspaceDimensions}
mode="remplacement-multi"
calcCentroids={true}
onDrop={(dataByDroppable, historyByDroppable) => {
// Por ejemplo, evaluar si todas las respuestas son correctas
console.log("Data actual:", dataByDroppable);
console.log("Historial:", historyByDroppable);
}}
>
<Droppable<Option> id="slot-1">
{({ isOver, data, history, antiDrop }) => (
// ...
)}
</Droppable>

<Draggable<Option> data={{ id: 1, text: "A" }}>
{({ data, isDragging, distance }) => (
// ...
)}
</Draggable>
</DNDProvider>
);
};

Relación con el resto de componentes

  • Draggable y Droppable asumen que están dentro de un DNDProvider:

    • Se registran en el slice al montarse.
    • Usan los actions/context expuestos por el provider.
  • MultipleDraggables, PadDND, PadDroppable y futuros componentes de alto nivel:

    • No crean su propio store; simplemente consumen el del DNDProvider.
  • El store de DnD es local al provider:

    • No interfiere con el Redux global de la app.
    • Podés tener más de un DNDProvider en distinta parte del árbol si necesitás escenas independientes.

Siguiente sección recomendada: 👉 MultipleDraggables