かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

React Warning: Cannot update a component (`Componet`) while rendering a different component.

Next.js (React) でログインユーザー情報がなければ / にリダイレクトするコンポーネントを作っていて、リロード時に state が無くなるので SessionStorage に保存しておいたデータを取ってきてログインを確認するような事をしていたら下記のような Warning が表示されました。
Warning: Cannot update a component (`Componet`) while rendering a different component.

結論: コンポーネントレンダリング中に別コンポーネントの state を変更しようとすると Warning が発生する。

⚠ 問題のあったコード

/pages/_app.tsx

import { AppProps } from 'next/app';
import { RecoilRoot } from 'recoil';
import { Auth } from '../Component/Auth';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <RecoilRoot>
      <Auth>
        <Component {...pageProps} />
      </Auth>
    </RecoilRoot>
  );
};

/hooks/useUser.tsx

import { useRecoilState } from 'recoil';

const readUser = (): UserInterface | undefined => {
  const user = sessionStorage.getItem('user');
  return user ? (JSON.parse(user) as UserInterface) : undefined;
};

interface IuseUser {
  user: UserInterface;
  isUser: () => boolean;
}

export const useUser = (): IuseUser => {
  const [user, setUserData] = useRecoilState(userState);

  const isUser = () => {
    if (user) {
      return true;
    }
    
    // SessionStorage にデータがあれば user state を更新する
    const userData = readUser();
    if (!userData) {
      return false;
    }

    setUserData(userData);
    return true;
  }

  return {
    user,
    isUser
  }
};

/Component/Auth.tsx

import { useEffect, useState, VFC } from 'react';
import { useRouter } from 'next/router';
import { useUser } from '../hooks/useUser';

interface AuthProps {
  children: React.ReactNode;
}

export const Auth: VFC<AuthProps> ({ children }) => {
  const [loading, setLoading] = useState<boolean>(true);
  const router = useRouter();
  const { isUser } = useUser();
  
  useEffect(() => {
    if (router.isReady) {
      setLoading(false);
    }
  }, [router.isReady]);
  
  if (loading) {
    return <Loading />
  }

  // state にも SessionStorage にもデータがない場合はリダイレクト
  if (!isUser()) {
    router.push('/');
    // リダイレクト前に children が一瞬表示されてしまわないように null を返す
    return null;
  }

  return <>{children}</>;
}

一度ログインして SessionStorage にデータが有る状態でリロードすると Warning が発生します。
Warning: Cannot update a component (`Componet`) while rendering a different component.

コンポーネントレンダリング中に別コンポーネントの state を変更しようとすると Warning が発生する。

レンダー中に setState を呼び出すことはサポートされていますが同じコンポーネントに対してのみ可能です。別のコンポーネントのレンダー中に setState を呼び出すと、警告が表示されるようになりました。

Warning: Cannot update a component from inside the function body of a different component.
この警告は、意図しない状態変更によって引き起こされるアプリケーションのバグを見つけるのに役立ちます。レンダーの結果として他のコンポーネントの状態を意図的に変更したいという稀なケースでは、setState 呼び出しを useEffect にラップすることができます。
cf. レンダー中のいくつかの更新に関する警告 | React v16.13.0 – React Blog

問題の箇所

/Component/Auth.tsx

export const Auth: VFC<AuthProps> ({ children }) => {
  // 略

  // state にも SessionStorage にもデータがない場合はリダイレクト
  if (!isUser()) { // <- state の変更を含む hooks がレンダリング前に実行される
    router.push('/');
    return null;
  }

  return <>{children}</>;
}

コンポーネント直下ではなく、 useEffect 内で呼び出すようにすればOK

/Component/Auth.tsx

export const Auth: VFC<AuthProps> ({ children }) => {
  const [loading, setLoading] = useState<boolean>(true);
  const router = useRouter();
  const { isUser } = useUser();
  
  useEffect(() => {
    if (!router.isReady) {
      return;
    }

    // state にも SessionStorage にもデータがない場合はリダイレクト
    if (!isUser()) {
      router.push('/');
      return;
    }
    
    // use 情報の確認が完了するまで loading のままにする
    setLoading(false);
  }, [router.isReady]);


  return loading? <Loading /> : <>{children}</>;
}

そもそも作った hooks があまり良くない気もしますが、これで Warning の発生しないリロードしても user 情報が保持されて、user 情報がなければ / にリダイレクトするコンポーネントが作れました!
₍ ᐢ. ̫ .ᐢ ₎ ワーイ

所管

Next.js とか recoil とか色々使ってたけど、単純に React Hooks の使い方の問題でした。

React とか Next.js とか個人制作の雰囲気で触ってるから良い方法なのかダメな方法なのか判断ができないので、ちゃんとしたフロントエンジニアのチームでコードレビュー受けたい〜


[参考]

ハマった時は、これもアイカツ!を思うと頑張れる!!