open
state값을 변경시켜 Accordion을 열고자 했다.TabScroll
는 useTabScroll(hooks)
에서 Recoil데이터를 선언하고, Tab제목
컴포넌트와 Accordion
컴포넌트를 하위 자식으로 가진다.// TabScroll.tsx
const TabScroll = ({
groupLikeStockList,
}: {
groupLikeStockList: TGroupLikeStockInfo[];
}) => {
// [registryRef]의 tabIndex로 스크롤될 컴포넌트의 ref배열에 등록한다.
// [handleScroll]로 지정된 컴포넌트 클릭시, ref배열의 위치로 스크롤 및 ref컴포넌트의 Accordion을 열어준다.
const { registryRef, handleScroll, headerRef } = useTabScroll({
length: groupLikeStockList.length,
});
...
return (
...
// Tab제목 컴포넌트
{groupLikeStockList.map((group, idx) => {
return (
<Button key={idx} tabIndex={idx} onClick={handleScroll}>
{group.groupName}
</Button>
);
})}
...
// Accordion컴포넌트
{groupLikeStockList.map((group, idx) => {
return (
<div key={idx} tabIndex={idx} ref={registryRef}>
<Section.Accordion
index={idx}
title={group.groupName}
noContent={<Button primary>주식 추가하기</Button>}
>
{group.likeStockInfoList.length &&
group.likeStockInfoList.map((likeStock, idx) => {
return (
<div
key={idx}
className="flex items-center justify-between"
>
<p>
{likeStock.name} ({likeStock.stockId})
</p>
<Button primary>BUY</Button>
</div>
);
})}
</Section.Accordion>
</div>
);
})}
)
}
// useTabScroll.ts | hooks함수
export const useTabScroll = ({ length }: { length: number }) => {
const [isOpenTabList, setIsOpenTabList] = useRecoilState(tabOpenStateList);
// 버튼 클릭시, ref에 등록된 element로 스크롤 이동
const handleScroll = (e: MouseEvent<HTMLButtonElement>) => {
const tabIndex = e.currentTarget.tabIndex;
...
setIsOpenTabList(() => {
const newState = [...isOpenTabList];
newState[tabIndex] = true;
return newState;
});
}
};
})
//
tabOpenStateList
가 등록되어있다.export const tabOpenStateList = atom<boolean[]>({
key: "tabOpenStateList",
default: [],
});
setIsOpenTabList
부분에서 recoil의 전체 상태값이 변경
되어, 모든 컴포넌트가 재렌더링된다고 생각되었다.setIsOpenTabList(() => {
const newState = [...isOpenTabList];
newState[tabIndex] = true;
return newState;
});
isOpenTabList
state배열을 잘게 잘라서 사용할 수 있도록 selectorFamily
를 활용하였다.// atoms/tab.ts
import { DefaultValue, atom, selectorFamily } from "recoil";
export const tabOpenStateList = atom<boolean[]>({
key: "tabOpenStateList",
default: [],
});
export const tabOpenState = selectorFamily({
key: "tabOpenState",
get:
(tabIndex: number) =>
({ get }) => {
return get(tabOpenStateList)[tabIndex];
},
set:
(tabIndex: number) =>
({ set }, newValue: boolean | DefaultValue) => {
set(tabOpenStateList, (prev) => {
const newState = [...prev];
if (!(newValue instanceof DefaultValue)) {
newState[tabIndex] = newValue;
}
return newState;
});
},
});
TabIndex
값에 따라 tabOpenStateList
에서 특정요소만 get/set할 수 있도록 설정했다.// useTabScroll.ts
const setOpenList = Array(length)
.fill(false)
.map((_, idx) => useSetRecoilState(tabOpenState(idx)));
...
// 버튼 클릭시, ref에 등록된 element로 스크롤 이동
const handleScroll = (e: MouseEvent<HTMLButtonElement>) => {
const tabIndex = e.currentTarget.tabIndex;
if (tabBodyRef.current) {
(tabBodyRef.current[tabIndex] as HTMLDivElement).scrollIntoView({
behavior: "smooth",
block: "start",
inline: "nearest",
});
setOpenList[tabIndex](true);
}
};
tabOpenState
selectorFamily를 배열의 수만큼 setOpenList
변수에 미리 할당해둔다.이처럼 적용한 이유!
handleScroll함수가 실행될 때, 비로소 openState값이 바뀌어야 하기에useSetRecoilState
를 함수내부에서 사용할 수 있도록미리 선언
해주어야 했다.React hooks의 선언 규칙은 항상 최상단에서 이루어져야 하기에, 위와 같이
setOpenList
에 미리 담아주었다.
https://github.com/Dolphin-PC/motoo/commit/500f93424fbf21b188545aa5dd7745d6f57f21ed
사용목적
에 따라 구분되어 있다는 것을 알 수 있었다.atomFamily
가 적합하다고 생각하여 코드를 리팩토링했다 (더 간결해졌다!)// atoms/tab.ts
export const tabOpenStateList = atomFamily<boolean, number>({
key: "openList",
default: () => false,
});
// atom관련한 코드가 이렇게 짧아졌다.
(atom 집합)
로 특정 컴포넌트만 re-render되도록 수정