Back to Code Bytes
6 min read
useQueue

Description

A React hook that manages the state in a queue data structure. A queue works based on the first-in, first-out (FIFO) principle.

Code Byte

import { useState, useCallback } from 'react';

interface QueueOptions<T> {
  initialValues?: T[];
}

export interface QueueState<T> {
  queue: T[];
  length: number;
  enqueue: (...items: T[]) => void;
  dequeue: () => void;
  updateQueue: (fn: (current: T[]) => T[]) => void;
  clearQueue: () => void;
  peek: () => T | undefined;
}

export const useQueue = <T>({
  initialValues = []
}: QueueOptions<T> = {}): QueueState<T> => {
  const [queue, setState] = useState<T[]>(initialValues);

  /**
   * Enqueue; Adds item to queue.
   */
  const enqueue = useCallback((...items: T[]) => {
    setState((current) => [...current, ...items]);
  }, []);

  /**
   * Dequeue; Removes first item in queue.
   */
  const dequeue = useCallback(() => {
    // Immutable solution to remove first element in array.
    setState((current) => current.slice(1));
  }, []);

  /**
   * Update function to allow for more granular control of updating the queue.
   * The `update` function accepts a callback fn where the first arg is the
   * current queue state that we can then manipulate. See tests for examples.
   * */
  const updateQueue = useCallback(
    (fn: (current: T[]) => T[]) => setState((current) => fn([...current])),
    []
  );

  /**
   * Remove all items from queue.
   */
  const clearQueue = useCallback(() => setState(() => []), []);

  /**
   * Returns the first item in the queue.
   */
  const peek = useCallback(() => {
    if (queue.length > 0) {
      return queue[0];
    }

    return undefined;
  }, [queue]);

  return {
    queue,
    length: queue.length,
    enqueue,
    dequeue,
    updateQueue,
    clearQueue,
    peek
  };
};

export default useQueue;

Arguments

ArgumentsTypeDefaultRequiredDescription
initialValuesArray<any>[]An array of initial values for the queue.

Returns

ReturnsTypeDescription
queueArray<any>The ordered list of items.
lengthnumberThe length of the queue.
enqueuefunctionAdds item to queue.
dequeuefunctionRemoves first item in queue.
updateQueuefunctionUpdate function to allow for more granular control of updating the queue. The update function accepts a callback fn where the first arg is the current queue state that we can then manipulate. See tests for examples.
clearQueuefunctionRemove all items from queue.
peekfunctionReturns the first item in the queue..

Usage

import { useQueue, useSafeState } from '@/react-hooks';

const MyComponent = () => {
  const { queue, enqueue, dequeue, updateQueue, clearQueue, peek } = useQueue({
    initialValues: [1, 2, 3]
  });

  const addItem = (item) => queue(item);
  const removeItem = (item) => dequeue();
  const removeOddNumbers = (item) => {
    updateQueue((items) => items.filter((item) => item % 2 === 1));
  };
  const clearAllItems = () => clearQueue();
  const getFirstItem = () => peek();

  return (
    <ul>
      {queue.map((item) => (
        <li>{item}</li>
      ))}
    </ul>
  );
};

Tests

import { renderHook, act, RenderResult } from '@testing-library/react-hooks';
import useQueue, { QueueState } from './useQueue.hook';

const initialValues = [1, 2, 3];

function assertQueue(
  result: RenderResult<QueueState<number>>,
  expectedQueue: number[],
  expectedLength: number,
  expectedPeek: number | undefined
) {
  expect(result.current.queue).toEqual(expectedQueue);
  expect(result.current.length).toEqual(expectedLength);
  expect(result.current.peek()).toEqual(expectedPeek);
}

describe('useQueue', () => {
  it('should return an empty array when no initialValues are passed', () => {
    const { result } = renderHook(() => useQueue({}));

    expect(result.current.queue).toEqual([]);
  });

  it('should set initialValues for queue', () => {
    const { result } = renderHook(() => useQueue({ initialValues }));

    assertQueue(result, [1, 2, 3], 3, 1);
  });

  it('should add values to queue', () => {
    const { result } = renderHook(() => useQueue({ initialValues }));

    act(() => {
      result.current.enqueue(4);
    });

    assertQueue(result, [1, 2, 3, 4], 4, 1);

    act(() => {
      result.current.enqueue(5);
    });

    assertQueue(result, [1, 2, 3, 4, 5], 5, 1);
  });

  it('should remove values from queue', () => {
    const { result } = renderHook(() => useQueue({ initialValues }));

    act(() => {
      result.current.dequeue();
    });

    assertQueue(result, [2, 3], 2, 2);

    act(() => {
      result.current.dequeue();
    });

    assertQueue(result, [3], 1, 3);
  });

  it('should use update fn to add value to queue', () => {
    const { result } = renderHook(() => useQueue({ initialValues }));
    const valueToAdd = 4;

    // Example of how we could use the update fn and pass it a callback
    // for a more granular control of updating the current queue state.
    function updateAddCallback(currentQueue: number[]) {
      const hasValue = currentQueue.some((item) => item === valueToAdd);

      if (hasValue) {
        return currentQueue;
      }

      return [...currentQueue, valueToAdd];
    }

    // Use update fn to add a value if it does not exist in the queue
    // Should ADD value to queue since it does NOT EXIST in queue.
    act(() => {
      result.current.updateQueue(updateAddCallback);
    });

    assertQueue(result, [1, 2, 3, 4], 4, 1);

    // Use update fn to add a value if it does not exist in the queue
    // Should NOT ADD value to queue since it does EXIST in queue
    act(() => {
      result.current.updateQueue(updateAddCallback);
    });

    assertQueue(result, [1, 2, 3, 4], 4, 1);
  });

  it('should use update fn to remove value to queue', () => {
    const { result } = renderHook(() => useQueue({ initialValues }));
    const valueToRemove = 2;
    const callbackFn = jest.fn();

    // Another example of how we could use the update fn and pass it a callback
    // for a more granular control of updating the current queue state.
    function updateRemoveCallback(currentQueue: number[]) {
      return currentQueue.filter((item) => {
        if (item === valueToRemove) {
          callbackFn();
          return false;
        }

        return true;
      });
    }

    // Use update fn to remove a value if it exists in the queue
    // Should REMOVE value from queue and call callbackFn
    act(() => {
      result.current.updateQueue(updateRemoveCallback);
    });

    assertQueue(result, [1, 3], 2, 1);
    expect(callbackFn).toHaveBeenCalledTimes(1);

    // Use update fn to remove a value if it exists in the queue
    // Should NOT REMOVE value from queue and NOT call callbackFn
    act(() => {
      result.current.updateQueue(updateRemoveCallback);
    });

    // Results should be the same as previous assertions
    assertQueue(result, [1, 3], 2, 1);
    expect(callbackFn).toHaveBeenCalledTimes(1);
  });

  it('should clear all values in queue', () => {
    const { result } = renderHook(() => useQueue({ initialValues }));

    act(() => {
      result.current.clearQueue();
    });

    assertQueue(result, [], 0, undefined);
  });

  it('should return first value in queue when calling peek', () => {
    const { result } = renderHook(() => useQueue({ initialValues }));
    let peekResult;

    act(() => {
      peekResult = result.current.peek();
    });

    expect(peekResult).toEqual(1);
  });

  it('should return the length of the queue', () => {
    const { result } = renderHook(() => useQueue({ initialValues }));

    expect(result.current.length).toEqual(3);
  });
});