router.push
로 페이지를 이동시켜주고있었다.<MainUI.CustomBox
key={uuid()}
width="100%"
height="70px"
onClick={() => {
router.push({
pathname: "/main/menu-detail/[id]/[detailId]",
query: {
id: id,
favorite,
detailId: name,
name,
picture,
rating,
address,
},
});
}}
>
</MainUI.CustomBox>
[id]
는 각각의 상점들가게 이름들이 정보와 함께 리스트처럼
나오고
[detailId]
는 각 상점들의 고유한 정보/메뉴/리뷰
를 볼수있는 페이지다.
왼쪽부분이 [id] / 오른쪽이 [detailId]
[detailId] 로 넘어갈때 필요한 정보들을 router query 로 넘겨주고 있다보니
query로 favorite을
[detailId]로 넘겨주고 ->
[detailId]에서 하트아이콘을 누를시 한번 클릭시 찜하기 true , 한번 더 누르면 false로 찜하기 삭제, 이렇게 토글되는 응답을 담아 업데이트 해주는게 내가 생각했던 로직이였다.
ISSUE
- 찜하기 아이콘을 눌러 상태값이 기본값 false에서 true로 바뀐후에 다른페이지로 이동 후 다시 돌아오면 true가 아니라 초기값 false 상태가 저장되어있음.
- favorite값이 ture일때 하트 아이콘의 컬러를 ture 일경우 orange , flase 일경우에는 #000로 설정했지만 각각 상점들의 상태값
true false
가 담겨있는게 아니라 , 전체의상태, 하나의 상태로true와 false
가 담겨있어 true 의 상태일경우 모든 하트아이콘의 컬러가 orange 컬러로 적용됨
고민도 많이 해보고 이것저것 시도도 많이 해보다가 recoil 을 이용해 각각의 상점들의 상태를 관리하고 업데이트 하는게 맞다고 생각했다.
1. atom 설정
atom.tsx
export const nearbyDataState = atom<TMenuResponse | undefined>({
key: `nearDataState/${v1()}`,
default: {
status: "",
data: [],
},
});
nearbyDataState에는 [detailId]로 넘겨줫던 router.query 데이터
들이 담겨있다.
2. atom의 값을 바꾸기
recoil 에서 selector와 selectorFamily가 있는데 솔직히 공식문서를 읽어보며 정확히 내가 원하는 프로그래밍을 어떻게 해야할지 몰랐고 다른 코드예시들을 많이 보면서 내가 어떻게 적용해야하는지 많은 고민을 했던것 같다.
recoil selector는 파생 상태를 나타낸다고 나와있는데 , 음 상태관리 도구인만큼 내가 생각하기엔 기존의 데이터들의 초기값을 담아놓는게 atom이라면 selector는 atom을 가지고 다른 상태를 만들어 순수함수인것처럼? 행동하며 부수효과가 적고 전역에서 내가 원하는 반환값을 만들어 사용할수있다고 생각하면 좀 편할지도 모르겠다 (말해놓고 뭔가 이상함 ㅎㅎ)
selectorFamily를 적용한 이유
nearbyDataState
atom 배열안에 여러개의item
들이 있다. item의 id값들 중 내가 찜하기 버튼을클릭한 id값의 item을 찾아서
그 item의 favorite의 boolean값을업데이트
해주어야하는데 그렇게 프로그래밍 하려면 selector에 id 매개변수를 넣어야했다.
그래서 selectorFamily를 선택했다.
selectorFamily 도 selector와 마찬가지로 get 과 set이 있는데, get는 읽기가능한 상태 / set은 쓰기가능한 상태라고 되어있다.
3. 구현
먼저 get으로 atom값을 가져온다.
그다음 set을 사용해서 업데이트 시켜줄값을 newValue로 적용해줘야한다.
set에서 마지막에는 두개의 매개변수가 들어가게 되는데 첫번째 매개변수 는 처음상태값 이고 두번째 매개변수는 업데이트 시켜줄 값이다.
set을 이용해서 반환시켜줄값을 업데이트값으로 설정하고 마지막엔 set메서드로 업데이트 시켜준다.
내가 원하는 업데이트 값은 favorite의 boolean 상태값이기 때문에 favorite의 값을 newValue로 설정해주었다.
selector.tsx
export const favoriteState = selectorFamily({
key: "favoriteState",
get:
(id) => ({ get }) => {
const nearbyState = get(nearbyDataState);
},
set:
(id) =>
({ set, get }, newValue) => {
const nearbyState = get(nearbyDataState);
const updatedData = nearbyState?.data?.map((item) => {
if (item.id == id) {
return { ...item, favorite: newValue };
}
return item;
});
set(nearbyDataState, { ...nearbyDataState, data: updatedData });
},
});
원래 내가 작성하는 컴포넌트로 와서 favorite을 어떤 newValue로 바꿔줄껀지 설정해야한다.
하트아이콘 클릭시 api로 보낸 응답값안에 isFavorite가 boolean값이 토글되고 이 값을 favorite의 newValue으로 넣어주면 된다.
...
const handleClick = () => {
favoriteApi.mutate(
{
storeId: id,
},
{
onSuccess: (res) => {
setTest(res.data.isFavorite);
},
}
);
};
이렇게 코드를 작성하면 query로 넘겨준값이 즉각적으로 업데이트되는걸 콘솔에서 확인할 수 있다.(너무 신남)
업데이트 해준 atom nearbyDataState에는 여러개의 객체들이 들어있는데 하트아이콘에 컬러를 업데이트된 favorite 값에 따라 변경해줘야해서 get에서 업데이트 된 값만 남도록 해줬다.(뭔가 더 좋은 방법이 있을거같다 ㅎㅎ)
전체 코드
export const favoriteState = selectorFamily({
key: "favoriteState",
get:
(id) =>
({ get }) => {
const nearbyState = get(nearbyDataState);
const findItem = nearbyState?.data?.find((item) => {
return item.id == id;
});
return findItem?.favorite;
},
set:
(id) =>
({ set, get }, newValue) => {
const nearbyState = get(nearbyDataState);
console.log('nearbystate',nearbyState)
const updatedData = nearbyState?.data?.map((item) => {
if (item.id == id) {
return { ...item, favorite: newValue };
}
return item;
});
set(nearbyDataState, { ...nearbyDataState, data: updatedData });
},
});
export default selector;
...
const MenuDetailInfoPage = () => {
const { query } = useRouter();
const { favoriteApi } = useCart();
const { name, picture, id, rating } = query;
const [favorite, setFavorite] = useRecoilState(favoriteState(Number(id)));
const handleClick = () => {
favoriteApi.mutate(
{
storeId: Number(id),
},
{
onSuccess: (res) => {
setFavorite(res.data.isFavorite);
},
}
);
};
return (
<>
<StyledHeaderWrap>
<HeaderLayout headerTitle={`${name}`} storeRating={`${rating}`} />
<StyledIcon onClick={handleClick} favorite={favorite}/>
</StyledHeaderWrap>
<Image src={String(picture)} alt="이미지" width={70} height={70} />
<MenuDetailInfoTab />
</>
);
};
const StyledHeaderWrap = styled.div`
position: relative;
`;
const StyledIcon = styled(FaRegHeart)<{favorite:boolean}>`
position: absolute;
right: 0;
top: 38px;
z-index: 2;
color:${({favorite}) => favorite ? "orange" : "#000"};
`;
MenuDetailInfoPage.getLayout = (page: React.ReactNode) => {
return <AuthPrivateLayout>{page}</AuthPrivateLayout>;
};
export default MenuDetailInfoPage;