Skip to content

Commit

Permalink
Refactor: [UI/UX 개선] 팔로우버튼, 콜렉트버튼 - optimistic UI 적용 (#223)
Browse files Browse the repository at this point in the history
* Refactor: 프로필 팔로우 기능에 optimistic updates 기능 적용

* Refactor: 프로필 팔로우취소 기능에 optimistic updates 기능 적용 및 하나의 mutation으로 수정

* Refactor: 리스트상세 콜렉트기능(북마크)에 optimistic updates 적용

* Fix: 팔로우 버튼 optimistic updates 일부 조건문 수정

* Refactor: 탐색페이지 팔로우 기능에 optimistic updates 적용
  • Loading branch information
ParkSohyunee authored and Eugene-A-01 committed Apr 20, 2024
1 parent 613f741 commit 3efc8e0
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 68 deletions.
40 changes: 29 additions & 11 deletions src/app/list/[listId]/_components/ListDetailInner/CollectButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import LoginModal from '@/components/login/LoginModal';
import useBooleanOutput from '@/hooks/useBooleanOutput';
import { useLanguage } from '@/store/useLanguage';
import toastMessage from '@/lib/constants/toastMessage';
import { ListDetailType } from '@/lib/types/listType';

interface CollectProps {
ownerId: number;
Expand All @@ -33,20 +34,39 @@ const CollectButton = ({ data }: { data: CollectProps }) => {
const collect = useMutation({
mutationKey: [QUERY_KEYS.collect, data.listId],
mutationFn: () => collectList(data.listId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.getListDetail],
});
onMutate: async (listId: string) => {
await queryClient.cancelQueries({ queryKey: [QUERY_KEYS.getListDetail, listId] });
const previousListDetail: ListDetailType | undefined = queryClient.getQueryData([
QUERY_KEYS.getListDetail,
listId,
]);

if (!previousListDetail) return;

const nextData = {
...previousListDetail,
isCollected: !data.isCollected,
collectCount: data.isCollected ? previousListDetail?.collectCount - 1 : previousListDetail?.collectCount + 1,
};

queryClient.setQueryData([QUERY_KEYS.getListDetail, listId], nextData);
return { previousListDetail };
},
onError: (error: AxiosError) => {
onError: (error: AxiosError, listId, context) => {
if (error.response?.status === 401) {
toasting({ type: 'warning', txt: toastMessage[language].failedCollect });
}
queryClient.setQueryData([QUERY_KEYS.getListDetail, listId], context?.previousListDetail);
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.getListDetail, data.listId + ''],
});
},
});

const handleCollect = () => {
collect.mutate();
collect.mutate(data.listId + ''); // string으로 변경
};

// TODO: (로그인유저 !== 작성자) 인경우, viewCount, CollectCount를 아예 받아오면안된다.
Expand Down Expand Up @@ -76,11 +96,9 @@ const CollectButton = ({ data }: { data: CollectProps }) => {
}

return (
<>
<div className={styles.collectWrapper}>
{data.isCollected ? <CollectedIcon onClick={handleCollect} /> : <CollectIcon onClick={handleCollect} />}
</div>
</>
<div className={styles.collectWrapper} onClick={handleCollect}>
{data.isCollected ? <CollectedIcon /> : <CollectIcon />}
</div>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function ListInformation() {
error,
isError,
} = useQuery<ListDetailType>({
queryKey: [QUERY_KEYS.getListDetail],
queryKey: [QUERY_KEYS.getListDetail, params?.listId],
queryFn: () => getListDetail(Number(params?.listId)),
enabled: !!params?.listId,
retry: 0,
Expand Down
48 changes: 24 additions & 24 deletions src/app/user/[userId]/_components/FollowButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,54 +35,54 @@ export default function FollowButton({ isFollowed, userId }: FollowButtonProps)
enabled: !!userMe.id,
});

const followUser = useMutation({
mutationKey: [QUERY_KEYS.follow, userId],
mutationFn: () => createFollowUser(userId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.userOne, userId],
});
const followUserMutation = useMutation({
mutationKey: [isFollowed ? QUERY_KEYS.deleteFollow : QUERY_KEYS.follow, userId],
mutationFn: isFollowed ? () => deleteFollowUser(userId) : () => createFollowUser(userId),
onMutate: async () => {
await queryClient.cancelQueries({ queryKey: [QUERY_KEYS.userOne, userId] });
const previousFollower: UserType | undefined = queryClient.getQueryData([QUERY_KEYS.userOne, userId]);

if (!previousFollower) return;

const nextData = {
...previousFollower,
isFollowed: !isFollowed,
followerCount: isFollowed ? previousFollower.followerCount - 1 : previousFollower.followerCount + 1,
};

queryClient.setQueryData([QUERY_KEYS.userOne, userId], nextData);

return { previousFollower };
},
onError: (error: AxiosError) => {
onError: (error: AxiosError, userId: number, context) => {
if (error.response?.status === 401) {
handleSetOn();
}
queryClient.setQueryData([QUERY_KEYS.userOne, userId], context?.previousFollower);
},
});

const deleteFollowingUser = useMutation({
mutationKey: [QUERY_KEYS.deleteFollow, userId],
mutationFn: () => deleteFollowUser(userId),
onSuccess: () => {
onSettled: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.userOne, userId],
});
},
onError: (error: AxiosError) => {
if (error.response?.status === 401) {
handleSetOn();
}
},
});

const handleFollowUser = (isFollowed: boolean) => () => {
if (isFollowed) {
deleteFollowingUser.mutate();
} else {
if (!isFollowed) {
if (userMeData && userMeData?.followingCount >= MAX_FOLLOWING) {
toasting({ type: 'warning', txt: toastMessage[language].limitFollow });
return;
}
followUser.mutate();
}
followUserMutation.mutate(userId);
};

return (
<>
<button
className={`${isFollowed ? styles.variant.gray : styles.variant.primary}`}
onClick={handleFollowUser(isFollowed)}
disabled={followUser.isPending || deleteFollowingUser.isPending}
disabled={followUserMutation.isPending}
>
{isFollowed ? userLocale[language].cancelFollow : userLocale[language].follow}
</button>
Expand Down
57 changes: 25 additions & 32 deletions src/components/exploreComponents/FollowButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import getUserOne from '@/app/_api/user/getUserOne';

import { QUERY_KEYS } from '@/lib/constants/queryKeys';
import { UserType } from '@/lib/types/userProfileType';
import { useUser } from '@/store/useUser';
import toasting from '@/lib/utils/toasting';
import toastMessage, { MAX_FOLLOWING } from '@/lib/constants/toastMessage';
import * as styles from './UsersRecommendation.css';
Expand All @@ -30,64 +29,58 @@ interface FollowButtonProps {
function FollowButton({ isFollowing, onClick, userId, targetId }: FollowButtonProps) {
const { language } = useLanguage();
const queryClient = useQueryClient();
const { user: userMe } = useUser();
const { isOn, handleSetOff, handleSetOn } = useBooleanOutput();

const { data: userMeData } = useQuery<UserType>({
queryKey: [QUERY_KEYS.userOne, userId],
queryFn: () => getUserOne(userId),
enabled: !!userMe.id,
enabled: !!userId,
retry: 1,
});

const followUser = useMutation({
mutationKey: [QUERY_KEYS.follow, userId],
mutationFn: () => createFollowUser(targetId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.userOne, userId],
});
const followUserMutation = useMutation({
mutationKey: [isFollowing ? QUERY_KEYS.deleteFollow : QUERY_KEYS.follow, targetId],
mutationFn: isFollowing ? () => deleteFollowUser(targetId) : () => createFollowUser(targetId),
onMutate: async () => {
await queryClient.cancelQueries({ queryKey: [QUERY_KEYS.userOne, userId] });
const previousFollower: UserType | undefined = queryClient.getQueryData([QUERY_KEYS.userOne, userId]);

if (!previousFollower) return;

const nextData = {
...previousFollower,
isFollowed: !isFollowing,
followerCount: isFollowing ? previousFollower.followerCount - 1 : previousFollower.followerCount + 1,
};

queryClient.setQueryData([QUERY_KEYS.userOne, userId], nextData);

return { previousFollower };
},
onError: (error: AxiosError) => {
onError: (error: AxiosError, userId: number, context) => {
if (error.response?.status === 401) {
handleSetOn();
}
queryClient.setQueryData([QUERY_KEYS.userOne, userId], context?.previousFollower);
},
});

const deleteFollowingUser = useMutation({
mutationKey: [QUERY_KEYS.deleteFollow, userId],
mutationFn: () => deleteFollowUser(targetId),
onSuccess: () => {
onSettled: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.userOne, userId],
});
},
onError: (error: AxiosError) => {
if (error.response?.status === 401) {
handleSetOn();
}
},
});

const handleFollowUser = (isFollowing: boolean) => () => {
if (isFollowing) {
deleteFollowingUser.mutate();
onClick();
} else {
if (!isFollowing) {
if (userMeData && userMeData?.followingCount >= MAX_FOLLOWING) {
toasting({ type: 'warning', txt: toastMessage[language].limitFollow });
return;
}
followUser.mutate();
onClick();
}
followUserMutation.mutate(userId);
onClick();
};

if (!userMe) {
return null;
}

return (
<>
<button
Expand Down

0 comments on commit 3efc8e0

Please sign in to comment.