React-dnd: Discusión de la API de Hooks

Creado en 26 oct. 2018  ·  43Comentarios  ·  Fuente: react-dnd/react-dnd

¡Alguien iba a preguntar eventualmente! La nueva API de ganchos posiblemente podría ayudar aquí. Creo que la mayor parte de la API puede permanecer prácticamente igual, con el mapeo de HOC directamente en un gancho.

Me pregunto si podemos reemplazar connectDragSource y connectDropTarget simplemente pasando el valor de useRef . ¡Definitivamente podría hacer las cosas más limpias si eso es posible!

design decisions discussion

Todos 43 comentarios

No puedo esperar para comenzar a usar ganchos en esta biblioteca. Tan pronto como los tipos estén horneados para el alfa, podemos configurar una rama de migración

Estoy jugando en una rama: experimento / ganchos, solo para pensar en cómo se vería esta API. El componente BoardSquare tiene este aspecto:


const dropTarget = createDropTarget(ItemTypes.KNIGHT, {
    canDrop: (props: BoardSquareProps) => canMoveKnight(props.x, props.y),
    drop: (props: BoardSquareProps) => moveKnight(props.x, props.y),
})

const BoardSquare = ({ x, y, children }) => {
    const black = (x + y) % 2 === 1
    const [connectDropTarget, isOver, canDrop] = useDnd(
        dropTarget,
        connect => connect.dropTarget,
        (connect, monitor) => !!monitor.isOver,
        (connect, monitor) => !!monitor.canDrop,
    )

    return connectDropTarget(
        <div>
            <Square black={black}>{children}</Square>
            {isOver && !canDrop && <Overlay color={'red'} />}
            {!isOver && canDrop && <Overlay color={'yellow'} />}
            {isOver && canDrop && <Overlay color={'green'} />}
        </div>,
    )
}

Entonces, la idea aquí es que createDropTarget configura el conocimiento lógico sobre el elemento de arrastrar / soltar, su ID y predicación, y el gancho useDnd lo conectaría al sistema DnD y recopilaría accesorios.

Tenga en cuenta que esto es solo esbozar un diseño candidato, en realidad no está implementado

@darthtrevino ¿ connectDropTarget todos juntos usando refs. ¡Me encantaría ver si puedo hacerlo funcionar en tu rama!

const dropTarget = createDropTarget(ItemTypes.KNIGHT, {
    canDrop: (props: BoardSquareProps) => canMoveKnight(props.x, props.y),
    drop: (props: BoardSquareProps) => moveKnight(props.x, props.y),
})

const BoardSquare = ({ x, y, children }) => {
    const dropTargetElement = useRef(null);
    const black = (x + y) % 2 === 1
    const [isOver, canDrop] = useDnd(
        dropTargetElement,
        dropTarget,
        ...
    )

    return (
        <div ref={dropTargetElement}>
            <Square black={black}>{children}</Square>
            {isOver && !canDrop && <Overlay color={'red'} />}
            {!isOver && canDrop && <Overlay color={'yellow'} />}
            {isOver && canDrop && <Overlay color={'green'} />}
        </div>
    )
}

@ jacobp100 Creo que parezco API basadas en ganchos similares donde la referencia es proporcionada y devuelta por el propio gancho (como const [isOver, canDrop, ref] = useDnd(...) ), lista para que el componente consumidor se coloque en su árbol JSX)

Supongo que funciona. Hace que sea más difícil usar la referencia en múltiples ganchos, pero no hay nada que te impida escribir algo que combine múltiples referencias en una sola referencia. ¿Qué biblioteca era esta?

¡Supongo que tenemos que ver cuál es la convención sobre esto!

Hace que sea más difícil usar la referencia en múltiples ganchos, pero no hay nada que te impida escribir algo que combine múltiples referencias en una sola referencia.

Verdadero y verdadero :-)

¿Qué biblioteca era esta?

Parece que no puedo encontrarlo de nuevo en el cajero automático: - / - muchos experimentos con ganchos en la última quincena ...

Hay https://github.com/beizhedenglong/react-hooks-lib que sí
const { hovered, bind } = useHover(); return <div {...bind}>{hovered ? 'yes' : 'no'}</div>;
que supongo que significa que bind incluye una referencia?
[editar: no, solo incluye { onMouseEnter, onMouseLeave } , por supuesto ...]

Pero recuerdo haber visto otra API devolviendo una referencia directamente desde el gancho.

No hay mucho allí y no se está construyendo en este momento, pero la sucursal en la que estoy es experiment/hooks

Solo iterando aquí:

const dropTarget = createDropTarget(ItemTypes.KNIGHT, {
    canDrop: props => canMoveKnight(props.x, props.y),
    drop: props => moveKnight(props.x, props.y),
})

const BoardSquare = ({ x, y, children }) => {
    const black = (x + y) % 2 === 1
        const ref = useRef(null)
    const [isOver, canDrop] = useDnd(
        connect => connect(ref, dropTarget),
        monitor => monitor.isOver,
        monitor => monitor.canDrop,
    )

    return (
        <div ref={ref}>
            <Square black={black}>{children}</Square>
            {isOver && !canDrop && <Overlay color={'red'} />}
            {!isOver && canDrop && <Overlay color={'yellow'} />}
            {isOver && canDrop && <Overlay color={'green'} />}
        </div>,
    )
}

Así es como podría funcionar el encadenamiento de dragSource y dropTarget, si usamos la referencia como primer argumento, los demás argumentos pueden conectarlo a múltiples conceptos dnd.

const dropTarget = createDropTarget(ItemTypes.CARD, {
    canDrop: () => false
    hover(props, monitor) {
        /**/
    },
})
const dragSource = createDragSource(ItemTypes.CARD, {
    beginDrag: props => /*...*/,
    endDrag: (props, monitor) => /*...*/
})

function Card({ text }) {
    const ref = useRef(null)
    const [isDragging] = useDnd(
        connect => connect(ref, dropTarget, dragSource),
        monitor => monitor.isDragging,
    )
    const opacity = isDragging ? 0 : 1

    return (
        <div ref={ref} style={{ ...style, opacity }}>
            {text}
        </div>
    )
}

Entonces useDnd se vería así


export type DndConcept = DragSource | DropTarget
export type ConnectorFunction = (connector: Connector /*new type*/) => void
export type CollectorFunction<T> = (monitor: DragDropMonitor) => T

export function useDnd(
    connector: ConnectorFunction,
    ...collectors: CollectorFunction[]
): any[] {
    const dragDropManager = useDragDropManager()
        // magic
       return collectedProperties
}

export function useDragDropManager(): DragDropManager {
    return useContext(DragDropContextType)
}

Idea loca, ¿y si no nos molestamos con connect y monitor ?

const Test = props => {
  const ref = useRef(null);
  const { isDragging } = useDragSource(ref, ItemTypes.CARD, {
    canDrag: props.enabled,
    beginDrag: () => ({ id: props.id }),
  });

  return <div ref={ref} style={{ color: isDragging ? 'red' : 'black' }} />
}

Creo que puedes deshacerte de connect, pero no estoy seguro del monitor, es donde obtienes tus accesorios como isDragging from

entonces tal vez useDragSource(ref, <type>, <spec>, <collect>) . Son muchos argumentos, y puede ser extraño tener dos objetos gordos uno al lado del otro

Sin embargo, ¿podemos devolver todos los accesorios del monitor?

Quizás, creo que este es el único método que causaría problemas: https://github.com/react-dnd/react-dnd/blob/84db06edcb94ab3dbb37e8fe89fcad55b1ad07ce/packages/react-dnd/src/interfaces.ts#L117

IIRC, DragSourceMonitor, DropTragetMonitor y DragLayerMonitor se reducen a la clase DragDropMonitor. Así que no creo que nos encontremos con colisiones de nombres, pero lo comprobaría dos veces.

@yched Solo estaba jugando con esto y ganchos y noté que teníamos que pasar la referencia. Vea el caso cuando algo es tanto una fuente como un objetivo,

const Test = () => {
  const ref = useRef(null)
  const source = useDragSource(ref, …props)
  const target = useDragTarget(ref, …props)

  return <div ref={ref}>…</div>
}

@ jacobp100 de hecho, tiene sentido.

Está bien, idea

const Test = (props) => {
  const ref = useRef(null)
  const sourceMonitor = useDragSource(ref, 'item' {
    beginDrag: () => ({ id: props.id })
  })
  const targetMonitor = useDropTarget(ref, ['item'] {
    drop: () => alert(targetMonitor.getItem().id)
  })
  const { isDragging } = useMonitorSubscription(targetMonitor, () => ({
    isDragging: targetMonitor.isDragging()
  })

  return <div ref={ref}>…</div>
}

Tenga en cuenta que las especificaciones para el origen y el destino de arrastre no reciben ningún parámetro, ya que ya tiene acceso a ellos

useMonitorSubscription puede hacer una igualdad superficial en el objeto para reducir las actualizaciones

He echado un vistazo inicial por aquí . No obtuve pruebas, pero el ejemplo de ajedrez funciona con ganchos, ¡debería mostrar lo que quiero hacer!

Creo que tener useDragSource(ref, <type>, <spec>, <collect>) es la propuesta que funciona muy bien y no trae muchos cambios en la API. La única diferencia es que cambia de un hoc a un gancho.

Además, tener dos objetos gordos uno al lado del otro no es un gran problema en mi opinión porque también tenías que hacer eso antes:

const DragDrop = () => {
  const ref = useRef(null);

  const dragSource = {
    beginDrag() {
      return props;
    }
  };

  const collectSource = monitor => {
    return {
      isDragging: monitor.isDragging()
    };
  };

  const { isDragging } = useDragSource(ref, "Item", dragSource, collectSource);

  const dropTarget = {
    drop() {
      return undefined;
    }
  };

  const collectTarget = monitor => {
    return {
      isOver: monitor.isOver()
    };
  };

  const { isOver } = useDropTarget(ref, "Item", dropTarget, collectTarget);

  return <div ref={ref}>Drag me</div>;
};

Lo bueno es que también puedes usar valores de otros ganchos.

const Drag = () => {
  const ref = useRef(null);
  const context = useContext(Context)

  const dragSource = {
    beginDrag() {
      context.setDragItem(props)
      return props;
    },
    endDrag() {
      context.setDragItem(null)
    }
  };

  const collectSource = monitor => {
    return {
      isDragging: monitor.isDragging()
    };
  };

  const { isDragging } = useDragSource(ref, "Item", dragSource, collectSource);

  return <div ref={ref}>Drag me</div>;
};

Una buena ventaja es que podríamos eliminar accesorios y componentes de los argumentos beginDrag y todas las demás funciones aceptar, ya que ya tiene acceso a ellos en el alcance.

^ Acabo de actualizar mi último comentario para mostrar que collectSource no pasa en el monitor a la función - usted acaba de leer desde el alcance

@ jacobp100 Ya veo. Para mí, la pregunta sería si necesitamos otro gancho para recopilar datos del monitor o si podríamos implementarlo en useDragSource y useDropTarget también.

Tenía sentido cuando las cosas eran HOC, donde tenía que vincular las cosas de conexión de todos modos.

Pero ahora ya no hay requisitos técnicos para acoplarlos, así que los dejé separados.

Entonces tiene más libertad con ellos, tal vez uno o más componentes secundarios para responder a los cambios del monitor, pero no el componente que comienza a arrastrarse. También tiene una ventaja adicional de que si no está utilizando suscripciones de monitor, ese gancho puede sacudirse.

Dicho esto, ¡este es un borrador inicial! ¡No me opongo a cambiarlo si ese es el consenso!

Ese es un buen argumento. El único problema que veo es que el usuario podría confundirse cuando llama al monitor directamente y se pregunta por qué no se comporta correctamente:

const Test = (props) => {
  const ref = useRef(null)

  const sourceMonitor = useDragSource(ref, 'item', {
    beginDrag: () => ({ id: props.id })
  })

  const targetMonitor = useDropTarget(ref, ['item'], {
    drop: () => alert(targetMonitor.getItem().id)
  })

  const { isDragging } = useMonitor(targetMonitor, () => ({
    isDragging: targetMonitor.isDragging()
  })

  return <div ref={ref}>{sourceMonitor.isDragging() ? 'Dragging' : 'Drag me'}</div>
}

Probablemente eso se puede resolver con documentación y advertencias cuando llamas a la función fuera del gancho useMonitor .

De hecho, ¡funcionará! Es una de las cosas en las que no estoy 100% interesado: la devolución de llamada en useMonitor se usa tanto para la detección de cambios como para el valor de retorno. Parece que va en contra de los ganchos actuales en el núcleo de React.

Tal vez algo como esto funcione mejor

const Test = (props) => {
  ...
  useMonitorUpdates(targetMonitor, () => [targetMonitor.isDragging()]);

  return <div ref={ref}>{sourceMonitor.isDragging() ? 'Dragging' : 'Drag me'}</div>
}

Es cierto que es mucho más fácil introducir errores con este formulario.

No estoy 100% seguro de los componentes internos de react-dnd, pero ¿no está el monitor allí para que no tengamos que renderizar el componente cada vez que se mueve el mouse?

Entonces, ¿lo anterior dejaría de funcionar si elimina useMonitorSubscription y solo tenga monitor.isDragging() en la función de renderizado?

Por tanto, ¿esto no funcionaría correctamente?

const Test = (props) => {
  const ref = useRef(null)

  const sourceMonitor = useDragSource(ref, 'item', {
    beginDrag: () => ({ id: props.id })
  })

  return <div ref={ref}>{sourceMonitor.isDragging() ? 'Dragging' : 'Drag me'}</div>
}

El monitor tiene un método subscribe que notifica a sus oyentes cada vez que se actualiza un valor. Así que tenemos que hacer _algo_ para que el componente sepa cuándo actualizar.

Sin embargo, ampliando la publicación anterior, si hacemos que la optimización de la detección de cambios sea una característica opcional, esto podría ser tan simple como,

const Test = (props) => {
  ...
  useMonitorUpdates(sourceMonitor);

  return <div ref={ref}>{sourceMonitor.isDragging() ? 'Dragging' : 'Drag me'}</div>
}

Un par de ideas.

Primero, ¿podemos hacer que el argumento ref opcional haciendo que el gancho devuelva una implementación Ref ?

const dragSource = useDragSource('item', spec);
return <div ref={dragSource}/>

// or if you want to use a ref
const ref = useRef();
const dragSource = useDragSource('item', dragSourceSpec)(ref); 
const dropTarget = useDropTarget('item', dropTargetSpec)(ref); 

En segundo lugar, en lugar de hacer que invoquen otro gancho en useMonitorUpdates , me pregunto si podemos hacer lo siguiente:

const dragSource = useDragSource('item', spec);

const { isDragging } = dragSource.subscribe(() => ({
  isDragging: targetMonitor.isDragging()
}));

Voy a cerrar esto por ahora, ya que hay una API candidata. Sin embargo, siéntase libre de comentar sobre eso con nuevos problemas. ¡Gracias!

Parece que la API de hooks tiene un defecto en el diseño: https://github.com/Swizec/useDimensions/issues/3

Interesante, así que supongo que una alternativa es que usamos una función de conexión, como lo hacemos actualmente en la API basada en clases:


const Box = ({ name }) => {
    const [{ isDragging }, dragSource] = useDrag({
        item: { name, type: ItemTypes.BOX },
        end: (dropResult?: { name: string }) => {
            if (dropResult) {
                alert(`You dropped ${item.name} into ${dropResult.name}!`)
            }
        },
        collect: monitor => ({
            isDragging: monitor.isDragging(),
        }),
    })
    const opacity = isDragging ? 0.4 : 1

    return (
        <div ref={node => dragSource(node)} style={{ ...style, opacity }}>
            {name}
        </div>
    )
}

Entonces, en general, la API se vería así ...

const [props, connectDragSource, connectDragPreview] = useDrag(spec)
const [props, connectDropTarget] = useDrop(spec)

Tenía la esperanza de dejar de requerir las funciones del conector, pero si la API se rompe sin ellas, entonces podemos hacerlo.

Sin embargo, mientras leo ese problema, nuestras API son similares, pero creo que el problema es que están usando un efecto de diseño para obtener la medición de un nodo DOM. Realmente no estamos usando efectos de diseño aquí, solo registramos los nodos DOM con el dnd-core.

@gaearon , nuestra API de hooks propuesta es muy similar a la API useDimensions: ¿es una forma específica de un antipatrón (por ejemplo, let [props, ref] = useCustomHook(config) ), o es idiosincrásico para el problema que la biblioteca está tratando de resolver?

@darthtrevino Por lo que tengo entendido, si usa el gancho useDragSource y pasa la referencia a un componente secundario y el componente secundario se reproduce, no actualizaremos el nodo dom registrado en dnd-core:

function Parent() {
  const ref = useRef();
  const dragSource = useDragSource(ref, ...);

  return <Child connect={ref} />;
}

function Child({ connect }) {
  const [open, setOpen] = useState(false);

  function toggle() {
    setOpen(!open);
  }

  return (
    <Fragment>
      <button onClick={toggle}>Toggle</button>
      {open ? <div ref={connect} /> : null}
    </Fragment>
  );
}

Yech. Veré si puedo hacer un caso de prueba para esto a finales de esta semana para demostrar si explota o no

Si explota, usar las funciones del conector será la alternativa

Si hice todo correctamente pude reproducirlo aquí: https://codesandbox.io/s/xj7k9x4vp4

Kaboom, buen trabajo, gracias @ k15a . Actualizaré la API de hooks para usar funciones de conexión pronto e incluiré su ejemplo como caso de prueba

Así que pasé un montón de tiempo anoche reelaborando la API de hooks para usar funciones de conector. En lo que respecta al diseño de API, realmente lo odio.

Mi siguiente pensamiento es que podemos devolver un callback-ref en lugar de un ref-object. Esto debería darnos flexibilidad para usarlo como referencia directamente o para pasarle una referencia.

Usando directamente:

let [props, dragSource] = useDrag({spec}) // dragSource result is a function
return <div ref={dragSource} {...props}>hey</div>

Encadenamiento

let [dragProps, dragSource] = useDrag({spec})
let [dropProps, dropTarget] = useDrag({spec})

return <div ref={node => dragSource(dropTarget(node))}>hey</div>

Con objeto de referencia

let ref = useRef(null)
let [dragProps, dragSource] = useDrag({spec})
let [dropProps, dropTarget] = useDrag({spec})
dragSource(dropTarget(ref))

return <div ref={ref}>hey</div>

Para mí, parece que useLayoutEffect debería activarse si el componente o cualquiera de sus hijos se actualiza. Si lo hiciera, podríamos usar eso.

He creado un ticket en el repositorio de reacción . Siéntete libre de comentar.

let [dragProps, dragSource] = useDrag({spec})
let [dropProps, dropTarget] = useDrag({spec})

return <div ref={node => dragSource(dropTarget(node))}>hey</div>

No sé qué tan bien funciona esto, ya que llamaría a la referencia con cada render. Entonces, con cada render, necesitaría conectarse y desconectarse.

Además, ¿no sería mejor tener tan

node => {
    dragSource(node)
    dropTarget(node)
}

Sería lo mismo

Entonces, para suavizar mi comentario anterior, la API en # 1280 se perfila para ser mejor de lo que pensé al principio. Siéntete libre de comentar aquí o allá

¿Fue útil esta página
0 / 5 - 0 calificaciones