React-dnd: Discussion sur l'API Hooks

Créé le 26 oct. 2018  ·  43Commentaires  ·  Source: react-dnd/react-dnd

Quelqu'un allait demander finalement! La nouvelle API de hooks pourrait éventuellement aider ici. Je pense que la plupart des API peuvent rester à peu près les mêmes, avec le mappage HOC directement dans un hook.

Je me demande si nous pouvons remplacer connectDragSource et connectDropTarget en passant simplement la valeur de useRef . Cela pourrait certainement rendre les choses plus propres si cela est possible!

design decisions discussion

Tous les 43 commentaires

J'ai hâte de commencer à utiliser des hooks dans cette bibliothèque. Dès que les types sont cuits pour l'alpha, nous pouvons configurer une branche de migration

Je joue dans une branche: expérimentation / hooks, juste pour penser à quoi cette API pourrait ressembler. Le composant BoardSquare ressemble à ceci:


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>,
    )
}

Donc, l'idée ici est que createDropTarget configure les connaissances logiques sur l'élément glisser / déposer, son ID et sa prédication, et le hook useDnd le câblerait dans le système DnD et collecterait les accessoires.

Notez qu'il ne s'agit que d'esquisser un design candidat, il n'est pas réellement implémenté

@darthtrevino Sur quelle branche travaillez-vous? Je me demande si nous pouvons supprimer connectDropTarget tous ensemble en utilisant des refs. J'aimerais voir si je peux le faire fonctionner sur votre branche!

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 Je pense que j'ai semblé des API basées sur des const [isOver, canDrop, ref] = useDnd(...) ), prêtes pour le composant consommateur à placer dans son arbre JSX)

Je suppose que cela fonctionne. Il est plus difficile d'utiliser la référence dans plusieurs hooks, mais rien ne vous empêche d'écrire quelque chose qui combine plusieurs références en une seule référence. De quelle bibliothèque s'agissait-il?

Je suppose que nous devons voir quelle est la convention à ce sujet!

Il est plus difficile d'utiliser la référence dans plusieurs crochets, mais rien ne vous empêche d'écrire quelque chose qui combine plusieurs références en une seule référence

Vrai et vrai :-)

De quelle bibliothèque s'agissait-il?

Je n'arrive pas à le retrouver atm: - / - beaucoup d'expériences autour des crochets au cours des quinze dernières jours ...

Il y a https://github.com/beizhedenglong/react-hooks-lib qui fait
const { hovered, bind } = useHover(); return <div {...bind}>{hovered ? 'yes' : 'no'}</div>;
ce qui, je suppose, signifie bind comprend un ref?
[edit: non, ça inclut juste { onMouseEnter, onMouseLeave } , bien sûr ...]

Mais je me souviens avoir vu une autre API renvoyer une référence directement du crochet.

Il n'y a pas grand-chose là-bas, et ce n'est pas en construction pour le moment, mais la branche dans laquelle je suis est experiment/hooks

Juste itérer ici:

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>,
    )
}

C'est ainsi que le chaînage dragSource & dropTarget peut fonctionner, si nous utilisons le ref comme premier argument, les arguments restants peuvent le connecter à plusieurs concepts 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>
    )
}

Donc useDnd ressemblerait à


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)
}

Idée folle, et si nous ne nous embêtions pas avec connect et 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' }} />
}

Je pense que vous pouvez vous débarrasser de Connect, mais je ne suis pas sûr du moniteur, c'est de là que vous obtenez vos accessoires comme isDragging

alors peut-être useDragSource(ref, <type>, <spec>, <collect>) . C'est beaucoup d'arguments, et il peut être étrange d'avoir deux gros objets l'un à côté de l'autre

Pouvons-nous simplement retourner tous les accessoires du moniteur?

Peut-être, je pense que c'est la seule méthode qui causerait des problèmes: https://github.com/react-dnd/react-dnd/blob/84db06edcb94ab3dbb37e8fe89fcad55b1ad07ce/packages/react-dnd/src/interfaces.ts#L117

IIRC, le DragSourceMonitor, DropTragetMonitor et DragLayerMonitor ont tous abouti à la classe DragDropMonitor. Donc, je ne pense pas que nous aurions rencontré des collisions de noms, mais je vérifierais cela.

@yched Je joue juste avec ça et des crochets et j'ai remarqué que nous devons passer la référence.

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

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

@ jacobp100 en effet, a du sens.

D'accord, idée,

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>
}

Notez que les spécifications de la source et de la cible de glisser ne reçoivent aucun paramètre, car vous y avez déjà accès

useMonitorSubscription peut faire un égal peu profond sur l'objet pour réduire les mises à jour

J'ai jeté un premier coup d'œil ici . Pas de tests, mais l'exemple d'échecs fonctionne avec des crochets - devrait montrer ce que je veux faire!

Je pense qu'avoir useDragSource(ref, <type>, <spec>, <collect>) est la proposition qui fonctionne vraiment bien et n'apporte pas beaucoup de changements d'API. La seule différence est que vous passez d'un hoc à un hook.

Avoir deux gros objets l'un à côté de l'autre n'est pas un gros problème à mon avis car vous deviez le faire avant aussi:

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>;
};

La bonne chose est que vous pouvez également utiliser les valeurs d'autres hooks.

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>;
};

Un avantage intéressant est que nous pourrions supprimer les accessoires et les composants des arguments beginDrag et toutes les autres fonctions acceptent car vous y avez déjà accès dans la portée.

^ Je viens de mettre à jour mon dernier commentaire pour montrer que collectSource ne passe pas dans le moniteur à la fonction - vous venez de lire à partir de la portée

@ jacobp100 Je vois. Pour moi, la question serait de savoir si nous avons besoin d'un autre hook pour collecter les données du moniteur ou si nous pouvons également l'implémenter dans useDragSource et useDropTarget.

Cela avait du sens quand les choses étaient des HOC, où vous deviez de toute façon relier les éléments de connexion.

Mais il n'y a plus d'exigence technique pour les coupler, alors je les ai laissés séparés.

Ensuite, vous avez plus de liberté avec eux - peut-être un ou plusieurs composants enfants pour répondre aux changements de surveillance, mais pas le composant qui commence à être déplacé. Il a également un bonus supplémentaire que si vous n'utilisez pas d'abonnements de moniteur, ce crochet peut être secoué.

Cela dit, ceci est une première ébauche! Je ne suis pas contre le changement si tel est le consensus!

C'est un bon argument. Le seul et unique problème que je vois est que l'utilisateur peut être confus lorsqu'il appelle directement le moniteur et se demande pourquoi il ne se comporte pas correctement:

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>
}

Cela peut probablement être résolu avec de la documentation et des avertissements lorsque vous appelez la fonction en dehors du hook useMonitor .

En fait, cela fonctionnera! C'est l'une des choses qui ne me passionne pas à 100%: le rappel dans useMonitor est utilisé à la fois pour la détection des modifications et la valeur de retour. On a l'impression que cela va à l'encontre des hooks actuels dans React core.

Peut-être que quelque chose comme ça fonctionne mieux,

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

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

Certes, il est beaucoup plus facile d'introduire des bugs avec ce formulaire

Je ne suis pas sûr à 100% des composants internes de react-dnd mais le moniteur n'est-il pas là, donc nous n'avons pas à rendre le composant à chaque fois que votre souris bouge?

Donc, la précédente cesserait de fonctionner si vous supprimiez useMonitorSubscription et que vous n'aviez que monitor.isDragging() dans la fonction de rendu?

Cela ne fonctionnerait donc pas correctement?

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>
}

Le moniteur a une méthode subscribe qui avertit ses écouteurs chaque fois qu'une valeur est mise à jour. Nous devons donc faire quelque chose pour que le composant sache quand mettre à jour.

En développant le post précédent, si nous faisons de l'optimisation de la détection des changements une fonctionnalité facultative, cela pourrait être aussi simple que,

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

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

Quelques idées.

Tout d'abord, pouvons-nous rendre l'argument ref facultatif en demandant au hook de renvoyer une implémentation 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); 

Deuxièmement, plutôt que de leur faire invoquer un autre hook dans useMonitorUpdates , je me demande si nous pouvons simplement faire ce qui suit:

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

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

Je vais fermer cela pour l'instant, car il existe une API candidate. N'hésitez pas à commenter cela avec de nouveaux problèmes. Merci!

On dirait que l'API des hooks a un défaut dans la conception: https://github.com/Swizec/useDimensions/issues/3

Intéressant, je suppose donc qu'une alternative est que nous utilisons une fonction de connexion, comme nous le faisons actuellement dans l'API basée sur les classes:


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>
    )
}

Donc, en général, l'API ressemblerait à ...

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

J'espérais en quelque sorte ne plus exiger les fonctions de connecteur, mais si l'API est cassée sans elles, nous pouvons nous débrouiller

En lisant ce problème, nos API sont similaires, mais je pense que le problème est qu'elles utilisent un effet de disposition pour obtenir la mesure d'un nœud DOM. Nous n'utilisons pas vraiment d'effets de mise en page ici, nous enregistrons simplement les nœuds DOM avec le dnd-core.

@gaearon , notre API de hooks proposée est très similaire à l'API useDimensions - est-ce que cette forme spécifique est un antipattern (par exemple let [props, ref] = useCustomHook(config) ), ou est-elle idiosyncratique au problème que la bibliothèque tente de résoudre?

@darthtrevino Autant que je

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. Je vais voir si je peux faire un cas de test pour cela plus tard cette semaine pour prouver s'il explose ou non

S'il explose, l'utilisation des fonctions de connecteur sera le secours

Si j'ai tout fait correctement, j'ai pu le reproduire ici: https://codesandbox.io/s/xj7k9x4vp4

Kaboom, bon travail, merci @ k15a . Je vais bientôt mettre à jour l'API des hooks pour utiliser les fonctions de connexion et inclure votre exemple comme cas de test

J'ai donc passé beaucoup de temps la nuit dernière à retravailler l'API des hooks pour utiliser les fonctions de connecteur. En ce qui concerne la conception d'API, je déteste vraiment ça.

Ma prochaine pensée est que nous pouvons renvoyer un callback-ref au lieu d'un ref-object. Cela devrait nous donner la flexibilité d'utiliser directement comme ref ou de passer une ref dedans

Utilisation directe:

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

Chaînage

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

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

Avec objet Ref

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

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

Pour moi, j'ai l'impression que useLayoutEffect devrait se déclencher si le composant ou l'un de ses enfants est mis à jour. Si c'était le cas, nous pourrions simplement l'utiliser.

J'ai créé un ticket dans le repo react . N'hésitez pas à commenter.

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

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

Je ne sais pas à quel point cela fonctionne, car vous appelleriez la référence avec chaque rendu. Ainsi, avec chaque rendu, vous devrez vous connecter et vous déconnecter.

Ne serait-il pas préférable non plus d'avoir

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

Ce serait la même chose

Donc, pour soulager mon commentaire précédent, l'API de # 1280 est en train d'être meilleure que je ne le pensais. N'hésitez pas à commenter ici ou là

Cette page vous a été utile?
0 / 5 - 0 notes