В современном веб‑разработке 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.
Заключение и чек‑лист
- Создан atom с эффектом персистентности.
- Настроен DndContext с sensor‑ами.
- Обработчик handleDragEnd корректно меняет порядок.
- Проверена работа localStorage и обновление UI.
- Отсутствуют типичные ошибки: лишние re-renders, дубли id, отсутствие feedback.
