В современном веб‑разработке drag & drop списки задач React Recoil становятся стандартом для интерактивных приложений. Когда пользователь перемещает карточки, порядок должен сохраняться не только в UI, но и в глобальном состоянии. Recoil предлагает простую модель атомов, позволяя сохранять массив задач и обновлять его после каждой операции перетаскивания. В этой статье мы разберём, как реализовать drag & drop со персистентностью без лишней сложности.

Разбор задачи

Нужно создать компонент списка задач, где каждая задача – карточка, которую можно перемещать мышью. После перемещения порядок карточек должен сохраняться в Recoil‑атоме. Кроме того, при перезагрузке страницы порядок должен восстанавливаться из локального хранилища. Это подразумевает три ключевых требования: реактивность UI, синхронизация с Recoil и персистентность в браузере.

Пошаговое решение

Шаг 1. Создайте атом Recoil для хранения массива задач. Шаг 2. Используйте библиотеку react‑dnd или @dnd-kit/core для реализации drag & drop. Шаг 3. В колбеке onDragEnd обновляйте атом, переставляя элементы в соответствии с новым индексом. Шаг 4. Подпишитесь на изменения атома и сохраняйте его в localStorage, чтобы при загрузке страницы восстанавливать порядок. Следуя этим шагам, вы получите полностью работающий, персистентный drag & drop список задач в React с Recoil.

Рабочий код

import {atom, selector, useRecoilState, useRecoilValue} from 'recoil';
import {DndContext, closestCenter, useSensor, useSensors, PointerSensor, KeyboardSensor, useDroppable} from '@dnd-kit/core';
import {arrayMove} from '@dnd-kit/sortable';

// 1. Хранилище списка задач
const tasksState = atom({
  key: 'tasksState',
  default: [],
  // Восстанавливаем из localStorage
  effects_UNSTABLE: [({setSelf, onSet}) => {
    const stored = localStorage.getItem('tasks');
    if (stored) setSelf(JSON.parse(stored));
    onSet(newValue => localStorage.setItem('tasks', JSON.stringify(newValue)), false);
  }],
});

// 2. Компонент TaskItem
const TaskItem = React.forwardRef(({id, content}, ref) => {
  const {isOver, setNodeRef} = useDroppable({id});
  return (
    
{content}
); }); // 3. Компонент TaskList const TaskList = () => { const [tasks, setTasks] = useRecoilState(tasksState); const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, {coordinateGetter: null}) ); const handleDragEnd = ({active, over}) => { if (!over) return; const oldIndex = tasks.findIndex(t => t.id === active.id); const newIndex = tasks.findIndex(t => t.id === over.id); if (oldIndex !== newIndex) { setTasks(arrayMove(tasks, oldIndex, newIndex)); } }; return ( {tasks.map(task => ( ))} ); }; export default TaskList; // подключаем в App

Типичные ошибки

  • Не обновляется localStorage после перетаскивания – забыли useRecoilEffect.
  • Проблемы с re-render – используете массивы в selector без memoization.
  • Плохая UX из‑за отсутствия visual feedback – не добавили isOver стили.

FAQ

Можно ли использовать Recoil без localStorage?

Да, можно хранить порядок только в состоянии. Для персистентности примените atomEffect, но без него порядок исчезнет после перезагрузки.

Как обрабатывать дублирующиеся id?

Проверьте генерацию id в момент создания задачи. Лучше использовать uuid или nanoid, чтобы избежать конфликтов.

Что делать, если drag & drop тормозит на больших списках?

Разбейте список на под‑компоненты, используйте React.memo, а также оптимизируйте collisionDetection, применяя custom strategy.

Заключение и чек‑лист

  1. Создан atom с эффектом персистентности.
  2. Настроен DndContext с sensor‑ами.
  3. Обработчик handleDragEnd корректно меняет порядок.
  4. Проверена работа localStorage и обновление UI.
  5. Отсутствуют типичные ошибки: лишние re-renders, дубли id, отсутствие feedback.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *