Tutorial de utilização do React Beautiful DND

1. Começando

Com um projeto iniciado, instalamos o pacote react-beautiful-dnd. Ele possui três componentes que importamos com o seguinte comando:

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

O DragDropContext funciona como um container onde especificamos a área que vamos utilizar para renderizar os outros dois componentes.

Ele possui alguns eventos que podemos utilizar, para isso passamos uma callback para o evento. Por exemplo, o evento onDragStart é chamado quando iniciamos o arrasto de um item, o onDragUpdate, quando temos alguma atualização no momento que o item é arrastado. Temos também o evento onDragEnd que é ==obrigatório== e é chamado quando o arrasto termina.

function App() {
        const [ state, dispatch ] = useReducer(reducer, { tasks });
        const onDragEnd = useCallback((result) => { }, []);
        return (
        <div>
            <DragDropContext
                onDragEnd={ onDragEnd }>
            </DragDropContext>
        </div>
    );
};
export default App;

A callback passada em onDragEnd recebe em result um objeto com diversas informações sobre o item arrastado, como a origem, o destino, se o usuário cancelou o arrasto, etc.

Utilizamos o hook useCallback para evitar renderizações desnecessárias e o hook useReducer para otimizar o gerenciamento quando tivermos mais de uma lista.

2. Droppable

O componente Droppable é o componente que cria a área onde podemos soltar os itens, ele recebe como parâmetro um id em droppableId e um tipo em type. O id é obrigatório e deve ser único para cada Droppable que adicionarmos e o type nós utilizamos para limitar quais itens podem ser aceitos dentro de um Droppable.

Por exemplo, se tivéssemos outro Droppable com o mesmo type, poderíamos arrastar os itens entre os dois componentes.

O componente não cria um elemento HTML, ele espera que o children seja uma função que retorne um componente. Este componente que a função retorna recebe uma ref e droppableProps que por sua vez são repassados por provided.

<Droppable
    droppableId="tasks"
    type="TODO">
        { (provided) => {
            return (
                <div
                    ref={ provided.innerRef }
                    { ...provided.droppableProps }>
                    { provided.placeholder }
                </div>
            );
        } }
</Droppable>                

Além de provided a função pode receber o snapshot como parâmetro, que é um objeto com algumas propriedades que podemos utilizar para estilizar nosso componente.

Por exemplo, se o usuário estiver arrastando um item sobre o Droppable, a propriedade isDraggingOver será definida como true, o que permitiria alterar o fundo da lista.

Por último, precisamos utilizar o componente placeholder que cria um espaço para quando um item estiver sendo arrastado.

3. Draggable

Agora precisamos criar nossa lista de itens arrastáveis. Em nosso exemplo vamos criar ela dentro de Droppable pois vamos arrastar os itens dentro da própria lista.

É importante que nossa estrutura de dados tenha um id único, pois é com este id que vamos identificar os Draggable dentro da propriedade draggableId. O index é utilizado para termos a ordem em que os itens estão organizados dentro da lista.

const data = [
    { id: "1a2b", title: "Primeira Tarefa" },
    { id: "3c4d", title: "Segunda Tarefa" } 
];

Assim como em Droppable criamos uma função que vai retornar o elemento HTML arrastável, que por sua vez recebe da função uma innerRef e as props draggableProps e dragHandleProps.

{ state.tasks?.map((task, index) => {
    return (
        <Draggable
            draggableId={ task.id }
            index={ index }
            key={ task.id }>
            { (provided) => {
                return (
                    <div
                        ref={ provided.innerRef }
                        { ...provided.draggableProps }
                        { ...provided.dragHandleProps }>
                        <div>
                            { task.title }
                        </div>
                    </div>
                );
            } }
        </Draggable>
    );
}) }

Podemos receber também a propriedade isDragging do snapshot para, por exemplo, indicar se o item está sendo arrastado e com isso estilizar o componente.

4. Arrastar e Soltar

Para arrastarmos os itens, primeiro verificamos em nossa callback onDragEnd se houve ou não um cancelamento do usuário na propriedade destination do objeto result, se sim, não fazemos nada.

Caso contrário, fazemos o dispatch enviando os ids e índices do item arrastado.

const onDragEnd = useCallback((result) => {
    if (result.reason === "DROP") return;
    dispatch({
        type: "MOVE",
        from: result.source.droppableId,
        to: result.destination.droppableId,
        fromIndex: result.source.index,
        toIndex: result.destination.index,
    });
}, []);

Depois verificamos em nosso reducer se temos algo em action.to e se action.from e action.to são iguais, com isso sabemos se o item foi arrastado para o mesmo local e, também não fazemos nada.

Senão, fazemos uma cópia da lista para manter a imutabilidade da lista original, o que garante uma renderização mais eficiente, e removemos o item que desejamos mover com o método splice e também com o método splice inserimos o item no local correto. Se desejarmos, poderíamos chamar em nosso reducer uma função para persistirmos os dados.

const reducer = (state, action) => {
        switch (action.type) {
            case "MOVE": {
                if (!action.to) return;
                if (action.to === action.from && 
                action.toIndex === action.fromIndex) return;
                const newState = JSON.parse(JSON.stringify(state.data));
                const [ removeItem ] = newState.splice(action.fromIndex, 1);
                newState.splice(action.toIndex, 0, removeItem);
                return { data: newState };
            }
        };
    };

O método splice adiciona novos elementos em um array enquanto remove os antigos.

5. Código Final

import { useCallback, useReducer } 
from "react";

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

const tasks = [
    { id: "1a2b", title: "Primeira Tarefa" },
    { id: "3c4d", title: "Segunda Tarefa" }
];

const containerStyle = {
    border: "1px solid lightgrey",
    borderRadius: "2px",
    fontFamily: "monospace",
    marginBottom: "8px",
    padding: "8px"
};

const itemStyle = {
    border: "1px solid lightgrey",
    borderRadius: "2px",
    fontFamily: "monospace",
    margin: "8px",
    padding: "8px"
};

function App() {
    const reducer = (state, action) => {
        switch (action.type) {
            case "MOVE": {
                if (!action.to) return; 
                if (action.to === action.from 
                && action.toIndex === action.fromIndex) return;
                const newState = JSON.parse(JSON.stringify(state.tasks));
                const [ removeItem ] = newState.splice(action.fromIndex, 1);
                newState.splice(action.toIndex, 0, removeItem);
                return { tasks: newState };
            }
        };
    };

    const [ state, dispatch ] = useReducer(reducer, { tasks });

    const onDragEnd = useCallback((result) => {
        if (result.reason === "DROP") {
            dispatch({
                type: "MOVE",
                from: result.source.droppableId,
                to: result.destination.droppableId,
                fromIndex: result.source.index,
                toIndex: result.destination.index,
            });
        }
    }, []);

    const DraggableItem = ({ provided, task }) => {
        return (
	      <div
	          ref={provided.innerRef}
	          {...provided.draggableProps}
	          {...provided.dragHandleProps}>
	          <div style={itemStyle}>{task.title}</div>
            </div>
        );
    };

    const DroppableContainer = ({ provided, tasks }) => {
        return (
            <div
                ref={provided.innerRef}
                {...provided.droppableProps}>
                {tasks?.map((task, index) => (
                    <DraggableItem
                        key={task.id}
                        provided={provided}
                        task={task}
                        index={index}
                    />
                ))}
                {provided.placeholder}
            </div>
        );
    };

return (
    <div style={containerStyle}>
        <DragDropContext onDragEnd={onDragEnd}>
            <Droppable
                droppableId='tasks'
                type='TODO'>
                {(provided) => (
                    <DroppableContainer
                        provided={provided}
                        tasks={state.tasks}
                    />
                )}
            </Droppable>
        </DragDropContext>
    </div>
    );
};

export default App;

https://www.lucasmantuan.com.br https://github.com/lucasmantuan https://www.linkedin.com/in/lucasmantuan