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
Arguments | Type | Default | Required | Description |
---|
initialValues | Array<any> | [] | ❌ | An array of initial values for the queue. |
Returns
Returns | Type | Description |
---|
queue | Array<any> | The ordered list of items. |
length | number | The length of the queue. |
enqueue | function | Adds item to queue. |
dequeue | function | Removes first item in queue. |
updateQueue | function | 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. |
clearQueue | function | Remove all items from queue. |
peek | function | Returns 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);
});
});