Droppable
Droppable es el componente base que define una zona donde se pueden soltar elementos (Draggable) dentro del sistema de DnD.
Se integra con el DNDProvider y con el slice interno para saber:
- Qué elemento se está arrastrando encima.
- QuĂ© se ha soltado ahĂ.
- El historial de drops en esa zona.
Su API está basada en render props, para que controles totalmente cómo se ve y se comporta.
Firma básica​
type DroppableChildrenProps<T> = {
/** Item que se está arrastrando encima de esta zona (o null si no hay) */
isOver: T | null;
/** Ăšltimo item soltado en esta zona (o undefined si nunca se ha soltado nada) */
data: T | undefined;
/** Historial completo de items que se han soltado aquĂ, en orden cronolĂłgico */
history: T[];
/** Función para “deshacer” el último drop en esta zona */
antiDrop: () => void;
};
export interface DroppableProps<T> {
/** ID Ăşnico de la zona de drop (si no se pasa, se genera un UUID) */
id?: string;
/** Deshabilita completamente esta zona de drop */
disable?: boolean;
/** Estilos del contenedor externo */
style?: ViewStyle;
/** Activa animación tipo “latido” mientras hay un drag encima */
heartAnimation?: boolean;
/**
* Callback que se dispara cada vez que ocurre un drop final en esta zona.
* Recibe el dato soltado, el historial actualizado y el id de la zona.
*/
onDrop?: (info: { data: T; history: T[]; id: string }) => void;
/** Render prop que recibe el estado del droppable y devuelve JSX */
children: (props: DroppableChildrenProps<T>) => React.ReactNode;
}
Uso tĂpico:
<Droppable<MyData> id="answer-slot">
{({ isOver, data, history, antiDrop }) => (
// Tu UI aquĂ
)}
</Droppable>
Props en detalle​
| Prop | Tipo | Default | DescripciĂłn |
|---|---|---|---|
id | string | UUID | Identificador de la zona. Si no se pasa, se genera uno internamente. Se usa para registrar la zona en el store. |
disable | boolean | false | Si está en true, la zona se desregistra y deja de aceptar drops. |
style | ViewStyle | — | Estilo del View externo que envuelve el área de drop. |
heartAnimation | boolean | false | Si está en true, aplica una animación de “latido” mientras un draggable pasa por encima. |
onDrop | (info: { data: T; history: T[]; id: string }) => void | — | Se llama cada vez que un drop final ocurre en esta zona, con el dato actual, el historial y el id. |
children | (props: DroppableChildrenProps<T>) => ReactNode | — | Render prop que recibe el estado del droppable (isOver, data, history, antiDrop). |
Props de la render prop​
Dentro de children recibes:
-
isOver: T | nullnull: ningún draggable está sobre esta zona.T: hay un draggable encima, yisOvercontiene sudata.- Úsalo tanto como bandera (
!!isOver) como para inspeccionar el contenido que se está arrastrando.
-
data: T | undefined- Ăšltimo dato que se soltĂł en esta zona.
undefinedsi todavĂa no ha caĂdo nada.
-
history: T[]- Lista de todos los
dataque se han soltado aquĂ, en orden. - En modos
multi/multi-disappearse seguirá acumulando.
- Lista de todos los
-
antiDrop: () => void-
“Deshace” el último drop:
- Limpia
datasi era el Ăşltimo elemento. - Hace
pop()sobrehistory. - Notifica al slice (
actions.antiDrop) para mantener el store sincronizado.
- Limpia
-
Ejemplo básico​
Zona de drop simple para mostrar el Ăşltimo elemento soltado:
import { View, Text } from "react-native";
import { DNDProvider, Droppable, Draggable } from "@components/DnDRevolution";
type Option = { id: number; text: string };
export const BasicDroppableExample = () => (
<DNDProvider<Option> mode="remplacement-multi">
<Droppable<Option> id="drop-1">
{({ isOver, data, history, antiDrop }) => (
<View
style={{
width: 220,
height: 80,
borderWidth: 2,
borderRadius: 12,
borderColor: isOver ? "green" : "gray",
alignItems: "center",
justifyContent: "center",
}}
>
<Text>
{data?.text ?? "Suelta una opciĂłn aquĂ"}
</Text>
{history.length > 0 && (
<Text style={{ fontSize: 10, marginTop: 4 }}>
Historial: {history.map((h) => h.text).join(", ")}
</Text>
)}
{history.length > 0 && (
<Text onPress={antiDrop} style={{ marginTop: 4 }}>
Deshacer Ăşltimo drop
</Text>
)}
</View>
)}
</Droppable>
<Draggable<Option> data={{ id: 1, text: "OpciĂłn A" }}>
{({ data, isDragging }) => (
<View
style={{
marginTop: 24,
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 8,
backgroundColor: isDragging ? "#4f46e5" : "#6366f1",
}}
>
<Text style={{ color: "white" }}>{data.text}</Text>
</View>
)}
</Draggable>
</DNDProvider>
);
Ejemplo con heartAnimation​
<Droppable<Option>
id="animated-drop"
heartAnimation
onDrop={({ data, history, id }) => {
console.log("Dropped on", id, ":", data, "historial:", history.length);
}}
>
{({ isOver, data }) => (
<View
style={{
width: 200,
height: 80,
borderRadius: 16,
borderWidth: 2,
borderColor: isOver ? "#22c55e" : "#9ca3af",
alignItems: "center",
justifyContent: "center",
}}
>
<Text>{data?.text ?? "Arrastra algo aquĂ đź’«"}</Text>
</View>
)}
</Droppable>
Mientras un Draggable está encima de esta zona y heartAnimation es true, el componente aplica internamente:
- Una animación de escala repetida (tipo “latido”) usando
withRepeat+withSequence+withTiming.
Flujo interno (cómo se integra con el store)​
Internamente Droppable:
-
Genera/usa un
id(idString):- Si no proporcionas
id, se crea un UUID viareact-native-uuid.
- Si no proporcionas
-
Se registra en el slice de DnD:
registerDroppable({ id })al montarse (sidisableesfalse).unregisterDroppable({ id })al desmontarse o cuando cambiasdisableatrue.
-
Mide su rectángulo en pantalla:
- Usa
ref.current?.measureInWindow(...)para obtener{ x, y, w, h }. - EnvĂa estas coordenadas al slice con
updateDroppableArea({ id, ...rect }). - Vuelve a medir cuando el estado
moveddel slice cambia (cada vez que se arrastra algo).
- Usa
-
Escucha el estado global:
overId: ID del droppable que está actualmente “debajo” del draggable.draggingData: datos (T) del draggable activo.lastDrop: último drop confirmado por el slice.mode: modo global (remplacement-multi,multi,multi-disappear, etc.).deleteData: bandera global que se activa cuando se limpia todo (cleanAll()).
-
Actualiza su estado local:
-
isOverse calcula como:overId === idString && draggingData !== null ? draggingData : null. -
Si
lastDrop.droppableId === idStringy el drop es válido:- Actualiza
droppedData(para la propdata). - Hace
pushenhistory. - Llama
onDropcon{ data, history, id }.
- Actualiza
-
Si
deleteDataestrue, reseteadroppedData(para integrarse con el botĂłn de limpiar).
-
-
Expone todo a la UI vĂa
children({ isOver, data, history, antiDrop }).
Esto te permite construir logĂsticas de correcciĂłn, feedback visual y resets sin tener que pelearte directamente con Redux.
Cuándo usar Droppable directo​
Usa Droppable cuando:
- Quieres definir una zona de drop custom (layout libre).
- Necesitas lĂłgica especial para cĂłmo se muestran los datos/historial.
- Estás creando un patrón nuevo que no encaja con
PadDroppableu otros componentes de alto nivel.
Si tu caso ya es un “pad” de respuesta (números, letras, etc.), normalmente será más cómodo usar:
y dejar Droppable como bloque base subyacente.
Siguiente sección recomendada: 👉 MultipleDraggables