Scientyfic World

How to Implement a Stack Data Structure in React (With an Undo/Redo Example)

To implement a stack data structure in React, keep the items in state as an array and treat it as immutable: add an item by creating a new array with...

Share:

Get an AI summary of this article

blog banner for the article implementing stack data structure using react js

To implement a stack data structure in React, keep the items in state as an array and treat it as immutable: add an item by creating a new array with the item at the end, remove the top by copying the array without its last item, and read the top as the last element. In code that’s [...stack, item], stack.slice(0, -1) and stack.at(-1). Never call push() or pop() on the array that’s already in state, because React only re-renders when state is replaced.

This guide builds a stack visualizer and a reusable useStack hook with React 19, then uses two stacks for a practical undo/redo feature. I tested every example in a headless browser. If stacks are new to you, read the introduction to the stack data structure first.

Looking for a “Stack” layout component instead? UI libraries such as MUI and Chakra UI have a Stack component that arranges children vertically or horizontally with spacing. That’s a layout helper, unrelated to the stack data structure covered here.

What Is a Stack?

A stack is a Last-In-First-Out (LIFO) collection: the last item added is the first one removed, like a pile of plates. It has a small set of operations, all of which take constant time with a JavaScript array:

OperationWhat it doesJavaScript array
pushAdd an item to the topstack.push(x) (mutating) or [...stack, x]
popRemove the top itemstack.pop() (mutating) or stack.slice(0, -1)
peekRead the top item without removing itstack.at(-1) or stack[stack.length - 1]
isEmpty / sizeCheck how many items there arestack.length

You meet stacks everywhere in software: the browser’s back button, undo in editors, the JavaScript call stack, and checking that brackets in code are balanced.

Why You Can’t Just Mutate the Array in React

The 2023 version of this post stored a Stack class instance in state, called stack.push(value) on it and then setState({ stack }) with the same object. That only appeared to work because class components re-render on every setState call. With hooks, setting state to the same array is ignored, so the screen doesn’t update:

const [stack, setStack] = useState([]);

// Wrong: mutates the existing array, then sets the same reference
stack.push('A');
setStack(stack);                    // React sees no change and skips the render

// Right: create a new array
setStack((s) => [...s, 'A']);

The old version also used string refs (ref="input"), which React 19 removed. Keeping the operations as small pure functions avoids both problems:

// Immutable stack helpers: each returns a new array, which is what React state needs
export const push = (stack, item) => [...stack, item];
export const pop = (stack) => stack.slice(0, -1);
export const peek = (stack) => stack[stack.length - 1];
export const isEmpty = (stack) => stack.length === 0;
// checked in Node.js
const a = [];
const b = push(a, 'A');
const c = push(b, 'B');
// a: []   b: ['A']   c: ['A', 'B']
// peek(c): 'B'   pop(c): ['A']   c is unchanged: ['A', 'B']

A Reusable useStack Hook

import { useState } from 'react';
import { push, pop, peek } from './stack';

export function useStack(initial = []) {
  const [items, setItems] = useState(initial);
  return {
    items,
    top: peek(items),          // read the top before popping if you need it
    size: items.length,
    push: (item) => setItems((s) => push(s, item)),
    pop: () => setItems((s) => pop(s)),
    clear: () => setItems([]),
  };
}

Note that pop doesn’t return the removed item. React runs state updater functions later, during the next render, so a value captured inside one isn’t available when pop() returns. Read top before popping instead, as the visualizer does.

Building the Stack Visualizer

import { useState } from 'react';
import { useStack } from './useStack';

export default function StackVisualizer({ limit = 5 }) {
  const stack = useStack();
  const [value, setValue] = useState('');
  const [message, setMessage] = useState('');

  function handlePush(e) {
    e.preventDefault();
    if (!value.trim()) return;
    if (stack.size >= limit) return setMessage(`Stack overflow: limit is ${limit}`);
    stack.push(value.trim());
    setValue('');
    setMessage('');
  }

  function handlePop() {
    if (stack.size === 0) return setMessage('Stack underflow: nothing to pop');
    setMessage(`Popped "${stack.top}"`);
    stack.pop();
  }

  return (
    <section>
      <form onSubmit={handlePush}>
        <input value={value} onChange={(e) => setValue(e.target.value)} placeholder="Value" />
        <button>Push</button>
        <button type="button" onClick={handlePop}>Pop</button>
      </form>
      <p id="stack-info">Top: {stack.top ?? 'none'} | Size: {stack.size}</p>
      <p role="status">{message}</p>
      {/* reversed so the top of the stack is drawn at the top */}
      <ol id="stack-items" className="stack">
        {[...stack.items].reverse().map((item, i) => (
          <li key={stack.size - i}>{item}</li>
        ))}
      </ol>
    </section>
  );
}

The list is drawn in reverse so the top of the stack appears at the top of the screen, and the stack has a size limit to demonstrate overflow. In my test:

ActionTopSizeItems (top first)Message
Push A, B, CC3C, B, A
PopB2B, APopped “C”
Push D, E, F, G (limit 5)F5F, E, D, B, AStack overflow: limit is 5
Pop six timesnone0Stack underflow: nothing to pop
Stack data structure visualizer in React: pushing and popping values
My original 2023 stack visualizer (live demo). The code below is the updated version and works much the same way.

The keys use each item’s position counted from the bottom, which stays the same for an item while it’s on the stack, because a stack only changes at the top. Add some CSS (a column of boxes with a transition on new items) to make it look like the demo above.

A Practical Example: Undo and Redo with Two Stacks

Undo history is a stack of past states; redo is a second stack of states you undid. Typing pushes onto the past stack and clears the future stack:

import { useState } from 'react';

// Two stacks: past states for undo, future states for redo
export default function UndoEditor() {
  const [history, setHistory] = useState({ past: [], present: '', future: [] });

  function type(text) {
    setHistory(({ past, present }) => ({ past: [...past, present], present: text, future: [] }));
  }
  function undo() {
    setHistory(({ past, present, future }) =>
      past.length === 0 ? { past, present, future }
        : { past: past.slice(0, -1), present: past[past.length - 1], future: [present, ...future] });
  }
  function redo() {
    setHistory(({ past, present, future }) =>
      future.length === 0 ? { past, present, future }
        : { past: [...past, present], present: future[0], future: future.slice(1) });
  }

  return (
    <div>
      <input id="editor" value={history.present} onChange={(e) => type(e.target.value)} />
      <button onClick={undo} disabled={history.past.length === 0}>Undo</button>
      <button onClick={redo} disabled={history.future.length === 0}>Redo</button>
    </div>
  );
}

In my test, typing “cat” (one history entry per keystroke) and pressing Undo twice gave “ca” and then “c”; Redo restored “ca”; typing “r” produced “car” and disabled Redo, because a new edit clears the redo stack. Real editors group keystrokes into larger steps, for example by pausing for a moment before pushing a new entry.

Another Classic: Checking Balanced Brackets

function isBalanced(text) {
  const pairs = { ')': '(', ']': '[', '}': '{' };
  const stack = [];
  for (const ch of text) {
    if ('([{'.includes(ch)) stack.push(ch);
    else if (ch in pairs && stack.pop() !== pairs[ch]) return false;
  }
  return stack.length === 0;
}
isBalanced('{ a: [1, 2] }')             // true
isBalanced('fn(a[0)]')                  // false: ) closes [
isBalanced('((')                        // false: never closed
isBalanced('if (x) { y[0] = (1); }')   // true

Each opening bracket is pushed; each closing bracket must match the item on top. Linters, parsers and code editors use the same idea.

Common Mistakes

  • Calling push() or pop() on the array in state, then setting the same array.
  • Using the value returned from inside a state updater function, which isn’t available yet.
  • Forgetting to handle an empty stack: peek() on an empty array returns undefined.
  • Storing a class instance with internal state in React state; plain arrays and pure functions are simpler to update and debug.

The same approach works for a first-in-first-out structure; see how to implement a queue in React.

Conclusion

A stack in React is just an array in state with immutable push and pop: [...stack, item] to add, stack.slice(0, -1) to remove and the last element to peek. Wrap those in a useStack hook, never mutate the array React already has, and you can build visualizers, navigation history or undo/redo on top of it.

FAQs

How do I implement a stack in React?

Keep an array in useState. Push with setStack(s => […s, item]), pop with setStack(s => s.slice(0, -1)), and read the top with stack[stack.length – 1]. Never mutate the array that is already in state.

Why does my stack not update in React when I use push()?

push() mutates the existing array, and setting state to the same array reference tells React nothing changed, so it skips the render. Create a new array instead.

What is a stack used for in a React app?

Undo and redo history, navigation history, nested modals or dialogs, and step-by-step wizards where the user can go back. Parsers also use stacks to match brackets and tags.

Is the MUI Stack component a stack data structure?

No. MUI’s Stack is a layout component that places children in a row or column with spacing. The stack data structure is a last-in-first-out collection.

What is the time complexity of stack operations with a JavaScript array?

push, pop and peek on the end of an array are constant time. The immutable versions used in React state copy the array, which is fine for the small stacks UIs usually need.

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