From 2c037337d734c00052b68374db4a437177e8f021 Mon Sep 17 00:00:00 2001 From: kanzariya-maulik Date: Thu, 30 Oct 2025 12:31:38 +0530 Subject: [PATCH] feat: add useStack hook (LIFO stack) with tests and docs --- README.md | 1 + docs/useStack.md | 26 +++++++++++ src/index.ts | 1 + src/useStack.ts | 93 ++++++++++++++++++++++++++++++++++++++ stories/useStack.story.tsx | 27 +++++++++++ tests/useStack.test.ts | 49 ++++++++++++++++++++ 6 files changed, 197 insertions(+) create mode 100644 docs/useStack.md create mode 100644 src/useStack.ts create mode 100644 stories/useStack.story.tsx create mode 100644 tests/useStack.test.ts diff --git a/README.md b/README.md index cc1728fafd..27f4e3ee3c 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ - [`useMap`](./docs/useMap.md) — tracks state of an object. [![][img-demo]](https://codesandbox.io/s/quirky-dewdney-gi161) - [`useSet`](./docs/useSet.md) — tracks state of a Set. [![][img-demo]](https://codesandbox.io/s/bold-shtern-6jlgw) - [`useQueue`](./docs/useQueue.md) — implements simple queue. + - [`useStack`](./docs/useStack.md) — implements simple Stack (LIFO). - [`useStateValidator`](./docs/useStateValidator.md) — tracks state of an object. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usestatevalidator--demo) - [`useStateWithHistory`](./docs/useStateWithHistory.md) — stores previous state values and provides handles to travel through them. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usestatewithhistory--demo) - [`useMultiStateValidator`](./docs/useMultiStateValidator.md) — alike the `useStateValidator`, but tracks multiple states at a time. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usemultistatevalidator--demo) diff --git a/docs/useStack.md b/docs/useStack.md new file mode 100644 index 0000000000..ef69bae74d --- /dev/null +++ b/docs/useStack.md @@ -0,0 +1,26 @@ +### useStack + +React hook implementing stack (LIFO) behavior. + +```tsx +import { useStack } from 'react-use'; + +const Demo = () => { + const [stack, { push, pop, peek, clear, reset, size }] = useStack([1, 2]); + + return ( +
+ + + + + + +
+ ); +}; + diff --git a/src/index.ts b/src/index.ts index 62b69356b7..bbcdb4b474 100644 --- a/src/index.ts +++ b/src/index.ts @@ -115,3 +115,4 @@ export { useFirstMountState } from './useFirstMountState'; export { default as useSet } from './useSet'; export { createGlobalState } from './factory/createGlobalState'; export { useHash } from './useHash'; +export { default as useStack } from './useStack'; diff --git a/src/useStack.ts b/src/useStack.ts new file mode 100644 index 0000000000..319e32b8e6 --- /dev/null +++ b/src/useStack.ts @@ -0,0 +1,93 @@ +import { useMemo, useRef } from 'react'; +import useUpdate from './useUpdate'; +import { IHookStateInitAction, IHookStateSetAction, resolveHookState } from './misc/hookState'; + +export interface StackActions { + /** + * @description Push an element onto the stack (top). + */ + push: (item: T) => void; + + /** + * @description Pop and return the top element. Returns undefined if empty. + */ + pop: () => T | undefined; + + /** + * @description Returns the top element without removing it. + */ + peek: () => T | undefined; + + /** + * @description Clear all elements from the stack. + */ + clear: () => void; + + /** + * @description Reset stack to its initial value. + */ + reset: () => void; + + /** + * @description Replace the entire stack manually. + */ + set: (newStack: IHookStateSetAction) => void; + + /** + * @description Returns the current stack size. + */ + size: () => number; +} + +/** + * @name useStack + * @description React hook that provides stack (LIFO) operations. + * @example + * const [stack, { push, pop, peek, clear, reset, size }] = useStack([1, 2]); + */ +function useStack(initialStack: IHookStateInitAction = []): [T[], StackActions] { + const stack = useRef(resolveHookState(initialStack)); + const update = useUpdate(); + + const actions = useMemo>(() => { + const a = { + set: (newStack: IHookStateSetAction) => { + stack.current = resolveHookState(newStack, stack.current); + update(); + }, + + push: (item: T) => { + a.set((curr: T[]) => curr.concat(item)); + }, + + pop: () => { + if (stack.current.length === 0) return undefined; + const item = stack.current[stack.current.length - 1]; + a.set((curr: T[]) => curr.slice(0, -1)); + return item; + }, + + peek: () => { + return stack.current[stack.current.length - 1]; + }, + + clear: () => { + a.set([]); + }, + + reset: () => { + a.set(resolveHookState(initialStack).slice()); + }, + + size: () => { + return stack.current.length; + }, + }; + + return a as StackActions; + }, []); + + return [stack.current, actions]; +} + +export default useStack; diff --git a/stories/useStack.story.tsx b/stories/useStack.story.tsx new file mode 100644 index 0000000000..948a515e85 --- /dev/null +++ b/stories/useStack.story.tsx @@ -0,0 +1,27 @@ +import { storiesOf } from '@storybook/react'; +import * as React from 'react'; +import useStack from '../src/useStack'; +import ShowDocs from './util/ShowDocs'; + +const Demo = () => { + const [stack, { push, pop, peek, clear, reset, size }] = useStack([1, 2]); + + return ( +
+
    +
  • Stack: {JSON.stringify(stack)}
  • +
  • Top: {peek()}
  • +
  • Size: {size()}
  • +
+ + + + + +
+ ); +}; + +storiesOf('State/useStack', module) + .add('Docs', () => ) + .add('Demo', () => ); diff --git a/tests/useStack.test.ts b/tests/useStack.test.ts new file mode 100644 index 0000000000..802922411e --- /dev/null +++ b/tests/useStack.test.ts @@ -0,0 +1,49 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import useStack from '../src/useStack'; + +describe('useStack', () => { + it('should initialize with initial values', () => { + const { result } = renderHook(() => useStack([1, 2])); + expect(result.current[0]).toEqual([1, 2]); + }); + + it('should push and pop elements correctly', () => { + const { result } = renderHook(() => useStack()); + const [, actions] = result.current; + + act(() => { + actions.push(10); + actions.push(20); + }); + + expect(result.current[0]).toEqual([10, 20]); + expect(actions.peek()).toBe(20); + + act(() => { + actions.pop(); + }); + + expect(result.current[0]).toEqual([10]); + }); + + it('should clear and reset stack', () => { + const { result } = renderHook(() => useStack([1, 2, 3])); + const [, actions] = result.current; + + act(() => { + actions.clear(); + }); + expect(result.current[0]).toEqual([]); + + act(() => { + actions.reset(); + }); + expect(result.current[0]).toEqual([1, 2, 3]); + }); + + it('should return correct size', () => { + const { result } = renderHook(() => useStack([1, 2])); + const [, actions] = result.current; + expect(actions.size()).toBe(2); + }); +});