483

useStateHistory

Hook that manages state with history functionality

statemediumtest coverage
Draft
import {
  useClickOutside,
  useDebounceCallback,
  useDisclosure,
  useStateHistory
} from '@siberiacancode/reactuse';
import { CheckIcon, HistoryIcon } from 'lucide-react';
import { useState } from 'react';

const INITIAL = 'Ship faster with reactuse — a collection of essential React hooks.';

const Demo = () => {
  const stateHistory = useStateHistory(INITIAL);
  const [text, setText] = useState(INITIAL);

  const menu = useDisclosure();
  const menuRef = useClickOutside<HTMLDivElement>(() => menu.close());

  const save = useDebounceCallback((value: string) => {
    stateHistory.set(value);
  }, 800);

  const onChange = (value: string) => {
    setText(value);

    save(value);
  };

  const jumpTo = (target: number) => {
    const delta = target - stateHistory.index;
    if (delta < 0) stateHistory.back(-delta);
    if (delta > 0) stateHistory.forward(delta);
    setText(stateHistory.history[target]);
    menu.close();
  };

  return (
    <section className='flex w-full max-w-md flex-col gap-2 p-4'>
      <div className='flex items-center justify-between'>
        <span className='text-foreground text-sm font-medium'>Draft</span>

        <div className='relative'>
          <button
            aria-expanded={menu.opened}
            data-size='sm'
            data-variant='ghost'
            type='button'
            onClick={() => menu.toggle()}
          >
            <HistoryIcon className='size-3.5' />
            History
          </button>

          {menu.opened && (
            <div
              ref={menuRef}
              className='absolute top-full right-0 z-10 mt-2 w-72'
              data-slot='dropdown-menu-content'
            >
              <div data-slot='dropdown-menu-label'>Version history</div>
              <div className='no-scrollbar max-h-56 overflow-y-auto'>
                {stateHistory.history.map((version, index) => {
                  const active = index === stateHistory.index;
                  return (
                    <div key={index} data-slot='dropdown-menu-item' onClick={() => jumpTo(index)}>
                      <span className='text-muted-foreground w-8 shrink-0 text-xs tabular-nums'>
                        v{index + 1}
                      </span>
                      <span className='truncate'>{version || 'Empty'}</span>
                      {active && <CheckIcon className='text-primary ml-auto size-4 shrink-0' />}
                    </div>
                  );
                })}
              </div>
            </div>
          )}
        </div>
      </div>

      <textarea
        className='no-scrollbar min-h-40 resize-none'
        value={text}
        onChange={(event) => onChange(event.target.value)}
      />
    </section>
  );
};

export default Demo;

Installation

pnpm add @siberiacancode/reactuse

Usage

const { value, history, index, set, back, forward, reset, undo, redo, canUndo, canRedo } = useStateHistory(0);

Type Declarations

interface UseStateHistoryReturn<Value> {
  /** True if a redo operation can be performed */
  canRedo: boolean;
  /** True if an undo operation can be performed */
  canUndo: boolean;
  /** All history values */
  history: Value[];
  /** Current index in history */
  index: number;
  /** Current value */
  value: Value;
  /** Go back specified number of steps in history (default: 1) */
  back: (steps?: number) => void;
  /** Go forward specified number of steps in history (default: 1) */
  forward: (steps?: number) => void;
  /** Redo the last change */
  redo: () => void;
  /** Reset history to initial state */
  reset: () => void;
  /** Set a new value */
  set: (value: Value) => void;
  /** Undo the last change */
  undo: () => void;
}

export type StateHistoryAction<Value> =
  | { type: 'BACK'; payload: { steps: number } }
  | { type: 'FORWARD'; payload: { steps: number } }
  | { type: 'REDO' }
  | { type: 'RESET'; payload: { initialValue: Value; capacity: number } }
  | { type: 'SET'; payload: { value: Value; capacity: number } }
  | { type: 'UNDO' };

export interface StateHistory<Value> {
  currentIndex: number;
  history: Value[];
  redoStack: Value[][];
  undoStack: Value[][];
}

API

Parameters

NameTypeDefaultNote
initialValueValue-- The initial value to start the history with
capacitynumber10- Maximum number of history entries and undo actions to keep

Returns

UseStateHistoryReturn<Value>

Contributors

ddebabinKKhasan

Last updated on