이슈 해결 - Can't perform a React state update on a component that hasn't mounted yet. This indicates that you have a side-effect in your render function that asynchronously later calls tries to update the component. Move this work to useEffect instead.

yesolog·2024년 3월 6일
0

개발일지

목록 보기
6/6

Warning: Can't perform a React state update on a component that hasn't mounted yet. This indicates that you have a side-effect in your render function that asynchronously later calls tries to update the component. Move this work to useEffect instead.

useRecoilValue를 사용하여 API를 호출한 결과값을 비동기적으로 가져오는 과정에서 위와 같은 에러가 발생했다.

다음은 store와 selelctor를 직접 사용한 코드이다.

//store
import { atom, selector } from 'recoil';
import { getPocketmontInfo } from '../api';

let localStorageId = localStorage.getItem("id") as string
export const pokemonId = atom({ key: 'pokemonId', default: localStorageId  ,});

export const poketmonDataSelector = selector({
  key: 'poketmonDataSelector',
  get: async ({ get }) => {
    const id = get(pokemonId);
    const result = await getPocketmontInfo(id);
    return result;
  },
});

스토어에서는 로컬스토리지 아이디라는 변수를 생성하고 pokemonId라는 아톰의 기본값으로 설정했다.

또한 리스트에서 포켓몬의 카드를 클릭하여 상세페이지로 이동할 때 localstorage의 id를 클릭한 포켓몬의 id로 변경하고 동시에 pokemonId도 useRecoilState의 set을 사용하여 변경해주었다.

(이렇게 로컬스토리지로 저장한 이유는 새로고침을 해도 데이터가 변경되지 않도록 하기 위해서다.)

이후 pokemonId에서 파생한 selector를 사용하여 포켓몬의 상세정보를 알려주는 getPocketmontInfo함수를 실행하였고 그 결과를 return하도록 구현했다.

문제는 다음과 같이 컴포넌트에서 해당 셀렉터를 구독할 때 생겼다.

import { useParams } from 'react-router-dom';
import styled from 'styled-components';
import Bg from '../assets/6114010.jpg';
import { useRecoilValue } from 'recoil';
import { poketmonDataSelector } from '../store/atom';

const Contents = () => {
  const params = useParams();
  const id = params.id && params.id;

  const fetchedData = useRecoilValue(poketmonDataSelector);

// 중간코드 생략
  
    return (
      <Container>
        <img src={imgSrc} width={100} height={100} alt={name} />
        <Description>앗 야생의 {name} 나타났다!</Description>
      </Container>
    );
  }

export default Contents;

poketmonDataSelector를 useRecoilValue로 구독하는데 다음과 같은 에러가 발생했다.

경고: 아직 마운트되지 않은 구성 요소에서는 React 상태 업데이트를 수행할 수 없습니다. 이는 나중에 비동기적으로 호출하여 구성 요소를 업데이트하려고 시도하는 렌더링 함수에 부작용이 있음을 나타냅니다. 대신 이 작업을 useEffect로 이동하세요.

셀렉터의 get과 getPocketmontInfo 함수는 모두 async-await을 사용하여 비동기적으로 api를 호출하고 있다.

하지만 컴포넌트에서 사용하는 useRecoilValue는 동기적으로 실행되므로 컴포넌트가 렌더링 중에 해당 Recoil 상태를 가져오면서 React 업데이트 주기와 충돌이 발생한 것으로 예상된다.

그럼 해당 작업이 끝나기 전까지는 로딩중인 상태를 나타내 주도록 분기를 나누는 방법을 생각했다. recoil은 useRecoilValue와 매우 유사한 useRecoilValueLodable이라는 훅을 제공한다.

공식문서에서는 다음과 같이 설명하고 있다.

useRecoilValueLoadable(state)

  • 이 hook은 비동기 selector의 값을 읽기 위해 사용합니다. 이 hook은 주어진 상태에 컴포넌트를 암묵적으로 구독합니다.

  • useRecoilValue()와는 다르게 이 hook은 비동기 selector에서 읽어올 때 (React Suspense와 함께 작동하기 위해) Error 혹은 Promise 를 던지지 않습니다. 대신 이 hook은 값에 대한 Loadable 객체를 리턴합니다.

비동기 selector의 값을 읽기 위해 사용되는 훅으로 객체를 return하는데 이때 객체안에 state를 통해 loading 상태인지 hasvalued 상태인지를 알 수 있다.

  const fetchedData = useRecoilValueLoadable(poketmonDataSelector);
  console.log(fetchedData);

useRecoil을 사용해서 셀렉터를 구독하는 방식에서 useRecoilValueLoadable로 변경을 진행했다. 이후 콘솔로 확인하면 다음과 같이 비동기 호출의 진행상태를 확인할 수 있다.

useRecoilValuleLodable의 state 프로퍼티를 사용하여 API 호출의 진행 상황을 파악하여 다음과 같이 컴포넌트의 로딩 상황에 따른 분기를 나눠주었다.

import { useParams } from 'react-router-dom';
import styled from 'styled-components';
import Bg from '../assets/6114010.jpg';
import { useRecoilValueLoadable } from 'recoil';
import { poketmonDataSelector } from '../store/atom';

const Contents = () => {
  const params = useParams();
  const id = params.id && params.id;

  const fetchedData = useRecoilValueLoadable(poketmonDataSelector);

  // state가 'hasValued'이면서 contents가 있을 경우
  if (fetchedData.state === 'hasValue' && fetchedData.contents) {
    
    // 중간 코드 생략
    
  // API를 호출한 데이터 사용
    return (
      <Container>
        <img src={imgSrc} width={100} height={100} alt={name} />
        <Description>앗 야생의 {name} 나타났다!</Description>
      </Container>
    );
  }

// 만약 state가 'hasValued'아니거나 contents가 없을 경우에는 로딩 중으로 분기 처리
  return <Container>로딩 중...</Container>;
};

브라우저를 확인해보면 다음과 같이 에러가 해결된 모습을 확인할 수 있다!

0개의 댓글