diff --git a/frontend/src/components/Recipe/RecipeFavorite/RecipeFavorite.tsx b/frontend/src/components/Recipe/RecipeFavoriteButton/RecipeFavoriteButton.tsx
similarity index 65%
rename from frontend/src/components/Recipe/RecipeFavorite/RecipeFavorite.tsx
rename to frontend/src/components/Recipe/RecipeFavoriteButton/RecipeFavoriteButton.tsx
index 703e209c..b4cab603 100644
--- a/frontend/src/components/Recipe/RecipeFavorite/RecipeFavorite.tsx
+++ b/frontend/src/components/Recipe/RecipeFavoriteButton/RecipeFavoriteButton.tsx
@@ -4,7 +4,6 @@ import styled from 'styled-components';
import { SvgIcon } from '@/components/Common';
import { useTimeout } from '@/hooks/common';
-import { useToastActionContext } from '@/hooks/context';
import { useRecipeFavoriteMutation } from '@/hooks/queries/recipe';
interface RecipeFavoriteProps {
@@ -13,23 +12,28 @@ interface RecipeFavoriteProps {
recipeId: number;
}
-const RecipeFavorite = ({ recipeId, favorite, favoriteCount }: RecipeFavoriteProps) => {
- const [isFavorite, setIsFavorite] = useState(favorite);
- const [currentFavoriteCount, setCurrentFavoriteCount] = useState(favoriteCount);
- const { toast } = useToastActionContext();
+const RecipeFavoriteButton = ({ recipeId, favorite, favoriteCount }: RecipeFavoriteProps) => {
+ const initialFavoriteState = {
+ isFavorite: favorite,
+ currentFavoriteCount: favoriteCount,
+ };
+
+ const [favoriteInfo, setFavoriteInfo] = useState(initialFavoriteState);
const { mutate } = useRecipeFavoriteMutation(Number(recipeId));
+ const { isFavorite, currentFavoriteCount } = favoriteInfo;
const handleToggleFavorite = async () => {
+ setFavoriteInfo((prev) => ({
+ isFavorite: !prev.isFavorite,
+ currentFavoriteCount: isFavorite ? prev.currentFavoriteCount - 1 : prev.currentFavoriteCount + 1,
+ }));
+
mutate(
{ favorite: !isFavorite },
{
- onSuccess: () => {
- setIsFavorite((prev) => !prev);
- setCurrentFavoriteCount((prev) => (isFavorite ? prev - 1 : prev + 1));
- },
onError: () => {
- toast.error('꿀조합 좋아요를 다시 시도해주세요.');
+ setFavoriteInfo(initialFavoriteState);
},
}
);
@@ -51,7 +55,7 @@ const RecipeFavorite = ({ recipeId, favorite, favoriteCount }: RecipeFavoritePro
);
};
-export default RecipeFavorite;
+export default RecipeFavoriteButton;
const FavoriteButton = styled(Button)`
display: flex;
diff --git a/frontend/src/components/Recipe/index.ts b/frontend/src/components/Recipe/index.ts
index b7976120..ff9566ec 100644
--- a/frontend/src/components/Recipe/index.ts
+++ b/frontend/src/components/Recipe/index.ts
@@ -4,7 +4,7 @@ export { default as RecipeUsedProducts } from './RecipeUsedProducts/RecipeUsedPr
export { default as RecipeItem } from './RecipeItem/RecipeItem';
export { default as RecipeList } from './RecipeList/RecipeList';
export { default as RecipeRegisterForm } from './RecipeRegisterForm/RecipeRegisterForm';
-export { default as RecipeFavorite } from './RecipeFavorite/RecipeFavorite';
+export { default as RecipeFavoriteButton } from './RecipeFavoriteButton/RecipeFavoriteButton';
export { default as CommentItem } from './CommentItem/CommentItem';
export { default as CommentForm } from './CommentForm/CommentForm';
export { default as CommentList } from './CommentList/CommentList';
diff --git a/frontend/src/hooks/queries/recipe/useRecipeFavoriteMutation.ts b/frontend/src/hooks/queries/recipe/useRecipeFavoriteMutation.ts
index c97b392a..00b0255e 100644
--- a/frontend/src/hooks/queries/recipe/useRecipeFavoriteMutation.ts
+++ b/frontend/src/hooks/queries/recipe/useRecipeFavoriteMutation.ts
@@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { recipeApi } from '@/apis';
+import { useToastActionContext } from '@/hooks/context';
import type { RecipeFavoriteRequestBody } from '@/types/recipe';
const headers = { 'Content-Type': 'application/json' };
@@ -11,10 +12,30 @@ const patchRecipeFavorite = (recipeId: number, body: RecipeFavoriteRequestBody)
const useRecipeFavoriteMutation = (recipeId: number) => {
const queryClient = useQueryClient();
+ const { toast } = useToastActionContext();
+
+ const queryKey = ['recipeDetail', recipeId];
return useMutation({
mutationFn: (body: RecipeFavoriteRequestBody) => patchRecipeFavorite(recipeId, body),
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ['recipeDetail', recipeId] }),
+ onMutate: async (newFavoriteRequest) => {
+ await queryClient.cancelQueries({ queryKey: queryKey });
+
+ const previousRequest = queryClient.getQueryData(queryKey);
+ queryClient.setQueryData(queryKey, newFavoriteRequest);
+
+ return { previousRequest };
+ },
+ onError: (error, _, context) => {
+ queryClient.setQueryData(queryKey, context?.previousRequest);
+ if (error instanceof Error) {
+ toast.error(error.message);
+ return;
+ }
+
+ toast.error('꿀조합 좋아요를 다시 시도해주세요.');
+ },
+ onSettled: () => queryClient.invalidateQueries({ queryKey: queryKey }),
});
};
diff --git a/frontend/src/pages/RecipeDetailPage.tsx b/frontend/src/pages/RecipeDetailPage.tsx
index 1f98f5d9..4b4fbb66 100644
--- a/frontend/src/pages/RecipeDetailPage.tsx
+++ b/frontend/src/pages/RecipeDetailPage.tsx
@@ -6,7 +6,7 @@ import styled from 'styled-components';
import RecipePreviewImage from '@/assets/plate.svg';
import { ErrorBoundary, ErrorComponent, Loading, SectionTitle } from '@/components/Common';
-import { CommentForm, CommentList, RecipeFavorite } from '@/components/Recipe';
+import { CommentForm, CommentList, RecipeFavoriteButton } from '@/components/Recipe';
import { useRecipeDetailQuery } from '@/hooks/queries/recipe';
import { getFormattedDate } from '@/utils/date';
@@ -46,7 +46,7 @@ export const RecipeDetailPage = () => {
{getFormattedDate(createdAt)}
-
+