Hone logo
Hone
Problems

Implementing an Undo/Redo System in React

This challenge focuses on building a robust undo/redo functionality for a React application. Implementing such a system is crucial for enhancing user experience, allowing users to easily correct mistakes and explore different states within an application. You will create a reusable hook that manages a history of states and provides functions to navigate through them.

Problem Description

Your task is to create a custom React hook, useUndoRedo, that manages a history of states for a given component or application feature. This hook should allow users to perform actions that modify a state and then undo those changes, or redo them if they've been undone.

Key Requirements:

  1. State Management: The hook should accept an initial state and maintain an array (or similar data structure) to store past states.
  2. set Function: A function to update the current state. When this function is called, the previous current state should be added to the history.
  3. undo Function: A function to revert the state to the previous one in the history. If there are no past states to undo to, this function should do nothing.
  4. redo Function: A function to reapply a state that has been undone. If there are no states to redo, this function should do nothing.
  5. History Limit (Optional but Recommended): Consider implementing a mechanism to limit the maximum number of states stored in the history to prevent excessive memory usage.
  6. Return Values: The hook should return the current state, the set function, the undo function, and the redo function.

Expected Behavior:

  • When the application starts, the undo function should be disabled (or no-op) if no initial state is provided or if it's the first state.
  • When set is called, the new state becomes the current state, and the old current state is pushed onto the undo stack. The redo stack should be cleared.
  • When undo is called, the current state is moved to the redo stack, and the state from the top of the undo stack becomes the new current state.
  • When redo is called, the current state is moved back to the undo stack, and the state from the top of the redo stack becomes the new current state.
  • If the user performs a new set operation after undoing, the redo history should be discarded.

Edge Cases to Consider:

  • Initial State: How to handle the initial state and the availability of undo/redo at the very beginning.
  • No History: Calling undo when there's nothing to undo or redo when there's nothing to redo.
  • History Limit: What happens when the history limit is reached? The oldest states should be discarded.
  • Complex State Objects: The hook should be able to handle any serializable state object (e.g., objects, arrays, primitives).

Examples

Example 1: Basic Usage

Let's imagine a simple counter component.

Initial State: 0

  1. User calls set(1).
    • Current State: 1
    • Undo Stack: [0]
    • Redo Stack: []
  2. User calls set(2).
    • Current State: 2
    • Undo Stack: [0, 1]
    • Redo Stack: []
  3. User calls undo().
    • Current State: 1
    • Undo Stack: [0]
    • Redo Stack: [2]
  4. User calls undo().
    • Current State: 0
    • Undo Stack: []
    • Redo Stack: [1, 2]
  5. User calls redo().
    • Current State: 1
    • Undo Stack: [0]
    • Redo Stack: [2]

Example 2: New Action After Undo

Continuing from Example 1, after undoing to 0 and then redoing to 1.

  1. Current State: 1
    • Undo Stack: [0]
    • Redo Stack: [2]
  2. User calls set(3).
    • Current State: 3
    • Undo Stack: [0, 1]
    • Redo Stack: [] (The redo history [2] is cleared)

Example 3: History Limit

Assume a history limit of 2.

Initial State: A

  1. User calls set(B).
    • Current State: B
    • Undo Stack: [A]
    • Redo Stack: []
  2. User calls set(C).
    • Current State: C
    • Undo Stack: [A, B]
    • Redo Stack: []
  3. User calls set(D).
    • Current State: D
    • Undo Stack: [B, C] (State A is discarded due to history limit)
    • Redo Stack: []
  4. User calls undo().
    • Current State: C
    • Undo Stack: [B]
    • Redo Stack: [D]

Constraints

  • The useUndoRedo hook must be implemented in TypeScript.
  • The state managed by the hook should be serializable (e.g., plain JavaScript objects, arrays, primitives). Deeply nested or circular references might not be handled correctly without further serialization logic.
  • The history limit, if implemented, should be configurable and a positive integer. If not implemented, the history can grow unbounded.
  • The hook should be efficient and not cause unnecessary re-renders in consuming components.

Notes

  • Consider using useState within your custom hook to manage the current state and potentially the history arrays.
  • Think about how to represent the undo and redo stacks. Arrays are a natural fit.
  • When managing the history, be mindful of immutability. Ensure you are creating new state objects rather than mutating existing ones when pushing to history.
  • You might want to expose canUndo and canRedo boolean flags to indicate whether the respective actions are available, which can be useful for disabling UI buttons.
  • This challenge focuses on the core logic. In a real-world application, you might also consider debouncing set calls for performance-sensitive operations.
Loading editor...
typescript