From c100b4c59426480797450fb9b4ea388a7fc61ab4 Mon Sep 17 00:00:00 2001 From: AMin Date: Sat, 24 Oct 2026 01:35:01 +0800 Subject: [PATCH 1/2] fix(useStateWithHistory): update position after trimming history --- src/useStateWithHistory.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/useStateWithHistory.ts b/src/useStateWithHistory.ts index 7e2b442cbf..2199db55cf 100644 --- a/src/useStateWithHistory.ts +++ b/src/useStateWithHistory.ts @@ -66,12 +66,14 @@ export function useStateWithHistory( history.current = history.current.slice(0, historyPosition.current + 1); } - historyPosition.current = history.current.push(newState as I) - 1; + history.current.push(newState as I); // if capacity is reached - shift first elements if (history.current.length > capacity) { history.current = history.current.slice(history.current.length - capacity); } + + historyPosition.current = history.current.length - 1; } return newState; From c5bb9ecffebb354b802704295f9efed41da2f21f Mon Sep 17 00:00:00 2001 From: AMin Date: Sat, 24 Oct 2026 01:35:27 +0800 Subject: [PATCH 2/2] test(useStateWithHistory): cover capacity overflow and navigation --- tests/useStateWithHistory.test.ts | 75 +++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/useStateWithHistory.test.ts b/tests/useStateWithHistory.test.ts index 5261daa8dc..a5f306490e 100644 --- a/tests/useStateWithHistory.test.ts +++ b/tests/useStateWithHistory.test.ts @@ -106,6 +106,81 @@ describe('useStateWithHistory', () => { expect(hook.result.current[0][2].history).toEqual([5, 111, 321]); }); + it('should keep the position at the newest entry when history reaches capacity', () => { + const { result } = getHook(0, 3); + + for (let value = 1; value <= 5; value++) { + act(() => { + result.current[0][1](value); + }); + + const [state, , history] = result.current[0]; + expect(history.position).toBe(history.history.length - 1); + expect(history.history[history.position]).toBe(state); + } + + expect(result.current[0][2].history).toEqual([3, 4, 5]); + + act(() => { + result.current[0][2].back(); + }); + expect(result.current[0][0]).toBe(4); + expect(result.current[0][2].position).toBe(1); + + act(() => { + result.current[0][2].forward(); + }); + expect(result.current[0][0]).toBe(5); + expect(result.current[0][2].position).toBe(2); + }); + + it('should keep a single-entry history at position zero after setting state', () => { + const { result } = getHook(0, 1); + + act(() => { + result.current[0][1](1); + }); + + expect(result.current[0][2].history).toEqual([1]); + expect(result.current[0][2].position).toBe(0); + + act(() => { + result.current[0][1](1); + }); + expect(result.current[0][2].history).toEqual([1]); + expect(result.current[0][2].position).toBe(0); + + act(() => { + result.current[0][2].back(); + }); + expect(result.current[0][0]).toBe(1); + expect(result.current[0][2].position).toBe(0); + }); + + it('should allow navigation after reducing capacity and setting state', () => { + const { result, rerender } = getHook(5, 5, [1, 2, 3, 4, 5]); + rerender({ state: 5, capacity: 2 }); + + act(() => { + result.current[0][1](6); + }); + + expect(result.current[0][2].history).toEqual([5, 6]); + expect(result.current[0][2].position).toBe(1); + + act(() => { + result.current[0][2].back(); + }); + expect(result.current[0][0]).toBe(5); + expect(result.current[0][2].position).toBe(0); + + act(() => { + result.current[0][2].forward(); + }); + expect(result.current[0][0]).toBe(6); + expect(result.current[0][2].position).toBe(1); + }); + describe('history.back()', () => { it('should cause rerender', () => { const hook = getHook(5, 5, [1, 2, 3, 4, 5]);