Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ export const useHomeTodosByDate = (
const [todosByDate, setTodosByDate] = useState<Record<string, Todo[]>>({});
const openTimerPanel = useTimeSidebarStore((state) => state.openTimerPanel);
const queryClient = useQueryClient();
const { data: activeTimer } = useActiveTimer();
const { data: activeTimer, isFetching: isActiveTimerFetching } =
useActiveTimer();
const { mutate: changeTodoStatus } = useChangeTodoStatus();
const { mutate: changeSubtaskStatus } = useChangeSubtaskStatus();
const { mutate: reorderTodo } = useReorderTodo();
Expand All @@ -54,22 +55,27 @@ export const useHomeTodosByDate = (
invalidateFocusTodo,
} = useTimerQueryInvalidation();

const { mutate: startTimer } = useStartTimer<ApiError>({
mutation: {
onSuccess: () => {
invalidateTimerState();
invalidateFocusTodo();
const { mutate: startTimer, isPending: isStartTimerPending } =
useStartTimer<ApiError>({
mutation: {
onSuccess: () => {
invalidateTimerState();
invalidateFocusTodo();
},
},
},
});
const { mutate: changeStatus } = useChangeStatus({
mutation: {
onSuccess: () => {
invalidateTimerState();
invalidateFocusTodo();
});
const { mutate: changeStatus, isPending: isChangeStatusPending } =
useChangeStatus({
mutation: {
onSuccess: () => {
invalidateTimerState();
invalidateFocusTodo();
},
},
},
});
});

const isTimerActionPending =
isStartTimerPending || isChangeStatusPending || isActiveTimerFetching;

useEffect(() => {
setTodosByDate(
Expand Down Expand Up @@ -157,6 +163,8 @@ export const useHomeTodosByDate = (
};

const handleTogglePlay = (dateKey: string, todoId: number) => {
if (isTimerActionPending) return;

const willRun =
todosByDate[dateKey]?.find((todo) => todo.todoId === todoId)
?.timerStatus !== "RUNNING";
Expand Down Expand Up @@ -260,6 +268,7 @@ export const useHomeTodosByDate = (
return {
todosByDate,
activeTimer,
isTimerActionPending,
handleToggleCompleted,
handleTogglePlay,
handleToggleSubtaskCompleted,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export const useTodayTodoList = (
const [todos, setTodos] = useState<TodayTodo[]>(initialTodos);
const openTimerPanel = useTimeSidebarStore((state) => state.openTimerPanel);
const queryClient = useQueryClient();
const { data: activeTimer } = useActiveTimer();
const { data: activeTimer, isFetching: isActiveTimerFetching } =
useActiveTimer();
const { mutate: changeTodoStatus } = useChangeTodoStatus();
const { mutate: changeSubtaskStatus } = useChangeSubtaskStatus();
const { mutate: stopTimer } = useStopTimer();
Expand All @@ -51,22 +52,27 @@ export const useTodayTodoList = (
invalidateFocusTodo,
} = useTimerQueryInvalidation();

const { mutate: startTimer } = useStartTimer({
const { mutate: startTimer, isPending: isStartTimerPending } = useStartTimer({
mutation: {
onSuccess: () => {
invalidateTimerState();
invalidateFocusTodo();
},
},
});
const { mutate: changeStatus } = useChangeStatus({
mutation: {
onSuccess: () => {
invalidateTimerState();
invalidateFocusTodo();

const { mutate: changeStatus, isPending: isChangeStatusPending } =
useChangeStatus({
mutation: {
onSuccess: () => {
invalidateTimerState();
invalidateFocusTodo();
},
},
},
});
});

const isTimerActionPending =
isStartTimerPending || isChangeStatusPending || isActiveTimerFetching;

useEffect(() => {
setTodos(initialTodos);
Expand Down Expand Up @@ -145,6 +151,8 @@ export const useTodayTodoList = (
};

const handlePlay = (todoId: number) => {
if (isTimerActionPending) return;

const dateKey = todos.find((todo) => todo.todoId === todoId)?.date;
if (!dateKey) return;

Expand Down Expand Up @@ -231,6 +239,7 @@ export const useTodayTodoList = (
return {
todos,
activeTimer,
isTimerActionPending,
handlePlay,
handleToggleCompleted,
handleDelete,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { keepPreviousData } from "@tanstack/react-query";
import { useTranslations } from "next-intl";
import { overlay } from "overlay-kit";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

렌더링 중 ref.current 직접 수정을 지양하고 useEffectEvent를 활용해 보세요.

훌륭한 아이디어로 클로저 캡처 문제를 해결하셨네요! 센스 만점입니다. 🎉
다만, React 컴포넌트 렌더링 중에 ref.current를 직접 수정하는 것은 컴포넌트의 순수성을 해칠 수 있어 동시성(Concurrent) 렌더링 환경에서 예기치 않은 동작을 유발할 수 있습니다.

제공된 React 19.2 문서에 따르면, 이러한 경우 안정적인 이벤트 핸들러 참조를 제공하는 useEffectEvent를 사용하도록 권장하고 있습니다. 이를 활용하면 렌더링 중 사이드 이펙트 걱정 없이 코드를 훨씬 깔끔하고 안전하게 유지할 수 있습니다. 또한 코딩 가이드라인의 이벤트 핸들러 네이밍 규칙(handle 접두사)에 맞춰 handleTogglePlay로 네이밍을 조정하면 더 완벽할 것 같습니다. 최신 API를 적용해서 코드를 한 단계 더 업그레이드해 보세요! ✨

참고 문서: React 공식 문서 - useEffectEvent (또는 React 19.2 릴리스 노트)

💡 제안하는 리팩토링 코드
-import { useCallback, useEffect, useRef, useState } from "react";
+import { useCallback, useEffect, useEffectEvent, useState } from "react";

// ...

-  const onTogglePlayRef = useRef(onTogglePlay);
-  onTogglePlayRef.current = onTogglePlay;
+  const handleTogglePlay = useEffectEvent(() => {
+    onTogglePlay();
+  });

// ...

-        onTogglePlay={() => onTogglePlayRef.current()}
+        onTogglePlay={handleTogglePlay}

Also applies to: 156-158, 177-177

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx` at
line 6, Replace the render-time ref.current mutation used for the play toggle
with React’s useEffectEvent, and rename the resulting handler to
handleTogglePlay. Update the affected effect and callback dependencies/usages to
invoke handleTogglePlay while preserving the existing toggle behavior and
closure-safety.

Source: Path instructions


import type { ErrorType } from "@/api/client/custom-instance";
import type { ErrorDto, TodoUpdateRequest } from "@/api/generated/models";
Expand Down Expand Up @@ -153,6 +153,9 @@ export const DetailTodoModalContainer = ({
const [actionErrorMessage, setActionErrorMessage] = useState("");
const [isActionErrorToastOpen, setIsActionErrorToastOpen] = useState(false);

const onTogglePlayRef = useRef(onTogglePlay);
onTogglePlayRef.current = onTogglePlay;

const showActionErrorToast = useCallback(
(error: ErrorType<ErrorDto>) => {
setActionErrorMessage(
Expand All @@ -171,7 +174,7 @@ export const DetailTodoModalContainer = ({
isOpen={isOpen}
onClose={close}
onExited={unmount}
onTogglePlay={onTogglePlay}
onTogglePlay={() => onTogglePlayRef.current()}
onToggleCompleted={onToggleCompleted}
onDelete={onDelete}
onActionError={showActionErrorToast}
Expand Down
Loading