React-dnd: Diskusi Hooks API

Dibuat pada 26 Okt 2018  ·  43Komentar  ·  Sumber: react-dnd/react-dnd

Seseorang pada akhirnya akan bertanya! API kait baru mungkin bisa membantu di sini. Saya pikir sebagian besar API bisa tetap sama, dengan pemetaan HOC langsung ke hook.

Saya bertanya-tanya apakah kita dapat mengganti connectDragSource dan connectDropTarget hanya dengan meneruskan nilai useRef . Itu pasti bisa membuat barang lebih bersih jika itu memungkinkan!

design decisions discussion

Semua 43 komentar

Saya tidak sabar untuk mulai menggunakan pengait di perpustakaan ini. Segera setelah jenis dibuat untuk alfa, kita dapat menyiapkan cabang migrasi

Saya bermain-main di cabang: percobaan / kait, hanya untuk memikirkan bagaimana API ini akan terlihat. Komponen BoardSquare terlihat seperti ini:


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

Jadi idenya di sini adalah bahwa createDropTarget menyiapkan pengetahuan logis tentang item drag / drop, ID dan predikasinya, dan hook useDnd akan menyambungkannya ke sistem DnD dan mengumpulkan props.

Perhatikan bahwa ini hanya membuat sketsa desain kandidat, itu tidak benar-benar diterapkan

@darthtrevino Cabang mana yang Anda kerjakan? Saya bertanya-tanya apakah kita bisa menghapus connectDropTarget semua bersama-sama menggunakan referensi. Saya ingin melihat apakah saya bisa membuatnya berfungsi di cabang Anda!

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 Saya rasa saya telah melihat API berbasis hook serupa di mana ref disediakan dan dikembalikan oleh hook itu sendiri (seperti const [isOver, canDrop, ref] = useDnd(...) ), siap untuk komponen konsumsi ditempatkan di pohon JSX-nya)

Saya kira itu berhasil. Itu membuat lebih sulit untuk menggunakan referensi dalam beberapa kait, tetapi tidak ada yang menghentikan Anda menulis sesuatu yang menggabungkan beberapa referensi menjadi satu referensi. Perpustakaan apa ini?

Sepertinya kita harus melihat konvensi tentang ini!

Ini membuat lebih sulit untuk menggunakan referensi dalam banyak kait, tetapi tidak ada yang menghentikan Anda menulis sesuatu yang menggabungkan beberapa referensi menjadi satu referensi

Benar, dan benar :-)

Perpustakaan apa ini?

Sepertinya tidak dapat menemukannya lagi atm: - / - banyak percobaan seputar kait dalam dua minggu terakhir ...

Ada https://github.com/beizhedenglong/react-hooks-lib yang melakukannya
const { hovered, bind } = useHover(); return <div {...bind}>{hovered ? 'yes' : 'no'}</div>;
yang saya kira berarti bind termasuk referensi?
[edit: tidak, itu hanya mencakup { onMouseEnter, onMouseLeave } , tentu saja ...]

Tapi saya ingat melihat beberapa API lain mengembalikan ref langsung dari hook.

Tidak banyak di sana, dan tidak sedang dibangun saat ini, tetapi cabang tempat saya berada adalah experiment/hooks

Hanya mengulang di sini:

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

Ini adalah cara kerja rangkaian dragSource & dropTarget, jika kita menggunakan ref sebagai argumen pertama, argumen lainnya dapat menghubungkannya ke beberapa konsep 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>
    )
}

Jadi useDnd akan terlihat seperti


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

Ide gila, bagaimana jika kita tidak peduli dengan connect dan 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' }} />
}

Saya pikir Anda dapat menyingkirkan koneksi, tetapi saya tidak yakin tentang monitor, di situlah Anda mendapatkan alat peraga seperti isDragging from

jadi mungkin useDragSource(ref, <type>, <spec>, <collect>) . Itu argumen yang banyak, dan mungkin aneh untuk memiliki dua benda gemuk di samping satu sama lain

Bisakah kita mengembalikan semua props dari monitor?

Mungkin, saya pikir ini adalah satu-satunya metode yang akan menyebabkan masalah: https://github.com/react-dnd/react-dnd/blob/84db06edcb94ab3dbb37e8fe89fcad55b1ad07ce/packages/react-dnd/src/interfaces.ts#L117

IIRC, DragSourceMonitor, DropTragetMonitor, dan DragLayerMonitor semuanya turun ke kelas DragDropMonitor. Jadi saya tidak berpikir kita akan mengalami tabrakan penamaan, tapi saya akan memeriksa ulang itu.

@yched Hanya bermain-main dengan ini dan kait dan perhatikan bahwa kita harus meneruskan wasit. Lihat kasus ketika sesuatu merupakan sumber dan target,

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

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

@ jacobp100 memang, masuk akal.

Oke, ide,

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

Perhatikan bahwa spesifikasi untuk sumber dan target seret tidak menerima parameter apa pun, karena Anda sudah memiliki akses ke sana

useMonitorSubscription dapat melakukan hal yang sama pada objek untuk mengurangi pembaruan

Saya telah melihat awal di sini . Tidak mendapat tes, tetapi contoh catur bekerja dengan kait - harus menunjukkan apa yang ingin saya lakukan!

Saya pikir memiliki useDragSource(ref, <type>, <spec>, <collect>) adalah proposal yang bekerja sangat bagus dan tidak membawa banyak perubahan api. Satu-satunya perbedaan adalah Anda berubah dari hoc menjadi hook.

Juga memiliki dua benda gemuk di samping satu sama lain bukanlah masalah besar menurut saya karena Anda juga harus melakukannya sebelumnya:

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

Hal yang menyenangkan adalah Anda dapat menggunakan nilai dari hook lain juga.

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

Satu keuntungan bagus adalah kita bisa menghapus props dan komponen dari argumen beginDrag dan semua fungsi lainnya menerima karena Anda sudah memiliki akses ke mereka di lingkup.

^ Saya baru saja memperbarui komentar terakhir saya untuk menunjukkan bahwa collectSource tidak meneruskan fungsi di monitor - Anda baru saja membaca dari ruang lingkup

@ jobp100 Saya mengerti. Bagi saya, pertanyaannya adalah apakah kita memerlukan pengait lain untuk mengumpulkan data dari monitor atau apakah kita bisa menerapkannya di useDragSource dan useDropTarget juga.

Masuk akal ketika barang-barang itu HOC, di mana Anda harus menghubungkan barang-barang yang terhubung.

Tapi sekarang tidak ada persyaratan teknis untuk memasangkannya lagi, jadi saya biarkan terpisah.

Kemudian Anda memiliki lebih banyak kebebasan dengannya - mungkin satu atau lebih komponen turunan untuk merespons perubahan monitor, tetapi bukan komponen yang mulai diseret. Ini juga memiliki bonus tambahan bahwa jika Anda tidak menggunakan langganan monitor, hook itu dapat terguncang.

Meskipun demikian, ini adalah draf awal! Saya tidak menentang untuk mengubahnya jika itu adalah konsensus!

Itu argumen yang bagus. Satu-satunya masalah yang saya lihat adalah pengguna dapat menjadi bingung ketika mereka memanggil monitor secara langsung dan bertanya-tanya mengapa monitor tidak berfungsi dengan benar:

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

Mungkin itu bisa diselesaikan dengan dokumentasi dan peringatan ketika Anda memanggil fungsi di luar hook useMonitor .

Sebenarnya, itu akan berhasil! Ini salah satu hal yang tidak 100% saya sukai: callback dalam useMonitor digunakan baik untuk deteksi perubahan dan nilai pengembalian. Rasanya seperti melawan hook saat ini di inti React.

Mungkin sesuatu seperti ini bekerja lebih baik,

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

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

Memang, jauh lebih mudah untuk memperkenalkan bug dengan formulir ini

Saya tidak 100% yakin dengan internal react-dnd tetapi bukankah monitornya ada di sana sehingga kita tidak perlu membuat komponen setiap kali mouse Anda bergerak?

Jadi yang sebelumnya akan berhenti berfungsi jika Anda menghapus useMonitorSubscription dan hanya memiliki monitor.isDragging() dalam fungsi render?

Karena itu, ini tidak akan berfungsi dengan benar?

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

Monitor memiliki metode subscribe yang memberi tahu pendengarnya setiap kali ada nilai yang diperbarui. Jadi kita perlu melakukan _something_ agar komponen tahu kapan harus mengupdate.

Memperluas posting sebelumnya, jika kita membuat deteksi perubahan pengoptimalan sebagai fitur opsional, ini bisa sesederhana,

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

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

Beberapa ide.

Pertama, dapatkah kita membuat argumen ref opsional dengan meminta hook mengembalikan implementasi 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); 

Kedua, daripada membuat mereka memanggil pengait lain di useMonitorUpdates , saya bertanya-tanya apakah kita bisa melakukan hal berikut:

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

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

Saya akan menutup ini untuk saat ini, karena ada API kandidat. Jangan ragu untuk mengomentarinya dengan masalah baru. Terima kasih!

Sepertinya API kait memiliki cacat dalam desain: https://github.com/Swizec/useDimensions/issues/3

Menarik, jadi saya kira alternatifnya adalah kami menggunakan fungsi koneksi, seperti yang saat ini kami lakukan di API berbasis kelas:


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

Jadi secara umum, API akan terlihat seperti ...

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

Saya agak berharap untuk tidak memerlukan fungsi konektor, tetapi jika API rusak tanpanya maka kita dapat melakukannya

Ketika saya membaca masalah itu - API kami serupa, tetapi saya pikir masalahnya adalah mereka menggunakan efek tata letak untuk mendapatkan pengukuran simpul DOM. Kami tidak benar-benar menggunakan efek tata letak di sini, hanya mendaftarkan simpul DOM dengan dnd-core.

@gaearon , API hook yang kami usulkan sangat mirip dengan API useDimensions - apakah bentuk spesifik itu antipattern (misalnya let [props, ref] = useCustomHook(config) ), atau apakah itu istimewa untuk masalah yang coba dipecahkan oleh perpustakaan?

@darthtrevino Sejauh yang saya mengerti jika Anda menggunakan hook useDragSource dan Anda meneruskan ref ke komponen anak dan rendering komponen anak, kami tidak akan memperbarui node dom terdaftar di 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. Saya akan melihat apakah saya dapat membuat kasus uji untuk ini akhir minggu ini untuk membuktikan apakah itu meledak atau tidak

Jika meledak, menggunakan fungsi konektor akan menjadi fallback

Jika saya melakukan semuanya dengan benar, saya dapat mereproduksinya di sini: https://codesandbox.io/s/xj7k9x4vp4

Kaboom, kerja bagus, terima kasih @ k15a . Saya akan memperbarui API hooks untuk segera menggunakan fungsi koneksi dan menyertakan contoh Anda sebagai kasus uji

Jadi saya menghabiskan banyak waktu tadi malam mengerjakan ulang API kait untuk menggunakan fungsi konektor. Sejauh desain API berjalan, saya benar-benar membencinya.

Pikiran saya selanjutnya adalah kita bisa mengembalikan callback-ref daripada objek ref. Ini akan memberi kita fleksibilitas untuk digunakan sebagai wasit secara langsung atau untuk memberikan wasit ke dalamnya

Menggunakan Langsung:

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

Merantai

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

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

Dengan Objek Ref

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

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

Bagi saya, rasanya useLayoutEffect harus diaktifkan jika komponen atau turunannya diperbarui. Jika ya, kita bisa menggunakan itu.

Saya telah membuat tiket di repo react . Jangan ragu untuk berkomentar.

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

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

Saya tidak tahu seberapa bagus ini bekerja karena Anda akan memanggil wasit dengan setiap render. Jadi dengan setiap render, Anda perlu menghubungkan dan memutuskan sambungan.

Juga bukankah lebih baik untuk memilikinya

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

Itu akan menjadi hal yang sama

Jadi, untuk memudahkan komentar saya sebelumnya, API di # 1280 sedang dibentuk menjadi lebih baik dari yang saya pikirkan sebelumnya. Jangan ragu untuk berkomentar di sini atau di sana

Apakah halaman ini membantu?
0 / 5 - 0 peringkat