React-dnd: Hooks API-Diskussion

Erstellt am 26. Okt. 2018  ·  43Kommentare  ·  Quelle: react-dnd/react-dnd

Jemand würde irgendwann fragen! Die neue Hooks-API könnte hier möglicherweise helfen. Ich denke, der größte Teil der API kann ziemlich gleich bleiben, da die HOC-Zuordnung direkt in einen Hook erfolgt.

Ich frage mich, ob wir connectDragSource und connectDropTarget ersetzen können, indem wir nur den Wert von useRef . Es könnte definitiv sauberer werden, wenn das möglich ist!

design decisions discussion

Alle 43 Kommentare

Ich kann es kaum erwarten, Hooks in dieser Bibliothek zu verwenden. Sobald die Typen für das Alpha gebacken sind, können wir einen Migrationszweig einrichten

Ich spiele in einem Zweig herum: Experiment / Hooks, nur um darüber nachzudenken, wie diese API aussehen könnte. Die BoardSquare-Komponente sieht folgendermaßen aus:


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

Die Idee hier ist also, dass createDropTarget das logische Wissen über das Drag / Drop-Element, seine ID und Prädikation einrichtet und der useDnd-Hook es mit dem DnD-System verbindet und Requisiten sammelt.

Beachten Sie, dass dies nur ein Kandidatendesign skizziert und nicht tatsächlich implementiert ist

@darthtrevino An welchem Zweig arbeiten Sie? Ich frage mich, ob wir connectDropTarget alle zusammen mit Refs entfernen können. Ich würde gerne sehen, ob ich es in Ihrer Branche zum Laufen bringen kann!

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 Ich glaube, ich habe ähnliche Hook-basierte APIs, bei denen der Ref vom Hook selbst bereitgestellt und zurückgegeben wird (wie const [isOver, canDrop, ref] = useDnd(...) ) und bereit ist, dass die konsumierende Komponente in ihren JSX-Baum eingefügt wird.

Ich nehme an, das funktioniert. Es macht es schwieriger, den Ref in mehreren Hooks zu verwenden, aber nichts hindert Sie daran, etwas zu schreiben, das mehrere Refs zu einem einzigen Ref kombiniert. Welche Bibliothek war das?

Ich denke, wir müssen sehen, was die Konvention dazu ist!

Es macht es schwieriger, den Ref in mehreren Hooks zu verwenden, aber nichts hindert Sie daran, etwas zu schreiben, das mehrere Refs zu einem einzigen Ref kombiniert

Richtig und richtig :-)

Welche Bibliothek war das?

Kann es nicht wiederfinden atm: - / - viele Experimente rund um Haken in den letzten 14 Tagen ...

Es gibt https://github.com/beizhedenglong/react-hooks-lib , das dies tut
const { hovered, bind } = useHover(); return <div {...bind}>{hovered ? 'yes' : 'no'}</div>;
was ich denke bedeutet, dass bind eine Referenz enthält?
[edit: nein, es enthält natürlich nur { onMouseEnter, onMouseLeave } ...]

Aber ich erinnere mich, dass eine andere API einen Ref direkt vom Hook zurückgab.

Es ist nicht viel da und es wird im Moment nicht gebaut, aber der Zweig, in dem ich bin, ist experiment/hooks

Nur hier wiederholen:

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

So könnte die Verkettung von dragSource & dropTarget funktionieren. Wenn wir den ref als erstes Argument verwenden, können die restlichen Argumente ihn mit mehreren dnd-Konzepten verbinden.

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

Also würde useDnd so aussehen


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

Verrückte Idee, was wäre, wenn wir uns nicht um connect und monitor kümmern würden?

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

Ich denke, Sie können Connect loswerden, aber ich bin mir nicht sicher, was den Monitor betrifft. Hier erhalten Sie Ihre Requisiten wie isDragging

also vielleicht useDragSource(ref, <type>, <spec>, <collect>) . Das sind viele Argumente, und es kann seltsam sein, zwei dicke Objekte nebeneinander zu haben

Können wir aber einfach alle Requisiten vom Monitor zurückgeben?

Vielleicht denke ich, dass dies die einzige Methode ist, die Probleme verursachen würde: https://github.com/react-dnd/react-dnd/blob/84db06edcb94ab3dbb37e8fe89fcad55b1ad07ce/packages/react-dnd/src/interfaces.ts#L117

IIRC, DragSourceMonitor, DropTragetMonitor und DragLayerMonitor sind alle auf die DragDropMonitor-Klasse beschränkt. Ich glaube also nicht, dass wir auf Namenskollisionen stoßen würden, aber ich würde das noch einmal überprüfen.

@yched Ich spiele nur damit herum und Schiedsrichter weitergeben müssen.

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

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

@ jacobp100 macht in der Tat Sinn.

Okay, Idee,

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

Beachten Sie, dass die Spezifikationen für Drag Source und Target keine Parameter erhalten, da Sie bereits Zugriff darauf haben

useMonitorSubscription kann eine flache Entsprechung für das Objekt ausführen, um Aktualisierungen zu reduzieren

Ich habe hier einen ersten Blick darauf geworfen . Ich habe keine Tests, aber das Schachbeispiel funktioniert mit Haken - sollte zeigen, was ich tun möchte!

Ich denke, useDragSource(ref, <type>, <spec>, <collect>) ist der Vorschlag, der wirklich gut funktioniert und nicht zu vielen API-Änderungen bringt. Der einzige Unterschied besteht darin, dass Sie von einem Hoc zu einem Hook wechseln.

Auch zwei dicke Gegenstände nebeneinander zu haben, ist meiner Meinung nach kein großes Problem, da Sie das auch vorher tun mussten:

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

Das Schöne ist, dass Sie auch Werte von anderen Hooks verwenden können.

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

Ein schöner Vorteil ist, dass wir Requisiten und Komponenten aus den Argumenten beginDrag entfernen können und alle anderen Funktionen akzeptieren, da Sie bereits im Bereich Zugriff darauf haben.

^ Ich habe gerade meinen letzten Kommentar aktualisiert, um zu zeigen, dass collectSource im Monitor nicht an die Funktion übergeben wird - Sie haben gerade aus dem Bereich gelesen

@ jacobp100 Ich

Es machte Sinn, wenn es sich um HOCs handelte, bei denen man die Connect-Inhalte sowieso verknüpfen musste.

Aber es gibt jetzt keine technischen Anforderungen mehr, um sie zu koppeln, also habe ich sie getrennt gelassen.

Dann haben Sie mehr Freiheit mit ihnen - möglicherweise eine oder mehrere untergeordnete Komponenten, um auf Änderungen zu überwachen, aber nicht die Komponente, die mit dem Ziehen begonnen wird. Es hat auch einen zusätzlichen Bonus, dass, wenn Sie keine Monitorabonnements verwenden, dieser Haken baumgeschüttelt werden kann.

Das heißt, dies ist ein erster Entwurf! Ich bin nicht dagegen, es zu ändern, wenn das der Konsens ist!

Das ist ein gutes Argument. Das einzige Problem, das ich sehe, ist, dass Benutzer verwirrt sein können, wenn sie den Monitor direkt anrufen und sich fragen, warum er sich nicht richtig verhält:

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

Wahrscheinlich kann dies mit Dokumentation und Warnungen gelöst werden, wenn Sie die Funktion außerhalb des useMonitor -Hakens aufrufen.

Eigentlich wird das funktionieren! Dies ist eines der Dinge, an denen ich nicht 100% interessiert bin: Der Rückruf in useMonitor wird sowohl für die Änderungserkennung als auch für den Rückgabewert verwendet. Es fühlt sich an, als würde es gegen die aktuellen Haken im React-Kern gehen.

Vielleicht funktioniert so etwas besser,

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

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

Zugegeben, es ist viel einfacher, Fehler mit diesem Formular einzuführen

Ich bin mir der Reaktion und der Interna nicht 100% sicher, aber ist der Monitor nicht da, damit wir die Komponente nicht jedes Mal rendern müssen, wenn sich Ihre Maus bewegt?

Das vorherige würde also nicht mehr funktionieren, wenn Sie das useMonitorSubscription entfernen und nur monitor.isDragging() in der Renderfunktion haben?

Das würde also nicht richtig funktionieren?

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

Der Monitor verfügt über eine subscribe -Methode, die seine Listener benachrichtigt, wenn ein Wert aktualisiert wird. Wir müssen also etwas tun, damit die Komponente weiß, wann sie aktualisiert werden muss.

Wenn wir den vorherigen Beitrag erweitern und die Optimierung der Änderungserkennung zu einer optionalen Funktion machen, kann dies so einfach sein wie:

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

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

Ein paar Ideen.

Können wir zunächst das Argument ref optional machen, indem der Hook eine Implementierung von Ref zurückgibt?

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

Zweitens frage ich mich, ob wir nur Folgendes tun können, anstatt sie dazu zu bringen, einen weiteren Hook in useMonitorUpdates aufzurufen:

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

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

Ich werde dies vorerst schließen, da es eine Kandidaten-API gibt. Fühlen Sie sich frei, dies mit neuen Themen zu kommentieren. Vielen Dank!

Die Hooks-API weist anscheinend einen Konstruktionsfehler auf: https://github.com/Swizec/useDimensions/issues/3

Interessant, daher denke ich, dass eine Alternative darin besteht, dass wir eine Verbindungsfunktion verwenden, wie wir es derzeit in der klassenbasierten API tun:


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

Im Allgemeinen würde die API also so aussehen ...

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

Ich hatte gehofft, nicht mehr die Connector-Funktionen benötigen zu müssen, aber wenn die API ohne sie nicht funktioniert, können wir das schaffen

Während ich dieses Problem lese, sind unsere APIs ähnlich, aber ich denke, das Problem besteht darin, dass sie einen Layouteffekt verwenden, um die Messung eines DOM-Knotens zu erhalten. Wir verwenden hier nicht wirklich Layout-Effekte, sondern registrieren nur DOM-Knoten beim dnd-core.

@gaearon , unsere vorgeschlagene Hooks-API ist der useDimensions-API sehr ähnlich - ist diese bestimmte Form ein Antimuster (z. B. let [props, ref] = useCustomHook(config) ) oder ist sie eigenwillig für das Problem, das die Bibliothek zu lösen versucht?

@darthtrevino Soweit ich weiß, werden wir den registrierten dom-Knoten in dnd-core nicht aktualisieren, wenn Sie den useDragSource-Hook verwenden und den Verweis an eine untergeordnete Komponente weitergeben und die untergeordneten Komponenten erneut rendern:

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. Ich werde später in dieser Woche sehen, ob ich einen Testfall dafür erstellen kann, um zu beweisen, ob er explodiert oder nicht

Wenn es explodiert, ist die Verwendung von Anschlussfunktionen der Fallback

Wenn ich alles richtig gemacht habe, konnte ich es hier reproduzieren: https://codesandbox.io/s/xj7k9x4vp4

Kaboom, gute Arbeit, danke @ k15a . Ich werde die Hooks-API aktualisieren, um bald Verbindungsfunktionen zu verwenden, und Ihr Beispiel als Testfall hinzufügen

Also habe ich letzte Nacht einige Zeit damit verbracht, die Hooks-API zu überarbeiten, um Connector-Funktionen zu verwenden. Was das API-Design angeht, hasse ich es verdammt noch mal.

Mein nächster Gedanke ist, dass wir einen Callback-Ref anstelle eines Ref-Objekts zurückgeben können. Dies sollte uns die Flexibilität geben, direkt als Ref zu verwenden oder einen Ref an ihn weiterzugeben

Direkt verwenden:

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

Verkettung

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

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

Mit Ref-Objekt

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

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

Für mich scheint useLayoutEffect ausgelöst zu werden, wenn die Komponente oder eines ihrer untergeordneten Elemente aktualisiert wird. Wenn ja, könnten wir das einfach nutzen.

Ich habe ein Ticket im React Repo erstellt . Fühlen Sie sich frei zu kommentieren.

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

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

Ich weiß nicht, wie gut das funktioniert, da Sie den Ref bei jedem einzelnen Rendering aufrufen würden. Bei jedem Rendern müssten Sie also eine Verbindung herstellen und trennen.

Wäre es auch nicht besser so?

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

Es wäre dasselbe

Um meinen früheren Kommentar zu vereinfachen, entwickelt sich die API in # 1280 besser, als ich zuerst dachte. Fühlen Sie sich frei, hier oder da zu kommentieren

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen