Rooks
HooksState Management

useStackState

Manages stack (LIFO) state with push, pop, and peek operations.

About

A React hook that manages state in the form of a stack

Examples

import { useStackState } from "rooks";

export default function NavigationStack() {
  const [, { peek, pop, push }, newestFirst] = useStackState(["Home"]);

  return (
    <section>
      <p>Top: {peek() ?? "Empty"}</p>
      <ol>
        {newestFirst.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ol>
      <button
        type="button"
        onClick={() => push(`Page ${newestFirst.length + 1}`)}
      >
        Push
      </button>
      <button type="button" onClick={() => void pop()}>
        Pop
      </button>
    </section>
  );
}

Basic usage

import React, { useRef } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { useStackState } from "rooks";
import styled from "styled-components";

const variants = {
  appear: {
    opacity: 0,
    y: "-150%",
    x: "-15%",
    rotate: -15,
    transition: {
      duration: 0.5,
    },
  },
  disappear: {
    opacity: 0,
    y: "-200%",
    x: "100%",
    rotate: 45,
    transition: {
      duration: 0.5,
    },
  },
  visible: {
    opacity: 1,
    y: "0%",
    x: "0%",
    rotate: 0,
    transition: {
      duration: 0.5,
    },
  },
};

const StackInner = styled(motion.div)`
  border-bottom: 5px solid black;
  padding: 0 12px 3px;
  width: 300px;
  > * {
    margin: 0;
    padding: 12px 16px;
  }
`;

export function StackDiv({ children, ...props }) {
  return (
    <StackInner {...props}>
      <AnimatePresence initial={false}>{children}</AnimatePresence>
    </StackInner>
  );
}

export const StackItem = styled(motion.div).attrs((props) => ({
  variants,
  initial: "appear",
  animate: "visible",
  exit: "disappear",
}))`
  width: 100%;
  background-color: rgba(254, 243, 199, 1);
  margin: 0;
  padding: 12px 16px;
  font-weight: 500;
  font-size: 1rem;
  line-height: 1.5rem;
  border-color: rgba(0, 0, 0, 1);
  border-width: 1px;
  border-style: solid;
  border-radius: 0.375rem;
  place-content: center;
  display: flex;
`;

export const ActionButton = styled(motion.button)`
    width: 100%;
  font-weight: 500;
  color: rgba(67,56,202,1);
  font-size: 1rem;
  line-height: 1.5rem;
  padding-top: 0.75rem;
  padding-bottom: 0.75rem;
  padding-left: 1.5rem;
  padding-right: 1.5rem;
  border-color: transparent;
  align-items: center;
  display: inline-flex;
  border-radius: 0.375rem;
  border-color: transparent;
  background-color: rgba(224,231,255,1);
}
`;

export const ActionButton2 = styled(motion.button)`
  width: 100%;
  font-weight: 500;
  font-size: 1rem;
  line-height: 1.5rem;
  padding-top: 0.75rem;
  padding-bottom: 0.75rem;
  padding-left: 1.5rem;
  padding-right: 1.5rem;
  border-color: transparent;
  align-items: center;
  display: inline-flex;
  border-radius: 0.375rem;
  border-color: transparent;
  background-color: rgba(254, 226, 226, 1);
`;

export const ActionButtons = styled.div`
  margin: 12px 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-gap: 16px;
`;

export const Root = styled.div`
  height: 100vh;
  padding-bottom: 100px;
  width: 300px;
  display: flex;
  align-items: flex-end;
  margin: auto;
  box-sizing: border-box;
`;

export default function App() {
  const numberToPushRef = useRef(3);
  const [list, { push, pop }, listInReverse] = useStackState([1, 2, 3]);

  function addToStack() {
    numberToPushRef.current = numberToPushRef.current + 1;
    push(numberToPushRef.current);
  }

  return (
    <Root>
      <div>
        <StackDiv>
          {listInReverse.map((item) => {
            return <StackItem key={item}>{item}</StackItem>;
          })}
        </StackDiv>
        <ActionButtons>
          <ActionButton onClick={addToStack}>Push</ActionButton>{" "}
          <ActionButton2 onClick={pop} warning>
            Pop
          </ActionButton2>{" "}
        </ActionButtons>
      </div>
    </Root>
  );
}

Inspect the top item and render newest-first

import { useStackState } from "rooks";

export default function UploadQueue() {
  const [queue, controls, newestFirst] = useStackState([
    "draft.pdf",
    "brief.png",
  ]);

  return (
    <div>
      <p>Next upload: {controls.peek() ?? "Nothing queued"}</p>
      <p>Items queued: {controls.length}</p>

      <button onClick={() => controls.push(`file-${controls.length + 1}.txt`)}>
        Add file
      </button>
      <button onClick={controls.pop} disabled={controls.isEmpty()}>
        Remove latest
      </button>
      <button onClick={controls.clear} disabled={controls.isEmpty()}>
        Clear queue
      </button>

      <ul>
        {newestFirst.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

Parameters

ParameterTypeDefaultDescription
initialListT[]RequiredItems copied into the initial stack.

Returned array items

Returned itemsTypeDescription
pushfunctionPut an item to the top of the stack
popfunctionRemove the item on the top of the stack
peekfunctionReturn the item on the top of the stack
lengthnumberNumber of items in the stack

Return value

Returns [list, controls, listInReverse]. list is ordered from bottom to top; listInReverse is a memoized copy ordered top to bottom. Controls include push, pop, peek, clear, isEmpty, and the current numeric length. Empty pop and peek return undefined; push returns the new length.

Behavior and lifecycle

The initial array and reversed view are shallow copies. push and pop callbacks close over the current rendered list rather than using functional updates, so several operations from one stale render do not reliably compose. clear is recreated on every render; other query/mutation callbacks are recreated when the list changes. No effects or external resources are created.

Compatibility and accessibility

The hook is SSR-safe and has no browser requirement. Render stack contents with semantic list markup, and manage focus or announcements if push/pop actions change content users are actively reviewing.

On this page