Hone logo
Hone
Problems

React useUndoRedo Hook Challenge

In modern web applications, providing users with the ability to undo and redo their actions is a common and valuable feature. This challenge focuses on building a custom React hook, useUndoRedo, that manages and facilitates this undo/redo functionality for any piece of state.

Problem Description

Your task is to create a custom React hook named useUndoRedo that takes an initial state value and returns the current state, along with functions to update the state, undo previous changes, and redo undone changes. The hook should maintain a history of state changes, allowing users to navigate through these states.

Key Requirements:

  1. State Management: The hook must manage a piece of state.
  2. History Tracking: It needs to keep track of past states and future (undone) states.
  3. Update Function: A function to update the current state and add it to the history.
  4. Undo Function: A function to revert to the previous state in the history.
  5. Redo Function: A function to reapply an undone state.
  6. State Availability: The hook should expose the current state.
  7. History Limits (Optional but recommended): Consider how to handle a potentially infinite history to prevent memory leaks.

Expected Behavior:

  • When the update function is called, the new state is set, and the old state is pushed onto the undo history. The redo history should be cleared upon any new update.
  • When the undo function is called, the current state is moved to the redo history, and the previous state from the undo history becomes the current state.
  • When the redo function is called, the current state is moved back to the undo history, and the next state from the redo history becomes the current state.
  • Calling undo when there are no previous states should have no effect.
  • Calling redo when there are no undone states should have no effect.

Edge Cases to Consider:

  • Initial state: What happens when the hook is first used?
  • No history: What happens when undo or redo is called immediately after initialization or after the history has been exhausted?
  • Clearing redo history: When a new state is updated after some undos, the redo history should be reset.

Examples

Let's consider a simple counter application to illustrate the useUndoRedo hook.

Example 1: Basic Increment and Undo

import { useUndoRedo } from './useUndoRedo'; // Assuming your hook is in this file

function Counter() {
  const {
    current: count,
    update,
    undo,
    redo,
    canUndo,
    canRedo,
  } = useUndoRedo(0); // Initial state is 0

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => update(count + 1)}>Increment</button>
      <button onClick={undo} disabled={!canUndo}>Undo</button>
      <button onClick={redo} disabled={!canRedo}>Redo</button>
    </div>
  );
}

// Initial rendering:
// Count: 0
// Buttons: Increment (enabled), Undo (disabled), Redo (disabled)

// User clicks "Increment":
// update(1) is called. State becomes 1. History: [0], Redo History: []
// Count: 1
// Buttons: Increment (enabled), Undo (enabled), Redo (disabled)

// User clicks "Undo":
// undo() is called. State reverts to 0. History: [], Redo History: [1]
// Count: 0
// Buttons: Increment (enabled), Undo (disabled), Redo (enabled)

Example 2: Increment, Undo, Redo, and New Update

// ... (Continuing from Example 1's state after undo)
// Current state: count = 0, History: [], Redo History: [1]

// User clicks "Increment" again:
// update(1) is called. State becomes 1. History: [0], Redo History: [] (cleared)
// Count: 1
// Buttons: Increment (enabled), Undo (enabled), Redo (disabled)

// User clicks "Undo":
// undo() is called. State reverts to 0. History: [], Redo History: [1]
// Count: 0
// Buttons: Increment (enabled), Undo (disabled), Redo (enabled)

// User clicks "Redo":
// redo() is called. State becomes 1. History: [0], Redo History: []
// Count: 1
// Buttons: Increment (enabled), Undo (enabled), Redo (disabled)

// User clicks "Undo" twice:
// First undo: State becomes 0. History: [], Redo History: [1]
// Second undo: No effect as history is empty.
// Count: 0
// Buttons: Increment (enabled), Undo (disabled), Redo (enabled)

Example 3: Handling Complex State (Object)

interface User {
  name: string;
  age: number;
}

function UserProfile() {
  const initialState: User = { name: 'Alice', age: 30 };
  const {
    current: user,
    update,
    undo,
    redo,
    canUndo,
    canRedo,
  } = useUndoRedo<User>(initialState);

  const updateName = (newName: string) => {
    update({ ...user, name: newName });
  };

  const incrementAge = () => {
    update({ ...user, age: user.age + 1 });
  };

  return (
    <div>
      <p>Name: {user.name}, Age: {user.age}</p>
      <input type="text" value={user.name} onChange={(e) => updateName(e.target.value)} />
      <button onClick={incrementAge}>Increment Age</button>
      <button onClick={undo} disabled={!canUndo}>Undo</button>
      <button onClick={redo} disabled={!canRedo}>Redo</button>
    </div>
  );
}

// Initial rendering:
// Name: Alice, Age: 30
// Buttons: Undo (disabled), Redo (disabled)

// User types "Bob" into the input:
// update({ name: 'Bob', age: 30 }) is called.
// History: [{ name: 'Alice', age: 30 }], Redo History: []
// Name: Bob, Age: 30

// User clicks "Increment Age":
// update({ name: 'Bob', age: 31 }) is called.
// History: [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 30 }], Redo History: []
// Name: Bob, Age: 31
// Buttons: Undo (enabled), Redo (disabled)

// User clicks "Undo":
// State becomes { name: 'Bob', age: 30 }. History: [{ name: 'Alice', age: 30 }], Redo History: [{ name: 'Bob', age: 31 }]
// Name: Bob, Age: 30
// Buttons: Undo (enabled), Redo (enabled)

Constraints

  • The hook must be implemented in TypeScript.
  • The hook should be generic to handle any state type.
  • Consider implementing a maximum history size (e.g., 50 states) to prevent excessive memory usage. If the history exceeds this limit, the oldest state should be discarded.
  • The update function should perform a deep copy or ensure immutability for object/array states to correctly track changes. (Hint: This is crucial for proper history tracking).

Notes

  • Think about the internal data structures you'll need to store the history. Arrays are a natural fit.
  • Consider returning flags like canUndo and canRedo to easily disable/enable your undo/redo buttons in the UI.
  • When dealing with complex data structures like objects and arrays, ensure that your update function creates new references for modified data rather than mutating existing ones. This is fundamental to how React state updates and history tracking work effectively.
  • The challenge implicitly asks you to also return canUndo and canRedo boolean flags, as seen in the examples.
Loading editor...
typescript