Путешествие во времени в React с помощью Immer: Пошаговое руководство
В условиях постоянно меняющегося характера фронтэнд-разработки умение эффективно манипулировать состояниями является важнейшим навыком. Представьте себе, что вы можете перематывать и ускорять изменения состояния вашего приложения, выявляя ошибки и добиваясь более глубокого понимания поведения вашего кода. Добро пожаловать в мир отладки с помощью путешествий во времени.
В этом подробном руководстве мы погрузимся в сферу отладки путешествий во времени в React, используя замечательные возможности Immer. Immer - это мощная библиотека, которая упрощает управление состояниями, позволяя работать с неизменяемыми структурами данных в изменяющей манере. Но этим дело не ограничивается - магия Immer по-настоящему раскрывается в сочетании с отладкой перемещений во времени, предлагая разработчикам потрясающий способ визуализации и отладки изменений состояния во времени.
В этом руководстве мы шаг за шагом расскажем вам, как интегрировать Immer в ваше React-приложение и раскрыть захватывающий потенциал отладки путешествий во времени. Мы рассмотрим такие важные понятия, как неизменяемость, переходы состояний и магию функции Immer's produce. По ходу работы вы увидите, как настроить среду разработки для отладки путешествий во времени, манипулировать снимками состояний и перемещаться по ним, а также воспроизводить прошлые последовательности состояний.
Демонстрация
В представленной демонстрации будет представлено компактное приложение, в котором пользователи смогут создавать, изменять размеры и перемещать поля в интерактивном холсте. Примечательно, что в приложении будет реализована функция отмены действий, позволяющая пользователям легко ориентироваться в своих действиях. Такое гармоничное сочетание функциональных возможностей дает пользователям свободу экспериментов и позволяет без труда вернуться назад или повторить свои действия. Этот интересный опыт продемонстрирует возможности современной фронтэнд-разработки и покажет, как простая на первый взгляд концепция превращается в мощное приложение.
Настройка
На самом деле Immer нам нужен только как обязательная библиотека для этой демонстрации, но я также устанавливаю Theme Ui и react-resizable, чтобы сократить время разработки.
Редуктор
Первое, что нам нужно, - это редуктор, чтобы мы могли отслеживать действия и возвращать желаемые результаты:
import { produceWithPatches, applyPatches } from "immer";
export const boxAction = (draft, action) => {
const { width, height, id, color, position } = action;
let box = draft.boxes[draft.selectBox];
switch (action.type) {
case "ADD_BOX":
draft.boxes[id] = {
id,
width,
height,
color,
position,
};
break;
case "SELECT_BOX":
draft.selectBox = id
break;
case "MOVE_BOX":
if (!box) return;
box.position = position;
break;
case "RESIZE_BOX":
if (!box) return;
box.width = width;
box.height = height;
box.position = position;
break;
case "DELETE":
delete draft.boxes[draft.selectBox];
break;
case "APPLY_PATCHES":
return applyPatches(draft, action.patches);
}
};
Мы можем начать рассматривать каждое действие:
ADD_BOX
: мы добавляем в магазин новое полеSELECT_BOX
: мы выбираем поле (для удаления, изменения размера или перемещения)MOVE_BOX
: мы перемещаем поле в новое положениеRESIZE_BOX
: мы изменяем размеры поляDELETE
: удаляем выделенное полеAPPLY_PATCHES
: для этого мы используем функцию ImmerapplyPatches
. Во время работы продюсера Immer может записать все патчи, которые воспроизведут изменения, сделанные редуктором. Эта функция позволяет нам исправлять состояние
Затем создадим producer
с помощью команды Immer produceWithPatches
и создадим начальное состояние:
export const patchGeneratingBoxesReducer = produceWithPatches(boxAction);
export function getInitialState() {
return {
boxes: {},
};
}
Функция диспетчеризации
Дело вот в чем: нам нужна функция, которая будет вызываться каждый раз, когда мы производим действие, и эта функция должна быть представлена со стеком, поскольку мы будем помещать в стек каждый "патч".
const undoStack = useRef([]);
const undoStackPointer = useRef(-1);
const dispatch = useCallback((action, undoable = true) => {
setState((currentState) => {
const [nextState, patches, inversePatches] = patchGeneratingBoxesReducer(
currentState,
action
);
if (undoable) {
const pointer = ++undoStackPointer.current;
undoStack.current.length = pointer;
undoStack.current[pointer] = { patches, inversePatches };
}
return nextState;
});
}, []);
Не волнуйтесь, я всё подробно объясню:
undoStack
иundoStackPointer
:undoStack
- это ссылка на массив, в котором будет храниться история изменений состояния (патчей) для отменяемых действий.undoStackPointer
- это ссылка на число, которое отслеживает текущую позицию в стеке отмены.- Параметр
undoable
: Логический флаг, указывающий, следует ли считать данное действие отменяемым. - Управление историей отмены: Если действие помечено как отменяемое (
undoable
равно true), то код добавляет патчи и инверсные патчи вundoStack
для потенциальных операций отмены. undoStackPointer
увеличивается и указывает на текущую позицию в стеке.- Предыдущие действия
undoable
за указателем удаляются из стека для сохранения линейной истории.
В целом этот код управляет историей изменений состояния в стеке отмены, позволяя пользователям выполнять отменяемые действия над набором полей, сохраняя при этом возможность отменить эти изменения. Функция диспетчеризации обновляет состояние и соответствующим образом управляет стеком отмены.
Кнопки
В этом приложении у нас будет 4 кнопки: Create - Delete - Undo - Redo (Создать - Удалить - Отмена - Повтор). Давайте разберемся с каждой из них:
Кнопка "Создать":
const createButton = () => {
const width = Math.floor(Math.random() * (300 - 100 + 1) + 100)
const height = Math.floor(Math.random() * (300 - 100 + 1) + 100)
dispatch({
type: "ADD_BOX",
width: width,
height: height,
id: uuidv4(),
color:
`#` +
Math.floor(16777215 * Math.random()).toString(16),
position: {
x: window.innerWidth * 0.8 / 2 - width / 2,
y: window.innerHeight / 2 - height / 2,
}
});
};
При создании нового поля можно обратить внимание на то, что я ввел произвольность в его размеры по ширине и высоте, а также придал ему характерный оттенок и единственный в своем роде идентификатор. Сформированное поле продуманно размещается в центре экрана путем умелого манипулирования координатами осей x и y. Кульминацией всех этих действий является вызов вышеупомянутой функции диспетчеризации, что позволяет воплотить задуманное в жизнь.
Кнопка "Удалить":
const deleteButton = () => {
dispatch({
type: "DELETE",
});
dispatch({
type: "SELECT_BOX",
id: null
}, false)
}
В данном контексте нет необходимости подробно останавливаться на этом, мы просто инициируем диспетчерское действие удаления, а затем убеждаемся, что поле не выбрано.
Кнопки "Отмена" и "Повтор":
const undoButton = () => {
if (undoStackPointer.current < 0) return;
const patches = undoStack.current[undoStackPointer.current].inversePatches;
dispatch({ type: "APPLY_PATCHES", patches }, false);
undoStackPointer.current--;
dispatch({
type: "SELECT_BOX",
id: null
}, false)
};
const redoButton = () => {
if (undoStackPointer.current === undoStack.current.length - 1) return;
undoStackPointer.current++;
const patches = undoStack.current[undoStackPointer.current].patches;
dispatch({ type: "APPLY_PATCHES", patches }, false);
dispatch({
type: "SELECT_BOX",
id: null
}, false)
};
Я поместил эти две функции вместе, чтобы вы могли увидеть контраст. Мы не позволяем пользователям отменять действия, если они находятся в нижней части стека, и не позволяем пользователям повторять действия, если они находятся в верхней части стека. Затем мы получаем патчи и применяем их с помощью действия "APPLY_PATCHES". Не забудьте снять флажок, чтобы избежать ошибок.
Результаты
Заключение
К концу этого урока вы не только поймете, как реализовать путешествие во времени с помощью Immer в React, но и овладеете мощной техникой отладки, способной значительно улучшить ваш процесс разработки. Присоединяйтесь к нам, чтобы раскрыть секреты путешествий во времени и изменить подход к отладке в приложениях React.
Исходный код: https://github.com/superdev163/immer-square
Живая демонстрация: https://immer-square.web.app/