Scientyfic World

How to Implement a Queue Data Structure in React (With a Notification Queue Example)

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...

Share:

Get an AI summary of this article

Queue Data Structure blog banner

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

OperationWhat it doesImmutable version for React state
enqueueAdd an item to the back[...queue, item]
dequeueRemove the item at the frontqueue.slice(1)
peek / frontRead the front item without removing itqueue[0]
isEmpty / sizeCheck how many items are waitingqueue.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>
  );
}
ActionFrontSizeQueueServed
Enqueue Ada, Alan, GraceAda3Ada, Alan, Grace
DequeueAlan2Alan, GraceAda
Dequeue twice morenone0Ada, Alan, Grace (Dequeue button disabled)

Items left in exactly the order they arrived, which is the whole point of a queue.

Queue data structure visualizer in React: enqueuing and dequeuing values
My original 2023 queue visualizer (live demo). The code below is the updated version and works much the same way.

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:

ItemsArray push + shiftQueue class (head index)Immutable […q, x] / q.slice(1)
10,0002–3 ms1 ms59 ms
100,000714–715 ms1 mstoo slow to run
300,0006.4 s3 mstoo 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() or shift() 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] is undefined.
  • 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.

Snehasish Konger
Developed @scientyficworld.org | Technical writer @Nected | Content Developer
Connect with Snehasish Konger

On This page

Take a Pause with Intervals

A Sunday letter on building, writing, and thinking deeper as a developer — short, honest, and worth your time.

Snehasish Konger profile photo

"Hey there — I'm Snehasish. Hope this post saved you some head-scratching time! I've spent years turning technical chaos into clarity, and I'm here to be your guide through the maze of modern tech. Stick around for more lightbulb moments — we're just getting started."

Related Posts