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
DraggableyDroppableque vivan dentro. - Los hooks auxiliares
useDndDispatch,useDndSelectoryuseDndActions.
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
DNDProviderno 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.
- El tipo lo pide (para mantener coherencia con otros componentes como
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}>dereact-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".
- Al reasignar un droppable a otro draggable, solo limpia vínculos previos cuando
-
El componente
Droppableusamodepara la lógica de historial:- En
"multi"y"multi-disappear"se permite acumular múltiples entradas enhistoryaunque 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
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
historyenDroppable.
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: mapadroppableId → último valor no nulodejado en ese droppable.history: mapadroppableId → arreglo de todos los valores (T)que han caído ahí, en orden.
Detalles:
-
El listener filtra
dataDroppablepara quitarnullantes 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.
- Se usa solapamiento de rectángulos (
-
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
getBestDroppabledel 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
dispatchtipado 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:
DraggableyDroppableya 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,antiDroppara 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
-
DraggableyDroppableasumen que están dentro de unDNDProvider:- Se registran en el slice al montarse.
- Usan los actions/context expuestos por el provider.
-
MultipleDraggables,PadDND,PadDroppabley futuros componentes de alto nivel:- No crean su propio store; simplemente consumen el del
DNDProvider.
- No crean su propio store; simplemente consumen el del
-
El store de DnD es local al provider:
- No interfiere con el Redux global de la app.
- Podés tener más de un
DNDProvideren distinta parte del árbol si necesitás escenas independientes.
Siguiente sección recomendada:
👉 MultipleDraggables