dangerouslyAllowMutability

내가 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의 성질)

  1. Recoil State는 불변값이다.
  2. Recoil State 값의 수정은 순수함수(Selector)에 의해서 이루어져야 한다.

즉 Recoil 객체 state는 불변값을 유지하기 위해 deep freeze라는 기술로 state의 변경을 막아 객체에 프로퍼티를 추가할 수 없다고한다.

Object.freeze

어떤 객체에 freeze를 적용하게 되면 그 객체에는 더이상 다른 프로퍼티를 추가할 수 없게된다. 하지만 객체를 같은 이름으로 재할당하는 것은 허용한다.
그래서 어떤 객체를 불변으로 만들 때는 const와 freeze를 같이 사용한다.

그동안 useSetRecoilState를 통해 atom 값을 업데이트할 때는 어떤 객체를 새로 만들고 그 객체를 재할당하는 방식으로 업데이트해서 가능한 것이었다.

그러나 이 경우에는 push를 통해 새로운 프로퍼티를 추가하려고 해서 recoil의 deep freeze에 의해 에러가 발생하였다고 본다.

해결

코드 한 줄만 추가하면 된다.

atom을 선언하고 default를 작성 후 바로 밑줄에

dangerouslyAllowMutability: true,

만 추가해주자. 그러면 freeze 기술을 못쓰도록 막는 것 같다.

useController Custom Onchange

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가 있다면 실행시키도록 하면 된다.

0개의 댓글