React-dnd: Discussão da API Hooks

Criado em 26 out. 2018  ·  43Comentários  ·  Fonte: react-dnd/react-dnd

Alguém iria perguntar eventualmente! A nova API de ganchos pode ajudar aqui. Acho que a maior parte da API pode permanecer praticamente a mesma, com o mapeamento HOC diretamente em um gancho.

Eu me pergunto se podemos substituir connectDragSource e connectDropTarget apenas passando o valor de useRef . Isso definitivamente poderia deixar as coisas mais limpas, se isso fosse possível!

design decisions discussion

Todos 43 comentários

Mal posso esperar para começar a usar ganchos nesta biblioteca. Assim que os tipos forem preparados para o alfa, podemos configurar um branch de migração

Estou brincando em um branch: experiment / hooks, só para pensar em como essa API pode ser. O componente BoardSquare tem esta aparência:


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

Portanto, a ideia aqui é que createDropTarget configura o conhecimento lógico sobre o item arrastar / soltar, seu ID e predicação, e o gancho useDnd o conectaria ao sistema DnD e coletaria adereços.

Observe que isso é apenas um esboço de um projeto candidato, não está realmente implementado

@darthtrevino Em que branch você está trabalhando? Estou pensando se podemos remover connectDropTarget todos juntos usando refs. Adoraria ver se consigo fazê-lo funcionar no seu ramo!

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 Acho que pareço APIs baseadas em ganchos semelhantes, onde o ref é fornecido e retornado pelo próprio gancho (como const [isOver, canDrop, ref] = useDnd(...) ), pronto para o componente de consumo colocar em sua árvore JSX)

Suponho que isso funcione. Torna mais difícil usar o ref em vários ganchos, mas não há nada que o impeça de escrever algo que combine vários refs em um único ref. Que biblioteca era essa?

Acho que temos que ver qual é a convenção sobre isso!

Torna mais difícil usar o ref em vários ganchos, mas não há nada que o impeça de escrever algo que combina vários refs em um único ref

Verdadeiro e verdadeiro :-)

Que biblioteca era essa?

Não consigo encontrar novamente atm: - / - muitos experimentos em torno de ganchos nas últimas quinze dias ...

https://github.com/beizhedenglong/react-hooks-lib que faz
const { hovered, bind } = useHover(); return <div {...bind}>{hovered ? 'yes' : 'no'}</div>;
o que eu acho que significa que bind inclui uma referência?
[editar: não, inclui apenas { onMouseEnter, onMouseLeave } , é claro ...]

Mas eu me lembro de ter visto alguma outra API retornando um ref direto do gancho.

Não há muito lá e não está sendo construído no momento, mas o branch em que estou é experiment/hooks

Apenas iterando aqui:

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

É assim que o encadeamento de dragSource & dropTarget pode funcionar, se usarmos ref como o primeiro argumento, os argumentos restantes podem conectá-lo a vários conceitos 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>
    )
}

Então useDnd pareceria


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éia maluca, e se não nos importássemos com connect e 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' }} />
}

Acho que você pode se livrar do Connect, mas não tenho certeza sobre o monitor, é de onde você obtém seus adereços como isDragging

então, talvez useDragSource(ref, <type>, <spec>, <collect>) . São muitos argumentos e pode ser estranho ter dois objetos gordos próximos um do outro

Podemos apenas devolver todos os adereços do monitor?

Talvez, eu acho que este é o único método que causaria problemas: https://github.com/react-dnd/react-dnd/blob/84db06edcb94ab3dbb37e8fe89fcad55b1ad07ce/packages/react-dnd/src/interfaces.ts#L117

IIRC, DragSourceMonitor, DropTragetMonitor e DragLayerMonitor são convertidos para a classe DragDropMonitor. Portanto, não acho que teríamos conflitos de nomenclatura, mas eu verificaria isso novamente.

@yched Apenas brincando com isso e ganchos e percebi que temos que passar o ref. Veja o caso quando algo é tanto uma origem quanto um destino,

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

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

@ jacobp100 de fato, faz sentido.

Ok, ideia,

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

Observe que as especificações para a origem e o destino do arrasto não recebem nenhum parâmetro, uma vez que você já tem acesso a eles

useMonitorSubscription pode fazer um igual superficial no objeto para reduzir as atualizações

Dei uma olhada inicial aqui . Não fiz testes, mas o exemplo do xadrez funciona com ganchos - deve mostrar o que quero fazer!

Acho que ter useDragSource(ref, <type>, <spec>, <collect>) é a proposta que funciona muito bem e não traz muitas mudanças de API. A única diferença é que você muda de um hoc para um gancho.

Além disso, ter dois objetos gordos um ao lado do outro não é um grande problema, na minha opinião, porque você também tinha que fazer isso 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>;
};

O bom é que você também pode usar valores de outros 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>;
};

Uma boa vantagem é que podemos remover adereços e componentes dos argumentos beginDrag e todas as outras funções aceitam, pois você já tem acesso a eles no escopo.

^ Acabei de atualizar meu último comentário para mostrar que collectSource não passa do monitor para a função - você acabou de ler do escopo

@ jacobp100 entendo. Para mim, a questão seria se precisamos de outro gancho para coletar dados do monitor ou se poderíamos implementá-lo em useDragSource e useDropTarget também.

Fazia sentido quando as coisas eram HOCs, onde você tinha que conectar as coisas de conexão de qualquer maneira.

Mas agora não há mais requisitos técnicos para acoplá-los, então os deixei separados.

Então você tem mais liberdade com eles - talvez um ou mais componentes filhos para responder às mudanças do monitor, mas não o componente que é arrastado. Ele também tem um bônus adicional de que, se você não estiver usando assinaturas de monitor, esse gancho pode ser abalado.

Dito isso, este é um esboço inicial! Não me oponho a mudá-lo se for esse o consenso!

Esse é um bom argumento. O único problema que vejo é que o usuário pode ficar confuso ao chamar o monitor diretamente e se perguntar por que ele não se comporta corretamente:

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

Provavelmente, isso pode ser resolvido com documentação e avisos quando você chama a função fora do gancho useMonitor .

Na verdade, isso vai funcionar! É uma das coisas que não me interessa 100%: o retorno de chamada em useMonitor é usado tanto para detecção de alterações quanto para o valor de retorno. Parece que está indo contra os ganchos atuais do núcleo do React.

Talvez algo assim funcione melhor,

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

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

Reconhecidamente, é muito mais fácil introduzir bugs com este formulário

Não tenho 100% de certeza sobre os componentes internos de reação-dnd, mas o monitor não está lá, então não precisamos renderizar o componente toda vez que o mouse se move?

Portanto, o anterior pararia de funcionar se você remover o useMonitorSubscription e tiver apenas monitor.isDragging() na função de renderização?

Portanto, isso não funcionaria corretamente?

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

O monitor possui um método subscribe que notifica seus ouvintes sempre que um valor é atualizado. Portanto, precisamos fazer _algo_ para que o componente saiba quando atualizar.

No entanto, expandindo a postagem anterior, se tornarmos a otimização da detecção de alterações um recurso opcional, isso poderia ser tão simples como,

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

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

Algumas idéias.

Primeiro, podemos tornar o argumento ref opcional fazendo com que o gancho retorne uma implementação 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); 

Em segundo lugar, em vez de fazê-los invocar outro gancho em useMonitorUpdates , me pergunto se podemos apenas fazer o seguinte:

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

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

Vou fechar isso por enquanto, pois há uma API candidata. Sinta-se à vontade para comentar sobre isso com novas questões. Obrigado!

Parece que a API de ganchos tem uma falha no design: https://github.com/Swizec/useDimensions/issues/3

Interessante, então acho que uma alternativa é usar uma função de conexão, como fazemos atualmente na API baseada em classe:


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

Portanto, em geral, a API seria semelhante a ...

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

Eu esperava deixar de exigir as funções do conector, mas se a API quebrar sem eles, podemos fazer

Porém, enquanto eu li esse problema - nossas APIs são semelhantes, mas acho que o problema é que elas estão usando um efeito de layout para obter a medição de um nó DOM. Não estamos realmente usando efeitos de layout aqui, apenas registrando os nós DOM com o dnd-core.

@gaearon , nossa API de ganchos proposta é muito semelhante à API useDimensions - essa forma específica é um antipadrão (por exemplo, let [props, ref] = useCustomHook(config) ) ou é idiossincrático para o problema que a biblioteca está tentando resolver?

@darthtrevino Pelo que entendi, se você usar o gancho useDragSource e passar a referência para um componente filho e os renderizadores do componente filho, não atualizaremos o nó dom registrado no 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. Vou ver se consigo fazer um caso de teste para isso ainda esta semana para provar se explode ou não

Se explodir, usar as funções do conector será uma alternativa

Se fiz tudo corretamente, consegui reproduzir aqui: https://codesandbox.io/s/xj7k9x4vp4

Kaboom, bom trabalho, obrigado @ k15a . Vou atualizar a API de ganchos para usar funções de conexão em breve e incluir seu exemplo como um caso de teste

Então, passei muito tempo na noite passada retrabalhando a API de ganchos para usar funções de conector. No que diz respeito ao design da API, eu realmente odeio isso.

Meu próximo pensamento é que podemos retornar um callback-ref em vez de um ref-object. Isso deve nos dar flexibilidade para usar como um ref diretamente ou para passar um ref para ele

Usando diretamente:

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

Encadeamento

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

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

Com Objeto Ref

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

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

Para mim, parece que useLayoutEffect deve disparar se o componente ou qualquer um de seus filhos for atualizado. Se fosse, poderíamos simplesmente usar isso.

Criei um tíquete no repositório de reação . Sinta-se à vontade para comentar.

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

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

Eu não sei o quão bom isso funciona como você chamaria o ref com cada renderização. Assim, com cada renderização, você precisa se conectar e desconectar.

Também não seria melhor ter

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

Seria a mesma coisa

Portanto, para facilitar meu comentário anterior, a API em # 1280 está se moldando para ser melhor do que eu pensava inicialmente. Sinta-se à vontade para comentar aqui ou ali

Esta página foi útil?
0 / 5 - 0 avaliações