From 4f54fb160add3cbb331ef9c4bbb13d93573f451f Mon Sep 17 00:00:00 2001 From: park gunhyung <162693556+rjsgud49@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:06:54 +0900 Subject: [PATCH] fix(useLoading): stay loading until every overlapping promise settles --- .changeset/use-loading-overlapping-calls.md | 5 ++ .../src/hooks/useLoading/useLoading.spec.ts | 68 +++++++++++++++++++ .../src/hooks/useLoading/useLoading.ts | 7 +- 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 .changeset/use-loading-overlapping-calls.md diff --git a/.changeset/use-loading-overlapping-calls.md b/.changeset/use-loading-overlapping-calls.md new file mode 100644 index 00000000..3a860fcb --- /dev/null +++ b/.changeset/use-loading-overlapping-calls.md @@ -0,0 +1,5 @@ +--- +'react-simplikit': patch +--- + +`useLoading` now keeps `loading` as `true` until every promise passed to `startLoading` has settled. Before, when two calls overlapped, the first promise to settle set `loading` to `false` while the other was still pending. diff --git a/packages/react-simplikit/src/hooks/useLoading/useLoading.spec.ts b/packages/react-simplikit/src/hooks/useLoading/useLoading.spec.ts index 0cd2e3ec..1b82613d 100644 --- a/packages/react-simplikit/src/hooks/useLoading/useLoading.spec.ts +++ b/packages/react-simplikit/src/hooks/useLoading/useLoading.spec.ts @@ -82,4 +82,72 @@ describe('useLoading', () => { // No errors should occur, and the loading state should not update after unmount expect(true).toBe(true); }); + it('should stay loading until every overlapping promise has settled', async () => { + const { result } = await renderHookSSR(() => useLoading()); + const [, startLoading] = result.current; + + let resolveFirst: () => void = () => {}; + let resolveSecond: () => void = () => {}; + const first = new Promise(resolve => { + resolveFirst = resolve; + }); + const second = new Promise(resolve => { + resolveSecond = resolve; + }); + + await act(async () => { + startLoading(first); + startLoading(second); + }); + + expect(result.current[0]).toBe(true); + + await act(async () => { + resolveFirst(); + await first; + }); + + expect(result.current[0]).toBe(true); + + await act(async () => { + resolveSecond(); + await second; + }); + + expect(result.current[0]).toBe(false); + }); + + it('should stay loading while another promise is pending after one rejects', async () => { + const { result } = await renderHookSSR(() => useLoading()); + const [, startLoading] = result.current; + + let rejectFirst: (error: Error) => void = () => {}; + let resolveSecond: () => void = () => {}; + const first = new Promise((_, reject) => { + rejectFirst = reject; + }); + const second = new Promise(resolve => { + resolveSecond = resolve; + }); + + let firstResult: Promise = Promise.resolve(); + await act(async () => { + firstResult = startLoading(first); + startLoading(second); + }); + + await act(async () => { + rejectFirst(new Error('failed')); + await expect(firstResult).rejects.toThrow('failed'); + }); + + expect(result.current[0]).toBe(true); + + await act(async () => { + resolveSecond(); + await second; + }); + + expect(result.current[0]).toBe(false); + }); }); diff --git a/packages/react-simplikit/src/hooks/useLoading/useLoading.ts b/packages/react-simplikit/src/hooks/useLoading/useLoading.ts index 46ccfdbf..df17c93c 100644 --- a/packages/react-simplikit/src/hooks/useLoading/useLoading.ts +++ b/packages/react-simplikit/src/hooks/useLoading/useLoading.ts @@ -33,15 +33,20 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; export function useLoading(): [boolean, (promise: Promise) => Promise] { const [loading, setLoading] = useState(false); const ref = useIsMountedRef(); + // Counts the promises that have not settled yet, so that `loading` stays `true` + // until the last of several overlapping calls finishes. + const pendingCountRef = useRef(0); const startTransition = useCallback( async (promise: Promise) => { + pendingCountRef.current += 1; try { setLoading(true); const data = await promise; return data; } finally { - if (ref.isMounted) { + pendingCountRef.current -= 1; + if (ref.isMounted && pendingCountRef.current === 0) { setLoading(false); } }