Hone logo
Hone
Problems

Implementing a Custom useUndoRedo Hook in React with TypeScript

This challenge focuses on building a reusable React hook, useUndoRedo, that manages undo and redo functionality for a given state value. Implementing such a hook is a common pattern in applications requiring complex editing or data manipulation, providing a clean and consistent way to handle user actions and revert them. This exercise will test your understanding of React hooks, state management, and functional programming principles.

Problem Description

You are tasked with creating a custom React hook called useUndoRedo. This hook should accept a state value as input and return an object containing functions to update the state, undo the last update, and redo the last undone update. The hook should maintain a history of state changes to enable undo/redo operations.

What needs to be achieved:

  • Create a useUndoRedo hook that accepts an initial state value.
  • Provide a function to update the state with a new value. This function should add the current state to the undo history before applying the new state.
  • Provide an undo function that reverts the state to the previous state in the undo history. If the undo history is empty, the state should remain unchanged.
  • Provide a redo function that reapplies the last undone state. If the redo history is empty, the state should remain unchanged.
  • The hook should return the current state and the update function.

Key Requirements:

  • The hook must be written in TypeScript.
  • The hook should maintain separate undo and redo histories.
  • When an update is performed, the current state should be pushed onto the undo history, and the redo history should be cleared.
  • The hook should handle edge cases where the undo or redo history is empty.
  • The hook should prevent infinite loops when undoing and redoing.

Expected Behavior:

  • Initial state is set correctly.
  • update function correctly updates the state and adds the previous state to the undo history.
  • undo function correctly reverts to the previous state.
  • redo function correctly reapplies the last undone state.
  • Empty histories are handled gracefully.

Important Edge Cases to Consider:

  • Initial state is null or undefined.
  • Multiple consecutive updates.
  • Undoing and redoing multiple times.
  • Attempting to undo when the undo history is empty.
  • Attempting to redo when the redo history is empty.

Examples

Example 1:

Input: useUndoRedo("Initial State")
Output: { state: "Initial State", update: (newState: string) => void }
Explanation: The hook initializes with the provided state.

Example 2:

Input:
  const [state, update] = useUndoRedo(0);
  update(1);
  update(2);
  undo();
  redo();

Output:
  state: 2
Explanation:
  1. Initial state: 0
  2. update(1): state becomes 1, undo history: [0]
  3. update(2): state becomes 2, undo history: [0, 1], redo history: []
  4. undo(): state becomes 1, redo history: [2]
  5. redo(): state becomes 2, redo history: []

Example 3: (Edge Case)

Input:
  const [state, update] = useUndoRedo(0);
  undo(); // Attempt to undo when history is empty

Output:
  state: 0
Explanation: Undo does nothing when the history is empty.

Constraints

  • The hook should be efficient in terms of memory usage. Avoid storing unnecessary data.
  • The hook should be reusable across different components and state types. While the examples use strings and numbers, the hook should be generic enough to handle other data types.
  • The hook should not cause any performance bottlenecks. Updates and undo/redo operations should be reasonably fast.
  • The hook should be well-documented with clear comments explaining the logic.

Notes

  • Consider using an array to store the undo and redo histories.
  • Think about how to handle different data types for the state value. You might want to use generics.
  • Pay close attention to the order of operations when updating, undoing, and redoing the state.
  • The update function should accept the new state as an argument.
  • The hook should return an object containing the current state and the update function. The undo and redo functions should also be returned.
  • Consider immutability when updating the state. Avoid directly modifying the existing state object. Instead, create a new object with the updated values. This is especially important when dealing with complex data structures.
Loading editor...
typescript