useImperativeHandle
Hook을 사용하여 자식 컴포넌트에서 부모 컴포넌트로 props 전달하기
토이3 프로젝트 중 플레이리스트를 추가하고 수정 할 수 있는 관리페이지를 담당하면서 부모컴포넌트인 managePlaylist.tsx
가 있었고, 하위로 영상을 추가 할 양식 컴포넌트 AddPlaylist.tsx
와 추가된 영상 아이템들을 목록으로 띄울 PlaylistChart.tsx
가 있었다.
자식 컴포넌트인 AddPlaylist.tsx
에서 입력된 데이터와 스토어에 저장된 아이템들의 목록을 최종적으로 부모 컴포넌트인 managePlaylist.tsx
에서 API를 이용해 데이터를 저장하는 방식이다. 그렇기때문에 useRef
와 useImperativeHandle
훅을 사용하여 자식 컴포넌트의 데이터를 부모 컴포넌트에서 받아올 수 있도록 하였다.
부모컴포넌트
...
const data = useRef(null)
// getData.current?.getData()?.title 자식컴포넌트의 데이터 가져오기
return (
<ChildrenFC ref={data} />
)
...
자식컴포넌트
// ref prop을 전달하기 위해 forwardRef를 사용
const ChildrenFC = forwardRef(({}, ref) => {
const title = useRef<HTMLInputElement>(null)
useImperativeHandle(ref, () => ({
getData,
}))
// useCallback을 사용하여 부모가 최신 상태의 value를 받을 수 있도록 한다.
const getData = useCallback(() => {
const data: {
title: ''
}
data.title = title.current?.value || ''
return data
}, [title])
return (
<>
<input ref={title} />
</>
)
})
부모컴포넌트는 온전히 자식컴포넌트와 스토어의 데이터만으로 최종적인 유효성검사와 훅을 사용하여 데이터베이스에 저장해주는 역할만 있으며 자식컴포넌트에서 받은 데이터는 playlistDataToAdd
이다.
const ManagePlaylist = () => {
const userData = useUserStore((state) => state.userInformation);
const addedPlaylist = useYoutubeDataStore((state) => state.youTubelistData);
const clearYoutubelistData = useYoutubeDataStore((state) => state.clearYoutubelistData);
const playlistDataToAdd = useRef<{ getPlaylistData: () => PlaylistDataStore }>(null);
const { playlistId } = useParams() as { playlistId: string };
const { mutate } = useNewPlaylist(playlistId);
const navigate = useNavigate();
.
.
.
// 자식컴포넌트 데이터의 최종 유효성검사
const handleValidation = (type: string) => {
if (playlistDataToAdd.current?.getPlaylistData()?.title === '') {
toast.error('플레이리스트 제목을 입력해주세요.');
} else if (playlistDataToAdd.current?.getPlaylistData()?.content === '') {
toast.error('플레이리스트 설명을 입력해주세요.');
} else if (addedPlaylist.length === 0) {
toast.error('플레이리스트를 추가해주세요.');
} else {
fetchCreatePlaylistData(type);
}
};
// 데이터베이스에 저장하기 위한 API요청
const fetchCreatePlaylistData = (type: string) => {
const newPlyData = playlistDataToAdd.current?.getPlaylistData();
if (newPlyData) {
const playlistData = {
userId: userData.userId,
title: newPlyData.title,
content: newPlyData.content,
disclosureStatus: newPlyData.disclosureStatus,
tags: newPlyData.tags,
link: addedPlaylist.map((item) => item.link?.[0]),
imgUrl: addedPlaylist.map((item) => item.imgUrl?.[0]),
};
try {
mutate({ playlistData, type, playlistId });
if (type === '추가') {
toast.success('플레이리스트가 생성되었습니다.');
} else if (type === '수정') {
toast.success('플레이리스트가 수정되었습니다.');
}
setTimeout(() => {
clearYoutubelistData();
navigate(`/playlist/${userData.userId}`);
}, 1000);
} catch (error) {
toast.error('플레이리스트 업데이트 중 오류가 발생하였습니다. 다시 시도해주세요.');
console.error(error);
}
}
};
return (
<div css={{ width: '100%', margin: '5px 15px 0 0' }}>
<div css={{ display: 'flex' }}>
<AddPlaylist ref={playlistDataToAdd} userPlyData={userPlyData} />
<div css={{ width: 'calc(100% - 450px)', position: 'relative' }}>
<PlaylistChart />
<div css={btnArea}>
<Button size="md" onClick={() => history.back()}>
취소
</Button>
<Button
size="md"
background={true}
onClick={() => {
if (playlistId) {
handleValidation('수정');
} else {
handleValidation('추가');
}
}}
>
완료
</Button>
</div>
</div>
<ToastContainer
position="bottom-center"
limit={2}
closeButton={false}
autoClose={2000}
hideProgressBar
/>
</div>
</div>
);
};
export default ManagePlaylist;
useCallback을 사용하여 value들이 모두 최신 상태로 유지되게끔 하여 부모 컴포넌트에서 데이터를 정상적으로 받을 수 있게 된다.
interface AddPlaylistProps {
userPlyData: IPlaylist | null | undefined;
}
interface AddPlaylistRef {
getPlaylistData: () => PlaylistDataStore;
}
const AddPlaylist = forwardRef<AddPlaylistRef, AddPlaylistProps>(({ userPlyData }, ref) => {
const [tags, setTags] = useState<string[]>([]);
const [content, setContent] = useState('');
const [disclosureStatus, setDisclosureStatus] = useState(true);
const title = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
getPlaylistData,
}));
const getPlaylistData = useCallback(() => {
const playlistData: PlaylistDataStore = {
title: '',
content: '',
disclosureStatus,
tags: [],
};
playlistData.title = title.current?.value || '';
playlistData.content = content;
playlistData.disclosureStatus = disclosureStatus;
playlistData.tags = tags;
return playlistData;
}, [title, content, tags, disclosureStatus]);
.
.
.
return (
...
<input type="text" ref={title} />
<textarea value={content} onChange={(e) => setContent(e.target.value)}></textarea>
<input
type="text"
ref={url}
onKeyDown={(e) => handleKeyDown(e, 'url')}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)}
/>
<input
type="text"
ref={tagValue}
onKeyDown={(e) => handleKeyDown(e, 'tag')}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)}
/>
...
);
});
export default AddPlaylist;