# How TREK's Undo Feature Tracks and Reverses User Actions in the Trip Planner

> Explore how TREK's undo feature tracks and reverses user actions in the trip planner. Discover the LIFO history stack and callback mechanism within the usePlannerHistory hook.

- Repository: [Maurice/TREK](https://github.com/mauriceboe/TREK)
- Tags: internals
- Published: 2026-07-01

---

**TREK implements trip planner undo functionality by maintaining a LIFO history stack inside the custom `usePlannerHistory` React hook, where each entry stores a descriptive label and an executable callback that reverses the specific user action.**

TREK is an open-source trip planning application that requires robust state management for complex user interactions like adding places, reordering itineraries, and modifying day assignments. Rather than implementing a centralized command pattern, the codebase uses a lightweight, decoupled approach to track and reverse user actions through a custom React hook that maintains an in-memory stack of reversible operations.

## Core Architecture of the Undo System

The undo mechanism centers on a custom hook called `usePlannerHistory`, defined in [`client/src/hooks/usePlannerHistory.ts`](https://github.com/mauriceboe/TREK/blob/main/client/src/hooks/usePlannerHistory.ts). This hook manages a mutable history stack using React refs to avoid unnecessary re-renders while providing imperative control over the trip planner's reversible actions.

### History Storage with UndoEntry

At the heart of the system lies the `UndoEntry` interface, which decouples the action description from its reversal logic:

```typescript
export interface UndoEntry {
  label: string
  undo: () => Promise<void> | void
}

```

The stack persists in a `useRef` array, ensuring that pushing new entries does not trigger component re-renders until explicitly requested.

### Pushing Actions to the Stack

When users perform CRUD operations in the trip planner, components call the `pushUndo` method with a human-readable label and an async undo function:

```typescript
const pushUndo = (label: string, undoFn: () => Promise<void> | void) => {
  historyRef.current = [{ label, undo: undoFn }, ...historyRef.current]
    .slice(0, maxEntries)
  forceUpdate()
}

```

This function prepends entries to maintain LIFO (Last-In-First-Out) order, enforces a default maximum of 30 entries to prevent memory bloat, and triggers a forced update via a dummy `useReducer` to refresh the UI state (e.g., enabling the Undo button).

### Executing Undo Operations

The `undo` function pops the most recent entry, updates the stack, and executes the stored callback:

```typescript
const undo = async () => {
  if (historyRef.current.length === 0) return
  const [first, ...rest] = historyRef.current
  historyRef.current = rest
  forceUpdate()
  try { await first.undo() } catch (e) { console.error('Undo failed:', e) }
}

```

Error handling ensures that failed undo operations log to the console without crashing the UI, maintaining application stability even when network requests or state reversals fail.

## Integration in the Trip Planner

The `useTripPlanner` hook in [`client/src/pages/tripPlanner/useTripPlanner.ts`](https://github.com/mauriceboe/TREK/blob/main/client/src/pages/tripPlanner/useTripPlanner.ts) integrates the history system by pushing undo entries for every mutable operation.

### Adding and Deleting Places

When adding a place, the system pushes an entry that deletes the newly created record:

```typescript
pushUndo(t('undo.addPlace'), async () => {
  await assignmentsApi.deletePlace(placeId)
  // additional state clean-up …
})

```

Conversely, deleting a place stores the previous state for restoration:

```typescript
pushUndo(t('undo.deletePlace'), async () => {
  await assignmentsApi.createPlace(previousPlaceData)
  // restore assignment if any …
})

```

### Reordering Itineraries

For drag-and-drop reordering operations, the hook captures the original sequence:

```typescript
pushUndo(t('undo.reorder'), async () => {
  await assignmentsApi.reorderPlaces(originalOrder)
})

```

## UI State and Feedback

The hook exposes `canUndo` and `lastActionLabel` to drive the interface. The trip planner component uses these to conditionally render the undo button and display tooltips showing which action will be reversed.

When users trigger undo, the handler executes the reversal and displays a confirmation toast:

```typescript
const handleUndo = useCallback(async () => {
  const label = lastActionLabel
  await undo()
  toast.info(t('undo.done', { action: label ?? '' }))
}, [undo, lastActionLabel, toast])

```

## Testing the Undo Stack

Unit tests in [`client/tests/unit/hooks/usePlannerHistory.test.ts`](https://github.com/mauriceboe/TREK/blob/main/client/tests/unit/hooks/usePlannerHistory.test.ts) verify LIFO behavior, empty-stack safety, and proper callback execution. These tests ensure that the stack correctly consumes entries in reverse chronological order and handles edge cases like calling undo on an empty history.

## Summary

- TREK's undo system uses a custom React hook called `usePlannerHistory` that maintains a mutable ref-based stack
- Each entry stores an `UndoEntry` with a label and async undo function, enabling decoupled action reversal
- The stack enforces a 30-entry limit and uses LIFO ordering to reverse the most recent action first
- Integration occurs in `useTripPlanner`, which pushes inverse callbacks for add, delete, and reorder operations
- The UI receives real-time feedback through `canUndo` and `lastActionLabel` return values

## Frequently Asked Questions

### How does TREK prevent memory leaks in the undo history?

The `usePlannerHistory` hook truncates the stack to a configurable `maxEntries` limit (default 30) using `slice(0, maxEntries)` whenever new entries are pushed. This prevents unbounded growth during extended planning sessions while maintaining sufficient history for practical use.

### What happens if an undo operation fails in TREK?

The `undo` function wraps the stored callback execution in a try-catch block that logs errors to the console without throwing. This ensures the UI remains responsive and the history stack stays synchronized even when network requests or state reversals fail.

### Why does TREK use useRef instead of useState for the history stack?

Using `useRef` prevents React re-renders on every history push, improving performance during rapid user actions. The hook only triggers updates via a dummy `useReducer` when necessary to refresh UI elements like the undo button's disabled state or tooltip text.

### Which file contains the translation strings for undo labels?

Internationalized undo labels reside in [`shared/src/i18n/zh/undo.ts`](https://github.com/mauriceboe/TREK/blob/main/shared/src/i18n/zh/undo.ts) and related i18n files, allowing the `useTripPlanner` hook to pass translated strings like "Add place" to the `pushUndo` function for display in tooltips and toast notifications.