From a28fc262d5503f52e8a13b8f9e1b2cddc09565b5 Mon Sep 17 00:00:00 2001 From: eunwoo-levi Date: Wed, 18 Mar 2026 15:00:34 +0900 Subject: [PATCH 1/5] fix(core/hooks): prevent immediate callback from re-firing when enabled is toggled The immediate effect depended on `enabled`, so the callback fired again every time `enabled` went from false to true. Added `immediateCalledRef` to ensure the callback is called only once per interval lifecycle. Co-Authored-By: Claude Sonnet 4.6 --- .../core/src/hooks/useInterval/useInterval.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/core/src/hooks/useInterval/useInterval.ts b/packages/core/src/hooks/useInterval/useInterval.ts index 996dbaf0..061f2b4a 100644 --- a/packages/core/src/hooks/useInterval/useInterval.ts +++ b/packages/core/src/hooks/useInterval/useInterval.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { usePreservedCallback } from '../usePreservedCallback/index.ts'; @@ -45,11 +45,19 @@ export function useInterval(callback: () => void, options: IntervalOptions) { const enabled = typeof options === 'number' ? true : (options.enabled ?? true); const preservedCallback = usePreservedCallback(callback); + const immediateCalledRef = useRef(false); - useEffect(() => { - if (immediate === true && enabled) { - preservedCallback(); + useEffect(function callImmediately() { + if (immediate !== true || !enabled) { + return; + } + + if (immediateCalledRef.current) { + return; } + + immediateCalledRef.current = true; + preservedCallback(); }, [immediate, preservedCallback, enabled]); useEffect(() => { From 0d1e774ccda20121f870f91b26815686d4198085 Mon Sep 17 00:00:00 2001 From: eunwoo-levi Date: Wed, 18 Mar 2026 15:00:39 +0900 Subject: [PATCH 2/5] test(core/hooks): add test for immediate callback not re-firing on enabled toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers the missing case where immediate=true and enabled is toggled false → true after mount — the callback should fire only once. Co-Authored-By: Claude Sonnet 4.6 --- .../src/hooks/useInterval/useInterval.spec.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/core/src/hooks/useInterval/useInterval.spec.ts b/packages/core/src/hooks/useInterval/useInterval.spec.ts index 9d58ff27..e989766a 100644 --- a/packages/core/src/hooks/useInterval/useInterval.spec.ts +++ b/packages/core/src/hooks/useInterval/useInterval.spec.ts @@ -124,6 +124,26 @@ describe('useInterval', () => { }); }); + it('should not re-fire immediate callback when enabled is toggled', async () => { + const callback = vi.fn(); + const { rerender } = await renderHookSSR( + ({ enabled }) => + useInterval(callback, { + delay: 1000, + immediate: true, + enabled, + }), + { initialProps: { enabled: true } } + ); + + expect(callback).toHaveBeenCalledTimes(1); + + rerender({ enabled: false }); + rerender({ enabled: true }); + + expect(callback).toHaveBeenCalledTimes(1); + }); + it('should handle enabled flag changes appropriately', async () => { const callback = vi.fn(); const { rerender } = await renderHookSSR( From c3f6786c59d8dee58d4ccc90c8698187dfb50a72 Mon Sep 17 00:00:00 2001 From: eunwoo-levi Date: Wed, 18 Mar 2026 15:08:26 +0900 Subject: [PATCH 3/5] chore: apply lint/format fixes --- .../core/src/hooks/useInterval/useInterval.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/core/src/hooks/useInterval/useInterval.ts b/packages/core/src/hooks/useInterval/useInterval.ts index 061f2b4a..f6fc0f6e 100644 --- a/packages/core/src/hooks/useInterval/useInterval.ts +++ b/packages/core/src/hooks/useInterval/useInterval.ts @@ -47,18 +47,21 @@ export function useInterval(callback: () => void, options: IntervalOptions) { const preservedCallback = usePreservedCallback(callback); const immediateCalledRef = useRef(false); - useEffect(function callImmediately() { - if (immediate !== true || !enabled) { - return; - } + useEffect( + function callImmediately() { + if (immediate !== true || !enabled) { + return; + } - if (immediateCalledRef.current) { - return; - } + if (immediateCalledRef.current) { + return; + } - immediateCalledRef.current = true; - preservedCallback(); - }, [immediate, preservedCallback, enabled]); + immediateCalledRef.current = true; + preservedCallback(); + }, + [immediate, preservedCallback, enabled] + ); useEffect(() => { if (!enabled) { From 2727b48340b0babc48db29cb0bf2601b23907109 Mon Sep 17 00:00:00 2001 From: eunwoo-levi Date: Wed, 18 Mar 2026 15:09:29 +0900 Subject: [PATCH 4/5] chore: add changeset for useInterval immediate fix --- .changeset/lovely-spies-change.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lovely-spies-change.md diff --git a/.changeset/lovely-spies-change.md b/.changeset/lovely-spies-change.md new file mode 100644 index 00000000..cd1a354c --- /dev/null +++ b/.changeset/lovely-spies-change.md @@ -0,0 +1,5 @@ +--- +'react-simplikit': patch +--- + +fix(core/hooks): prevent immediate callback from re-firing when enabled is toggled From 776c5303af7641e9488c51302c0892fa19996662 Mon Sep 17 00:00:00 2001 From: eunwoo-levi Date: Wed, 18 Mar 2026 15:30:01 +0900 Subject: [PATCH 5/5] refactor(core/hooks): rename immediate effect function to runImmediateCallback --- packages/core/src/hooks/useInterval/useInterval.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/hooks/useInterval/useInterval.ts b/packages/core/src/hooks/useInterval/useInterval.ts index f6fc0f6e..e47871b1 100644 --- a/packages/core/src/hooks/useInterval/useInterval.ts +++ b/packages/core/src/hooks/useInterval/useInterval.ts @@ -48,7 +48,7 @@ export function useInterval(callback: () => void, options: IntervalOptions) { const immediateCalledRef = useRef(false); useEffect( - function callImmediately() { + function runImmediateCallback() { if (immediate !== true || !enabled) { return; }