diff --git a/packages/views/settings/components/use-auto-save.test.tsx b/packages/views/settings/components/use-auto-save.test.tsx
index 513bf8f6b..55ea0f16d 100644
--- a/packages/views/settings/components/use-auto-save.test.tsx
+++ b/packages/views/settings/components/use-auto-save.test.tsx
@@ -1,3 +1,4 @@
+import { StrictMode } from "react";
import { act, render, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { useAutoSave } from "./use-auto-save";
@@ -53,6 +54,23 @@ describe("useAutoSave success feedback", () => {
});
});
+ it("still reports success under StrictMode's double-invoked mount", async () => {
+ // StrictMode runs setup/cleanup/setup on mount. A mount effect that only
+ // clears the mounted flag in cleanup leaves it false for the component's
+ // life, so a successful save is silently dropped: neither the "saved"
+ // status nor onSuccess ever fires and the indicator spins forever.
+ const onSave = vi.fn(async () => undefined);
+ const onSuccess = vi.fn();
+ render(
+
+
+ ,
+ );
+
+ await waitFor(() => expect(onSave).toHaveBeenCalledWith("saved"));
+ await waitFor(() => expect(onSuccess).toHaveBeenCalledWith("saved"));
+ });
+
it("waits for the latest queued value before reporting success", async () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
const first = deferred();
diff --git a/packages/views/settings/components/use-auto-save.ts b/packages/views/settings/components/use-auto-save.ts
index b0c1b0874..a8656213d 100644
--- a/packages/views/settings/components/use-auto-save.ts
+++ b/packages/views/settings/components/use-auto-save.ts
@@ -131,13 +131,16 @@ export function useAutoSave({
};
}, [delay, enabled, isEqual, runSave, value]);
- useEffect(
- () => () => {
+ useEffect(() => {
+ // Restore the invariant on every mount. Under React StrictMode the initial
+ // setup/cleanup/setup cycle would otherwise leave mountedRef pinned to false
+ // for the component's whole life, stranding status at "saving".
+ mountedRef.current = true;
+ return () => {
mountedRef.current = false;
if (timerRef.current) clearTimeout(timerRef.current);
- },
- [],
- );
+ };
+ }, []);
return { status, flush, saveNow };
}