내가 recoil atom에 default로 지정한 타입은 다음과 같다.
interface IAtom {
bedCount: number;
bedType: {
id: number;
beds: {
name: string;
count: number;
}[]
}[]
/* .. */
};
atom값을 꺼내와 beds에 추가된 속성을 추가하려고 push한 후에 useSetRecoilState로 값을 업데이트를 하려고 했다.
그런데 다음과 같은 에러가 발생했다.
TypeError: Cannot add property 0, object is not extensible
이유는 다음과 같다고 생각된다(사실 아래는 redux state의 성질)
즉 Recoil 객체 state는 불변값을 유지하기 위해 deep freeze라는 기술로 state의 변경을 막아 객체에 프로퍼티를 추가할 수 없다고한다.
어떤 객체에 freeze를 적용하게 되면 그 객체에는 더이상 다른 프로퍼티를 추가할 수 없게된다. 하지만 객체를 같은 이름으로 재할당하는 것은 허용한다.
그래서 어떤 객체를 불변으로 만들 때는 const와 freeze를 같이 사용한다.
그동안 useSetRecoilState를 통해 atom 값을 업데이트할 때는 어떤 객체를 새로 만들고 그 객체를 재할당하는 방식으로 업데이트해서 가능한 것이었다.
그러나 이 경우에는 push를 통해 새로운 프로퍼티를 추가하려고 해서 recoil의 deep freeze에 의해 에러가 발생하였다고 본다.
코드 한 줄만 추가하면 된다.
atom을 선언하고 default를 작성 후 바로 밑줄에
dangerouslyAllowMutability: true,
만 추가해주자. 그러면 freeze 기술을 못쓰도록 막는 것 같다.
useController로 Common Selector 컴포넌트를 만들었다. 만들 때
interface IProps<T extends FieldValues>
extends React.SelectHTMLAttributes<HTMLSelectElement> {
/* .. */
};
로 onChange 이벤트를 부모 컴포넌트에서 등록하여 바로 사용하려고 했는데 안되었다.
아마 useController에서 자체적으로 제공하는 onChange가 덮어써서 그런 것 같았다.
그래서 또다른 Selector 컴포넌트를 만들어야하나라고 절망하던 순간 구글링을 통해 방법을 발견했다.
// Selector.tsx
function Selector({
icons,
control,
name,
rules,
options = [],
disabledOptions = [],
errorMessage,
lable,
myChange,
}: IProps<any>) {
const {
field: { value, onChange },
} = useController({ name, rules, control });
return (
<Container iconExist={!!icons}>
{lable && <span>{lable}</span>}
<select
value={value || ''}
onChange={(e) => {
onChange(e);
// 아래에 props로 넘겨준 onChange 이벤트를 등록!
myChange && myChange(e);
}}
>
{disabledOptions.map((option, index) => (
<option key={index} value={option} disabled>
{option}
</option>
))}
{options.map((option, index) => (
<option key={index} value={option}>
{option}
</option>
))}
</select>
<IconWrapper>{icons}</IconWrapper>
<p style={{ textAlign: 'center' }}>{errorMessage}</p>
</Container>
);
}
// 부모 컴포넌트의 myChange
myChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
console.log('target', e.target.value);
setBedOptions([...bedOptions, e.target.value as BedType]);
const roomUpdate = { ...room };
roomUpdate.bedList.forEach((bed) => {
if (bed.id === bedroom.id) {
bed.beds.push({ type: e.target.value as BedType, count: 0 });
}
});
setRoom(roomUpdate);
부모 컴포넌트에서 myChange를 따로 정의하고 props로 넘겨주고, Selector에서 자신의 onChange를 실행시킨 후 받아온 Chagne Event가 있다면 실행시키도록 하면 된다.