Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ node_modules
dist
dist-ssr
*.local
.env*

# Editor directories and files
.vscode/*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { useEffectOnce } from '@endolphin/core/hooks';
import { useQueryClient } from '@tanstack/react-query';
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { useEffect } from 'react';
import { useCallback } from 'react';

import { calendarKeys } from '@/features/my-calendar/api/keys';

const Redirect = () => {
const queryClient = useQueryClient();
const navigate = useNavigate();

useEffect(() => {
const clearCalendarCache = useCallback(() => {
(async () => {
await queryClient.invalidateQueries({ queryKey: calendarKeys.all });
navigate({ to: '/my-calendar' });
})();
}, [queryClient, navigate]);

useEffectOnce({ callback: clearCalendarCache });

return null;
};
Expand Down
16 changes: 10 additions & 6 deletions frontend/apps/client/src/routes/oauth.redirect/login/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffectOnce } from '@endolphin/core/hooks';
import { createFileRoute, useSearch } from '@tanstack/react-router';
import { useEffect } from 'react';
import { useCallback } from 'react';

import { useJWTMutation } from '@/features/login/api/mutations';
import { getLastRoutePath } from '@/utils/route';
Expand All @@ -10,11 +11,14 @@ const Redirect = () => {
const params: { code: string } = useSearch({ from: '/oauth/redirect/login/' });
const { code } = params;

useEffect(() => {
if (code) {
loginMutate({ code, lastPath });
}
}, [code, loginMutate, lastPath]);
const loginCache = useCallback(() => {
loginMutate({ code, lastPath });
}, [loginMutate, code, lastPath]);

useEffectOnce({
condition: Boolean(code),
callback: loginCache,
});

return null;
};
Expand Down
1 change: 1 addition & 0 deletions frontend/packages/core/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './useClickOutside';
export * from './useEffectOnce';
export * from './useSafeContext';
export * from './useSelectTime';
28 changes: 28 additions & 0 deletions frontend/packages/core/src/hooks/useEffectOnce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect, useRef } from 'react';

interface UseEffectOnceOptions {
condition?: boolean;
callback: () => void;
}

/**
*
* @param options
* @param options.condition - 콜백을 실행할 조건 (기본값: true)
* @param options.callback - 한 번만 실행할 콜백 함수. useCallback을 사용하여 메모이제이션하는 것이 좋습니다.
*/
export const useEffectOnce = ({ condition = true, callback }: UseEffectOnceOptions) => {
const isCalled = useRef(false);

useEffect(() => {
if (condition && !isCalled.current) {
callback();
}

return () => {
if (!isCalled.current) {
isCalled.current = true;
}
};
}, [condition, callback]);
Comment on lines +17 to +27
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix critical logic error in the hook implementation.

The current implementation has a fundamental flaw in the cleanup function logic that breaks the hook's intended behavior.

Issues:

  1. isCalled.current is not set to true immediately after calling the callback
  2. The cleanup function incorrectly sets the flag when the callback wasn't called
  3. This prevents the hook from working correctly with conditional execution

Example of the bug:
If condition is initially false, the effect runs but doesn't call the callback. The cleanup then sets isCalled.current = true, preventing future execution even when condition becomes true.

Apply this fix:

 useEffect(() => {
   if (condition && !isCalled.current) {
+    isCalled.current = true;
     callback();
   }
-
-  return () => {
-    if (!isCalled.current) {
-      isCalled.current = true;
-    }
-  };
 }, [condition, callback]);

This ensures the callback runs only once when the condition is met, which aligns with the hook's purpose and StrictMode compatibility goals.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (condition && !isCalled.current) {
callback();
}
return () => {
if (!isCalled.current) {
isCalled.current = true;
}
};
}, [condition, callback]);
useEffect(() => {
if (condition && !isCalled.current) {
isCalled.current = true;
callback();
}
}, [condition, callback]);
🤖 Prompt for AI Agents
In frontend/packages/core/src/hooks/useEffectOnce.ts between lines 17 and 27,
the hook's logic incorrectly sets isCalled.current to true in the cleanup
function even if the callback was not called, causing the callback to never run
when the condition later becomes true. To fix this, set isCalled.current to true
immediately after calling the callback inside the effect, and remove or adjust
the cleanup function so it does not set isCalled.current when the callback was
not executed. This ensures the callback runs only once when the condition is met
as intended.

};