Artigo original escrito por Colby Fayock
Artigo original: How to Add Drag and Drop in React with React Beautiful DnD
Traduzido e adaptado por Thiago Giammattey

Arrastar e soltar (em inglês, "Drag and Drop") é uma técnica de interação comum, que permite que as pessoas possam intuitivamente mudar as coisas de lugar na página. Pode ter a ver com reordenar uma lista ou até mesmo montar um quebra-cabeças.

Como podemos adicionar esse tipo de interação ao construir um aplicação React com a biblioteca React Beautiful DnD?

O que é arrastar e soltar?

Arrastar e soltar é, basicamente, o que o próprio nome indica – uma interação que permite a alguém clicar sobre um item, arrastá-lo e, em seguida, soltá-lo em outro lugar, geralmente causando um efeito colateral no aplicativo.

react-beautiful-dnd-board
Movimentação dos itens em um quadro

Esse efeito é bastante comum em aplicativos como listas de tarefas ou painel de gerenciamento de projetos, onde você precisa estabelecer prioridades e criar uma ordem para como as coisas devem ser feitas.

Embora a técnica de "arrastar e soltar" possa ter alguns casos de uso avançados, nos limitaremos à funcionalidade de lista básica em nosso passo a passo.

O que é o React Beautiful DnD?

O React Beautiful DnD é uma biblioteca de arrastar e soltar acessível desenvolvida pela Atlassian.  Se você não conhece a Atlassian, eles são a equipe por trás do Jira. Se você não está familiarizado com o Jira, ele é provavelmente a maior ferramenta de gestão ágil na internet atualmente.

Os objetivos da equipe de desenvolvimento da Atlassian eram fornecer recursos de arrastar e soltar tendo a acessibilidade em mente, além de manter a biblioteca enxuta, com um ótimo desempenho e uma API poderosa.

O que vamos construir?

Vamos começar com uma simples lista e adicionar a ela a funcionalidade de arrastar e soltar.

Neste passo a passo, não perderemos tempo construindo a própria lista.  A lista que usaremos é composta de uma tag de lista não ordenada padrão (<ul>) e tags de itens de lista (<li>), com um pouco de CSS para deixá-los parecidos com cartões.

Vamos nos concentrar em adicionar a funcionalidade de arrastar e soltar para reorganizar a lista usando o React Beautiful DnD.

Passo 0: criar um novo projeto do React.js

Para começar, queremos um aplicativo simples, que inclua uma lista de itens. Pode ser um projeto já existente ou um novo projeto usando seu framework preferido, como, por exemplo, o Create React App.

Comecei com um novo projeto usando o Create React App e adicionei a ele uma lista simples de personagens de Final Space.

list-of-final-space-characters-1
Lista de personagens de Final Space

Se você quiser começar do mesmo lugar, pode clonar meu repositório de demonstração na branch específica e começar desde o início comigo.

Este comando clonará a branch específica para começarmos:

git clone --single-branch --branch part-0-starting-point git@github.com:colbyfayock/my-final-space-characters.git

Caso contrário, você pode clonar o repositório normalmente e passar direto para a branch part-0-starting-point.

Se você quiser acompanhar apenas o código, primeiramente, eu criei um array de objetos:

const finalSpaceCharacters = [
  {
    id: 'gary',
    name: 'Gary Goodspeed',
    thumb: '/images/gary.png'
  },
  ...

Então, eu percorri esses objetos com um laço para criar minha lista:

<ul className="characters">
  {finalSpaceCharacters.map(({id, name, thumb}) => {
    return (
      <li key={id}>
        <div className="characters-thumb">
          <img src={thumb} alt={`${name} Thumb`} />
        </div>
        <p>
          { name }
        </p>
      </li>
    );
  })}
</ul>

Siga junto comigo pelo commit!

Passo 1: instalar o React Beautiful DnD

O primeiro passo é instalar a biblioteca via npm.

De dentro do projeto, execute o seguinte comando:

yarn add react-beautiful-dnd
# or
npm install react-beautiful-dnd --save

Isso adicionará a biblioteca ao projeto e estaremos prontos para usá-la na nossa aplicação.

Passo 2: permitir o arrastar e soltar de uma lista com o React Beautiful DnD

Com a biblioteca instalada, nós podemos dar a nossa lista a habilidade de ser arrastada pela página e largada em outro lugar.

Adicionar o contexto de arrastar e soltar ao nosso app

Na parte superior do arquivo, importe DragDropContext da biblioteca com:

import { DragDropContext } from 'react-beautiful-dnd';

DragDropContext vai dar ao nosso app a habilidade de utilizar a biblioteca. Funciona de maneira parecida à API Context do React, na qual a biblioteca ganha acesso à árvore de componentes.

Observação: se você planeja adicionar a funcionalidade de arrastar e soltar a mais de uma lista, precisa garantir que o DragDropContext englobe todos esses itens, como, por exemplo, a raiz de sua aplicação. Você não pode aninhar o DragDropContext.

Queremos envolver nossa lista com o DragDropContext:

<DragDropContext>
  <ul className="characters">
  ...
</DragDropContext>

Até aqui, nada terá mudado na aplicação e ela deve carregar como antes.

Permitir arrastar os itens de nossa lista

Em seguida, queremos criar uma área onde podemos arrastar os itens (em inglês, uma área Droppable). Isso nos permitirá prover uma área específica onde nossos itens poderão ser movidos de lugar dentro desse limite.

Primeiramente, adicionamos Droppable à importação na parte superior do arquivo:

import { DragDropContext, Droppable } from 'react-beautiful-dnd';

Para nosso objetivo, queremos que nossa lista não ordenada inteira (<ul>) fique dentro da nossa "drop zone" (a zona onde soltaremos os itens). Assim, precisamos envolvê-la com esse componente:

<DragDropContext>
  <Droppable droppableId="characters">
    {(provided) => (
      <ul className="characters">
        ...
      </ul>
    )}
  </Droppable>
</DragDropContext>

Você notará, porém, que envolvemos a lista de um modo um pouco diferente. Primeiro, definimos um droppableId para nosso componente <Droppable>. Isso permite à biblioteca acompanhar essa instância específica entre as interações.

Também estamos criando uma função diretamente naquele componente, que passa o argumento provided (fornecido, em inglês).

Observação: essa função pode passar dois argumentos, incluindo um argumento snapshot, mas não o usaremos nesse exemplo.

O argumento provided (texto em inglês) inclui informações e referências ao código de que a biblioteca necessita para funcionar adequadamente.

Para usá-lo, em nosso elemento de lista, adicionamos o seguinte:

<ul className="characters" {...provided.droppableProps} ref={provided.innerRef}>

Isso criará uma referência (provided.innerRef) para que a biblioteca acesse o HTML dos elementos da lista. Também aplicará props ao elemento (provided.droppableProps) que permitem a biblioteca para acompanhar os movimentos e o posicionamento.

Novamente, nesse ponto, ainda não haverá uma funcionalidade perceptível.

Permitir que nossos itens sejam arrastáveis

Agora, vamos para a parte divertida!

A última parte para permitir arrastar e soltar nossos elementos da lista é envolver cada item da lista com um componente de modo semelhante ao que fizemos com toda a lista.

Usaremos o componente Draggable (texto do link em inglês), que, mais uma vez, de modo semelhante ao componente Droppable, incluirá uma função na qual passaremos props aos componentes do item de lista.

Primeiro, precisamos importar Draggable, juntamente com o resto dos componentes.

import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';

Em seguida, dentro de nosso laço, envolvemos o item de lista retornado com o componente <Draggable /> e sua função de nível superior.

{finalSpaceCharacters.map(({id, name, thumb}) => {
  return (
    <Draggable>
      {(provided) => (
        <li key={id}>
          ...
        </li>
      )}
    </Draggable>

Como agora temos um novo componente de nível superior em nosso laço, vamos mover a prop key do elemento da lista para Draggable:

{finalSpaceCharacters.map(({id, name, thumb}) => {
  return (
    <Draggable key={id}>
      {(provided) => (
        <li>

Também precisamos definir duas props adicionais em <Draggable>, um draggableId e um index.

Queremos adicionar esse index (índice, em inglês) como um argumento para nossa função map e incluir essas props ao nosso componente:

{finalSpaceCharacters.map(({id, name, thumb}, index) => {
  return (
    <Draggable key={id} draggableId={id} index={index}>

Por fim, precisamos definir algumas props na própria lista de elementos.

No nosso elemento de lista, adicionaremos essa ref e faremos o spreading das props adicionais do argumento provided:

<Draggable key={id} draggableId={id} index={index}>
  {(provided) => (
    <li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>

Depois, se atualizarmos a página e passarmos o mouse por nossos itens da lista, veremos que é possível arrastá-los!

drag-items-revert-state-1
Arrastando itens em uma lista

Entretanto, você perceberá que, ao começar a mover um item, a parte inferior da página parecerá um pouco estranha. Parece haver uma questão com relação ao vazamento (em inglês, overflowing) dos itens da lista sobre o rodapé.

Você também perceberá que, no console do desenvolvedor, o React Beautiful DnD está nos passando uma mensagem de aviso, dizendo que está faltando algo, chamado placeholder.

react-beautiful-dnd-warning-placeholder
Aviso do console - não foi possível encontrar o placeholder

Adicionar o placeholder do React Beautiful DnD

Parte dos requisitos do React Beautiful DnD é a adição de um item chamado placeholder.

Isso é algo fornecido na instalação e é usado para preencher o espaço ocupado anteriormente pelo item que agora estamos arrastando.

Para adicioná-lo, incluiremos provided.placeholder na parte inferior de nosso componente de lista de nível superior "Droppable" - em nosso caso, na parte inferior do elemento <ul>:

<Droppable droppableId="characters">
  {(provided) => (
    <ul className="characters" {...provided.droppableProps} ref={provided.innerRef}>
      ...
      {provided.placeholder}
    </ul>
  )}
</Droppable>

Agora, se começarmos a arrastar os itens em nosso navegador, veremos que o fluxo da página não terá mais problemas e que o conteúdo ficará onde deve estar!

drag-items-revert-fixed-spacing
Arrastando os itens após consertar a questão do espaçamento

O último problema, no entanto, está no fato de, ao mover algum dos itens, ele não permanecer no lugar em que o soltamos. Como, então, salvar a ordem dos nossos itens?

Siga junto comigo pelo commit!

Passo 3: salvar a nova ordem da lista após ter reordenado os itens com o React Beautiful DnD

Quando movemos nossos itens, eles permanecem onde estão por uma fração de segundo e, assim que o React Beautiful DnD termina de fazer seu trabalho, a árvore de componentes renderizará novamente.

Depois da nova renderização dos componentes, os itens retornam ao mesmo lugar onde estavam, pois nunca foram salvos como estavam fora da memória do DnD.

Para resolver isso, o DragDropContext recebe uma prop onDragEnd, que nos permitirá disparar uma função após termos concluído o processo de arrastar. A função passa argumentos que incluem a nova ordem dos nossos itens para podermos atualizar nosso estado para o próximo ciclo de renderização.

Salvar nossos itens da lista no state

Primeiro, vamos armazenar nossos itens no state, para termos algo para atualizar entre os ciclos.

Na parte superior do arquivo, adicionamos useState ao import do React:

import React, { useState } from 'react';

Em seguida, criaremos nosso state usando nossa lista de itens padrão.

Adicione o seguinte à parte superior do nosso componente App:

const [characters, updateCharacters] = useState(finalSpaceCharacters);

Como atualizaremos nosso novo state, characters, fornecendo nossos itens da lista e sua ordem, queremos substituir o array que mapeamos inicialmente por nosso novo state:

<ul className="characters" {...provided.droppableProps} ref={provided.innerRef}>
  {characters(({id, name, thumb}, index) => {

Se salvarmos e atualizarmos nossa página, nada deve mudar!

Atualizar o state ao arrastar os itens

Agora que temos nosso state, podemos atualizar o state sempre que itens da lista forem arrastados.

O componente DragDropContext que adicionamos à nossa página recebe uma prop onDragEnd. Conforme o nome diz (ao final do processo de arrastar, em inglês), essa prop disparará uma função sempre que alguém parar de arrastar um item na lista.

Vamos adicionar uma função handleOnDragEnd como nossa prop:

<DragDropContext onDragEnd={handleOnDragEnd}>

Em seguida, precisamos que aquela função exista de fato.

Podemos definir uma função em nosso state:

function handleOnDragEnd(result) {
}

Nossa função recebe um argumento chamado result.

Se adicionarmos console.log(result) à função e movermos um item em nossa lista, veremos que ela inclui detalhes sobre o que deve ser o state atualizado após nossa ação de movimentação.

react-beautiful-dnd-ondragend-result
Resultado após arrastar o item

Especificamente, queremos usar o valor de index nas propriedades destination e source, que nos informam o índice do item que está sendo movido e qual deve ser o novo índice daquele item no array de itens.

Usando-o, adicionamos o seguinte à nossa função:

const items = Array.from(characters);
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);

updateCharacters(items);

O que estamos fazendo é:

  • Criar uma cópia de nosso array characters
  • Usar o valor source.index para encontrar nosso item do novo array e removê-lo usando o método splice
  • O resultado é desestruturado, assim, temos um novo objeto reorderedItem, que é o nosso item arrastado
  • Usar o destination.index para adicionar o item novamente no array, mas em seu novo local, usando splice novamente
  • Por fim, atualizar o state characters com a função updateCharacters. Agora, após salvar nossa função, podemos mover nossos personagens e eles são salvos no novo local!
drag-drop-save-state
Arrastando um item com o state salvo

Evitando o erro de arrastar para fora dos limites

Uma pequena questão a resolver com nossa implementação é o que ocorre quando o item não é arrastado exatamente para dentro do contêiner definido. Quando isso ocorre, temos um erro.

drag-out-of-container-error
Erro ao arrastar o item para fora do contêiner

O problema é que, ao fazermos isso, não temos um local de destino.

Para evitar isso, podemos simplesmente adicionar uma instrução acima do código que move nosso item para verificar se o destino existe. Se não existir, saímos da função:

function handleOnDragEnd(result) {
  if (!result.destination) return;

Se recarregarmos a página e tentarmos arrastar nosso item para fora do contêiner novamente, ele retornará ao local original sem o erro!

drag-out-of-container-snap-back
O item retorna ao local de origem quando arrastado para fora do contêiner

Siga junto comigo pelo commit!

O que mais podemos fazer com o React Beautiful DnD?

Estilos personalizados ao arrastar e soltar

Ao mover os itens, o DnD fornecerá um snapshot do state dado. Com essas informações, podemos aplicar estilos personalizados, de modo que, quando movermos nossos itens, poderemos mostrar um state ativo para a lista, para o item que movemos, ou ambos!

https://react-beautiful-dnd.netlify.app/?path=/story/single-vertical-list--basic

Arrastar entre listas diferentes

Se você já usou o Trello anteriormente, ou uma ferramenta semelhante, deve estar familiarizado com o conceito de colunas diferentes entre as quais você pode arrastar cartões, de modo a priorizar e organizar suas tarefas e ideias.

Esta biblioteca permite a você fazer isso, dando a capacidade de arrastar e soltar itens de uma área dentro da qual um item pode ser arrastado para outra área do mesmo tipo.

https://react-beautiful-dnd.netlify.app/?path=/story/multiple-vertical-lists--stress-test