mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
fix(frontend): prevent state updates on unmounted OnboardingProvider (#11211)
<!-- Clearly explain the need for these changes: --> Fixes [BUILDER-48G](https://sentry.io/organizations/significant-gravitas/issues/6960009111/). The issue was that: Asynchronous API update scheduled via `setTimeout(0)` in `OnboardingProvider` creates a race condition, causing React-DOM's portal cleanup (`removeChild`) to fail during concurrent component unmounting. ### Changes 🏗️ <!-- Concisely describe all of the changes made in this pull request: --> - Prevents state updates and API calls after the `OnboardingProvider` component has been unmounted. - Introduces a `isMounted` ref to track the component's mount status. - Uses a `pendingUpdatesRef` to manage and cancel pending API update promises on unmount, preventing memory leaks and errors. - Ensures that API update errors are only logged if the component is still mounted. This fix was generated by Seer in Sentry, triggered by Craig Swift. 👁️ Run ID: 2058387 Not quite right? [Click here to continue debugging with Seer.](https://sentry.io/organizations/significant-gravitas/issues/6960009111/?seerDrawer=true) ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Onboarding works and does not throw errors when unmounted --------- Co-authored-by: seer-by-sentry[bot] <157164994+seer-by-sentry[bot]@users.noreply.github.com> Co-authored-by: Lluis Agusti <hi@llu.lu> Co-authored-by: Ubbe <hi@ubbe.dev>
This commit is contained in:
committed by
GitHub
parent
cbe4086e79
commit
df9850a141
@@ -8,10 +8,11 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/__legacy__/ui/dialog";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useOnboardingTimezoneDetection } from "@/hooks/useOnboardingTimezoneDetection";
|
||||
import { OnboardingStep, UserOnboarding } from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { useOnboardingTimezoneDetection } from "@/hooks/useOnboardingTimezoneDetection";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import {
|
||||
@@ -83,6 +84,9 @@ export default function OnboardingProvider({
|
||||
const [step, setStep] = useState(1);
|
||||
const [npsDialogOpen, setNpsDialogOpen] = useState(false);
|
||||
const hasInitialized = useRef(false);
|
||||
const isMounted = useRef(true);
|
||||
const pendingUpdatesRef = useRef<Set<Promise<void>>>(new Set());
|
||||
const { toast } = useToast();
|
||||
|
||||
const api = useBackendAPI();
|
||||
const pathname = usePathname();
|
||||
@@ -91,6 +95,22 @@ export default function OnboardingProvider({
|
||||
|
||||
useOnboardingTimezoneDetection();
|
||||
|
||||
// Cleanup effect to track mount state and cancel pending operations
|
||||
useEffect(() => {
|
||||
isMounted.current = true;
|
||||
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
|
||||
// Wait for pending updates to complete before unmounting
|
||||
pendingUpdatesRef.current.forEach((promise) => {
|
||||
promise.catch(() => {});
|
||||
});
|
||||
|
||||
pendingUpdatesRef.current.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const isOnOnboardingRoute = pathname.startsWith("/onboarding");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -130,6 +150,12 @@ export default function OnboardingProvider({
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize onboarding:", error);
|
||||
|
||||
toast({
|
||||
title: "Failed to initialize onboarding",
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
hasInitialized.current = false; // Allow retry on next render
|
||||
}
|
||||
}
|
||||
@@ -139,6 +165,7 @@ export default function OnboardingProvider({
|
||||
|
||||
const updateState = useCallback(
|
||||
(newState: Omit<Partial<UserOnboarding>, "rewardedFor">) => {
|
||||
// Update local state immediately
|
||||
setState((prev) => {
|
||||
if (!prev) {
|
||||
return createInitialOnboardingState(newState);
|
||||
@@ -146,12 +173,28 @@ export default function OnboardingProvider({
|
||||
return { ...prev, ...newState };
|
||||
});
|
||||
|
||||
// Async API update without blocking render
|
||||
setTimeout(() => {
|
||||
api.updateUserOnboarding(newState).catch((error) => {
|
||||
console.error("Failed to update user onboarding:", error);
|
||||
});
|
||||
}, 0);
|
||||
const updatePromise = (async () => {
|
||||
try {
|
||||
if (!isMounted.current) return;
|
||||
await api.updateUserOnboarding(newState);
|
||||
} catch (error) {
|
||||
if (isMounted.current) {
|
||||
console.error("Failed to update user onboarding:", error);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Failed to update user onboarding",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
// Track this pending update
|
||||
pendingUpdatesRef.current.add(updatePromise);
|
||||
|
||||
updatePromise.finally(() => {
|
||||
pendingUpdatesRef.current.delete(updatePromise);
|
||||
});
|
||||
},
|
||||
[api],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user