Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor/124 optimistic update #127

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
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 @@ -14,8 +14,11 @@ import { SubmitHandler, useForm } from "react-hook-form";
import { useUI } from "@/components/common/uiContext";
import { notify } from "@/utils/toast";
import { ICreateComment } from "@/types";
import { ME } from "@/constants/queryKey";
import { ME, POST } from "@/constants/queryKey";
import { Spinner } from "@/components/common/Spinner";
import { _GET } from "@/api";
import { useQuery } from "@tanstack/react-query";
import { useDebounce } from "@/hooks/useDebounce";

interface ModalProps {
postId: string;
Expand Down Expand Up @@ -65,13 +68,16 @@ const PostDetailModalController = ({ props }: IPostDetailModalProps) => {
});

const { data: myData } = useInitData(ME, "/auth-user");
const { data: postData, isLoading } = useInitData(
`postId-${postId}`,
`/posts/${postId}`
);
const userId = postData?.data.author._id;
const userName = postData?.data.author.fullName;
const imageUrl = postData?.data.image;

const { data: postData, isLoading } = useQuery({
queryKey: [POST, postId],
queryFn: async () => await _GET(`/posts/${postId}`),
gcTime: 0,
});

const userId = postData?.data?.author._id;
const userName = postData?.data?.author.fullName;
const imageUrl = postData?.data?.image;

useEffect(() => {
if (postData) {
Expand Down Expand Up @@ -115,6 +121,7 @@ const PostDetailModalController = ({ props }: IPostDetailModalProps) => {
const { createLikeMutation, deleteLikeMutation } = useLikeMutation({
myId,
userId,
postId,
notify,
setLikeInfo,
likeDataBinding,
Expand Down Expand Up @@ -145,13 +152,21 @@ const PostDetailModalController = ({ props }: IPostDetailModalProps) => {
if (confirm("포슀트λ₯Ό μ‚­μ œν• κΉŒμš”? μ‚­μ œ ν›„μ—λŠ” 되돌릴 수 μ—†μŠ΅λ‹ˆλ‹€.")) {
deletePostMutation.mutate({ id: postId });
closeModal();
// TODO : 포슀트 μ‚­μ œ api톡신 ν›„ ν™ˆ 화면이 λ¦¬λ Œλ”λ§λ  수 μžˆλ„λ‘ 해야함
}
};

const handleClose = () => {
closeModal();
};

const changeServerFollowState = useDebounce(userId => {
if (!followInfo.isIFollowed) {
myId !== null && followMutation.mutate({ userId });
} else {
unfollowMutation.mutate({ id: followInfo.followId });
}
}, 400);

const handleFollow = () => {
if (!myId) {
if (confirm(`둜그인이 ν•„μš”ν•©λ‹ˆλ‹€. 둜그인 νŽ˜μ΄μ§€λ‘œ μ΄λ™ν• κΉŒμš”?`)) {
Expand All @@ -161,15 +176,26 @@ const PostDetailModalController = ({ props }: IPostDetailModalProps) => {
return;
}
if (!followInfo.isIFollowed) {
myId !== null && followMutation.mutate({ userId });
myId !== null &&
setFollowInfo(prevState => ({ ...prevState, isIFollowed: true }));
} else {
unfollowMutation.mutate({ id: followInfo.followId });
setFollowInfo(prevState => ({ ...prevState, isIFollowed: false }));
}

changeServerFollowState(userId);
};
const handleContentDetail = () => {
setIsContentDetail(true);
};

const changeServerLikeState = useDebounce(postId => {
if (postData?.data.likes.some((like: any) => like.user === myId)) {
likeInfo.isILiked && deleteLikeMutation.mutate({ id: likeInfo.myLikeId });
} else {
!likeInfo.isILiked && createLikeMutation.mutate({ postId: postId });
}
}, 400);

const handleLike = () => {
if (!myId) {
if (confirm(`둜그인이 ν•„μš”ν•©λ‹ˆλ‹€. 둜그인 νŽ˜μ΄μ§€λ‘œ μ΄λ™ν• κΉŒμš”?`)) {
Expand All @@ -178,16 +204,36 @@ const PostDetailModalController = ({ props }: IPostDetailModalProps) => {
}
return;
}
if (likeInfo.isILiked === true) {
deleteLikeMutation.mutate({ id: likeInfo.myLikeId });
}
if (likeInfo.isILiked === false) {

if (!likeInfo.isILiked) {
myId !== null &&
setLikeInfo(prevState => ({
...prevState,
isILiked: true,
count: prevState.count + 1,
}));
setHeartAnimation(previousState => ({
isShow: true,
key: previousState.key + 1,
}));
createLikeMutation.mutate({ postId: postId });
notify({
type: "success",
text: "μ’‹μ•„μš”λ₯Ό λˆŒλ €μ–΄μš”!",
});
} else {
setLikeInfo(prevState => ({
...prevState,
isILiked: false,
count: prevState.count - 1,
}));
notify({
type: "default",
text: "μ’‹μ•„μš”λ₯Ό μ·¨μ†Œν–ˆμ–΄μš”.",
});
}

changeServerLikeState(postId);
console.log(likeInfo.isILiked);
};

const handleChat = () => {
Expand Down Expand Up @@ -251,6 +297,7 @@ const PostDetailModalController = ({ props }: IPostDetailModalProps) => {
handleLike={handleLike}
isILiked={likeInfo.isILiked}
likeCount={likeInfo.count}
// likeCount={postData?.data.likes.length}
toggleShowComments={toggleShowComments}
handleChat={handleChat}
register={register}
Expand Down
119 changes: 68 additions & 51 deletions src/components/modalViews/PostDetailModal/PostDetailModal.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
INotification,
IUnfollow,
} from "@/types";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { POST } from "@/constants/queryKey";

interface ILikeInfo {
count: number;
Expand All @@ -30,6 +31,7 @@ interface IFollowInfo {
interface ILikeMuationProps {
myId: string | null;
userId: string;
postId: string;
notify: ({ type, text }: IToastProps) => void;
setLikeInfo: React.Dispatch<React.SetStateAction<ILikeInfo>>;
likeDataBinding: (data: any) => void;
Expand Down Expand Up @@ -74,19 +76,42 @@ export const useNotifyMutation = () => {
export const useLikeMutation = ({
myId,
userId,
postId,
notify,
setLikeInfo,
likeDataBinding,
}: ILikeMuationProps) => {
const queryClient = useQueryClient();
const notificationMutation = useNotifyMutation();
const createLikeMutation = useMutation({
mutationFn: async (formData: ICreateLike) => await _CREATE_LIKE(formData),
onMutate() {
setLikeInfo(prevState => ({
...prevState,
isILiked: true,
count: prevState.count + 1,
}));
onMutate: async ({ postId }) => {
queryClient.cancelQueries({ queryKey: [POST, postId] });
const previousPostData: any = queryClient.getQueryData([POST, postId]);

const previousLikes = previousPostData?.data?.likes.filter(
(like: any) => {
if (like.user !== myId) {
return like;
}
}
);

const newPostData = {
...previousPostData?.data,
likes: [
...previousLikes,
{
createdAt: "",
updatedAt: "",
post: postId,
user: myId,
__v: 0,
_id: "",
},
],
};
queryClient.setQueryData([POST, postId], { data: newPostData });

return { previousPostData };
},
onSuccess(data) {
if (myId) {
Expand All @@ -98,49 +123,51 @@ export const useLikeMutation = ({
};

notificationMutation.mutate(newNotification);
}
},
onError: (_, __, context) => {
if (context?.previousPostData) {
queryClient.setQueryData([POST, postId], context.previousPostData);
notify({
type: "success",
text: "μ’‹μ•„μš”λ₯Ό λˆŒλ €μ–΄μš”!",
type: "error",
text: "μ’‹μ•„μš” 생성에 μ‹€νŒ¨ν–ˆμ–΄μš”.",
});
likeDataBinding(data);
setLikeInfo(prevState => ({ ...prevState, myLikeId: data._id }));
}
},
onError() {
notify({
type: "error",
text: "μ’‹μ•„μš” 생성에 μ‹€νŒ¨ν–ˆμ–΄μš”.",
});
setLikeInfo(prevState => ({
...prevState,
isILiked: false,
count: prevState.count - 1,
}));
onSettled() {
queryClient.invalidateQueries({ queryKey: [POST, postId] });
},
});

const deleteLikeMutation = useMutation({
mutationFn: async (formData: IDeleteLike) => await _DELETE_LIKE(formData),
onMutate() {
setLikeInfo(prevState => ({
...prevState,
isILiked: false,
count: prevState.count - 1,
}));
},
onSuccess(data) {
notify({
type: "default",
text: "μ’‹μ•„μš”λ₯Ό μ·¨μ†Œν–ˆμ–΄μš”.",
onMutate: async ({ id }) => {
queryClient.cancelQueries({ queryKey: [POST, postId] });
const previousPostData: any = queryClient.getQueryData([POST, postId]);

const newLikes = previousPostData?.data?.likes.filter((like: any) => {
if (like._id !== id && like.user !== myId) {
return like;
}
});
likeDataBinding(data);

const newPostData = {
...previousPostData?.data,
likes: newLikes,
};

queryClient.setQueryData([POST, postId], { data: newPostData });

return { previousPostData };
},
onError() {
setLikeInfo(prevState => ({
...prevState,
isILiked: true,
count: prevState.count + 1,
}));

onError: (_, __, context) => {
if (context?.previousPostData) {
queryClient.setQueryData([POST, postId], context.previousPostData);
}
},
onSettled() {
queryClient.invalidateQueries({ queryKey: [POST, postId] });
},
});

Expand Down Expand Up @@ -174,7 +201,6 @@ export const useCommentMutation = ({

notificationMutation.mutate(newNotification);
setComments(newComments);
console.log(newComments);
}
},
onError(error) {
Expand All @@ -189,7 +215,6 @@ export const useCommentMutation = ({
type: "default",
text: "λŒ“κΈ€μ„ μ‚­μ œν–ˆμ–΄μš”.",
});
console.log("API : λŒ“κΈ€ μ‚­μ œ 성곡", data);
const newComments = comments.filter(({ _id }: any) => _id !== data._id);
setComments(newComments);
},
Expand Down Expand Up @@ -224,10 +249,6 @@ export const useFollowMutation = ({

notificationMutation.mutate(newNotification);
}
notify({
type: "success",
text: "νŒ”λ‘œμš°λ₯Ό μ„±κ³΅ν–ˆμ–΄μš”.",
});
setFollowInfo(prevState => ({ ...prevState, followId: data._id }));
},
onError(error) {
Expand All @@ -246,10 +267,6 @@ export const useFollowMutation = ({
setFollowInfo(prevState => ({ ...prevState, isIFollowed: false }));
},
onSuccess() {
notify({
type: "default",
text: "νŒ”λ‘œμš°λ₯Ό ν•΄μ œν–ˆμ–΄μš”.",
});
setFollowInfo(prevState => ({ ...prevState, followId: "" }));
},
onError(error) {
Expand Down
1 change: 1 addition & 0 deletions src/constants/queryKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export const ME = "me";
export const NOTIFICATION = "notification";
export const CHATS = "chats";
export const CHAT = "chat";
export const POST = "post";
20 changes: 20 additions & 0 deletions src/hooks/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useCallback, useRef } from "react";

export const useDebounce = (
callback: (...params: any) => void,
delay: number
) => {
const timer = useRef<any>(null);
return useCallback(
(...params: any) => {
const later = () => {
clearTimeout(timer.current);
callback(...params);
};

clearTimeout(timer.current);
timer.current = setTimeout(later, delay);
},
[callback, delay]
);
};