A queue data structure is a First-In-First-Out (FIFO) collection: items join at the back and leave from the front, like people waiting in line. In React, keep the queue as an array in state and update it immutably, adding with a new array that has the item at the end and removing by copying the array without its first item. For large queues outside React state, use a queue class with a head index instead of array.shift(), which my benchmark shows becomes very slow.
This guide covers the queue operations, a reusable useQueue hook and visualizer built with React 19, a practical notification queue, and the performance trap in JavaScript’s shift(). Every example was tested in a browser or Node.js. For the last-in-first-out counterpart, see how to implement a stack in React.
Queue Operations
| Operation | What it does | Immutable version for React state |
|---|---|---|
| enqueue | Add an item to the back | [...queue, item] |
| dequeue | Remove the item at the front | queue.slice(1) |
| peek / front | Read the front item without removing it | queue[0] |
| isEmpty / size | Check how many items are waiting | queue.length |
Variants you’ll meet: a circular queue reuses a fixed-size buffer, a priority queue always removes the most important item first (usually built on a heap), and a deque (double-ended queue) allows adding and removing at both ends. In apps, queues schedule work in arrival order: print jobs, uploads, chat messages, notifications and the browser’s own task queue for events and timers.
Queue Helpers and a useQueue Hook
As with any React state, never mutate the array you already have: queue.push() followed by setQueue(queue) passes the same reference, and React skips the render. Small pure functions keep this safe:
// Immutable queue helpers for React state
export const enqueue = (queue, item) => [...queue, item];
export const dequeue = (queue) => queue.slice(1);
export const front = (queue) => queue[0];
export const isEmpty = (queue) => queue.length === 0;import { useState } from 'react';
import { enqueue, dequeue, front } from './queue';
export function useQueue(initial = []) {
const [items, setItems] = useState(initial);
return {
items,
front: front(items),
size: items.length,
enqueue: (item) => setItems((q) => enqueue(q, item)),
dequeue: () => setItems((q) => dequeue(q)),
clear: () => setItems([]),
};
}Building the Queue Visualizer
Each queued item gets its own ID, so the list keys stay stable as items move up the line (using the array index as the key would give every item a new key after each dequeue):
import { useState } from 'react';
import { useQueue } from './useQueue';
export default function QueueVisualizer() {
const queue = useQueue();
const [value, setValue] = useState('');
const [served, setServed] = useState([]);
function handleEnqueue(e) {
e.preventDefault();
if (!value.trim()) return;
queue.enqueue({ id: crypto.randomUUID(), name: value.trim() });
setValue('');
}
function handleDequeue() {
if (queue.size === 0) return;
setServed((s) => [...s, queue.front.name]);
queue.dequeue();
}
return (
<section>
<form onSubmit={handleEnqueue}>
<input value={value} onChange={(e) => setValue(e.target.value)} placeholder="Customer" />
<button>Enqueue</button>
<button type="button" onClick={handleDequeue} disabled={queue.size === 0}>Dequeue</button>
</form>
<p id="queue-info">Front: {queue.front?.name ?? 'none'} | Size: {queue.size}</p>
<ol id="queue-items" className="queue">
{queue.items.map((item) => <li key={item.id}>{item.name}</li>)}
</ol>
<p id="served">Served: {served.join(', ')}</p>
</section>
);
}| Action | Front | Size | Queue | Served |
|---|---|---|---|---|
| Enqueue Ada, Alan, Grace | Ada | 3 | Ada, Alan, Grace | |
| Dequeue | Alan | 2 | Alan, Grace | Ada |
| Dequeue twice more | none | 0 | Ada, Alan, Grace (Dequeue button disabled) |
Items left in exactly the order they arrived, which is the whole point of a queue.

A Practical Example: A Notification Queue
If several actions finish at once, showing all their notifications together is noisy. A queue shows them one at a time, in order:
import { useEffect, useState } from 'react';
// A real use of a queue: show notifications one at a time, in order
export default function ToastQueue() {
const [queue, setQueue] = useState([]);
const current = queue[0];
useEffect(() => {
if (!current) return;
const timer = setTimeout(() => setQueue((q) => q.slice(1)), 1000); // dequeue after 1 s
return () => clearTimeout(timer);
}, [current]);
const notify = (text) => setQueue((q) => [...q, { id: crypto.randomUUID(), text }]);
return (
<div>
<button onClick={() => notify('Saved')}>Save</button>
<button onClick={() => notify('Copied')}>Copy</button>
<button onClick={() => notify('Deleted')}>Delete</button>
{current && <p id="toast" role="status">{current.text} ({queue.length - 1} waiting)</p>}
</div>
);
}I clicked Save, Copy and Delete in quick succession. The app showed “Saved (2 waiting)”, then after one second “Copied (1 waiting)”, then “Deleted (0 waiting)”, and cleared the notification after the third second. The effect depends on current, so each time the front item changes, a new timer starts and the cleanup cancels the old one.
Performance: Why array.shift() Is a Trap for Big Queues
shift() removes the first element and moves every remaining element down by one, so it gets slower as the array grows. For a large queue, keep a “head” index and only move items occasionally:
// A queue with O(1) dequeue: keep a head index instead of shifting the array
class Queue {
#items = [];
#head = 0;
enqueue(item) { this.#items.push(item); }
dequeue() {
if (this.#head === this.#items.length) return undefined;
const item = this.#items[this.#head];
this.#items[this.#head++] = undefined; // let it be garbage-collected
if (this.#head > 1024 && this.#head * 2 > this.#items.length) {
this.#items = this.#items.slice(this.#head); // compact occasionally
this.#head = 0;
}
return item;
}
peek() { return this.#items[this.#head]; }
get size() { return this.#items.length - this.#head; }
}I enqueued and then dequeued n numbers in Node.js 23, twice:
| Items | Array push + shift | Queue class (head index) | Immutable […q, x] / q.slice(1) |
|---|---|---|---|
| 10,000 | 2–3 ms | 1 ms | 59 ms |
| 100,000 | 714–715 ms | 1 ms | too slow to run |
| 300,000 | 6.4 s | 3 ms | too slow to run |
At 10,000 items shift() was still fast, thanks to engine optimizations, but by 100,000 items it had collapsed. The immutable pattern copies the whole array on every operation, which is fine for a UI queue of a few dozen items and wrong for a large data-processing queue. Use the right tool for each: immutable arrays in React state, a proper queue class (or a library) for work queues in Node.js or Web Workers.
Common Mistakes
- Mutating the state array with
push()orshift()and setting the same reference. - Using the array index as the list key, so every item gets a new key after a dequeue.
- Dequeuing from an empty queue without checking;
queue[0]isundefined. - Using
shift()for queues with tens of thousands of items. - Starting timers for queued work without cleaning them up when the component unmounts.
New to how React state updates work? Start with React core concepts.
Conclusion
A queue in React is an array in state with immutable enqueue ([...queue, item]) and dequeue (queue.slice(1)), wrapped in a small useQueue hook and rendered with stable keys. It’s ideal for anything that should happen in arrival order, such as notifications, uploads or a waiting list. When queues grow large, outside React state, switch from shift() to a head-index queue: in my benchmark it handled 300,000 items in 3 ms instead of 6.4 seconds.
FAQs
How do I implement a queue in React?
Keep an array in useState. Enqueue with setQueue(q => […q, item]), dequeue with setQueue(q => q.slice(1)), and read the front with queue[0]. Do not mutate the array that is already in state.
What is the difference between a stack and a queue?
A stack is last-in-first-out: the newest item is removed first. A queue is first-in-first-out: the oldest item is removed first.
Is array.shift() slow in JavaScript?
For large arrays, yes. It moves every remaining element. In my Node.js test, dequeuing 100,000 items with shift() took about 715 ms, while a queue with a head index took about 1 ms.
What is a queue used for in a React app?
Showing notifications one at a time, processing uploads or requests in order, chat message buffers, and any work that should happen in arrival order.
What are the types of queues?
A simple queue, a circular queue that reuses a fixed buffer, a priority queue that removes the highest-priority item first, and a deque that allows adding and removing at both ends.








