Blog

【エラーとの戦い】Next.js ログインを判定してボタンを条件分岐

DBはfirebaseを使い、Authenticationでログイン機能を実装しました。

ヘッダーにある「ログイン」ボタンを『ログインしている時は「ログアウト」にしたい』だけでかなりハマってしまったので、事細かくエラーを残しておこうと思います。

最後のもの以外全部エラーのコードです。

なぜここまでエラーを出せるのかというと

  • TypeScript初心者
  • Next.js今回が初めて
  • firebaseも3回目くらい

と、入門レベルのものに同時にチャレンジしたからですね…。TypeScriptは、自分が使えない以前に、参考サイトを読み解くところからやらないといけないので。

大筋のやり方としては、ボタンのコンポーネントを作ってheaderで読み込むことにしました。

import Link from 'next/link'
import LoginBtn from '../components/loginBtn’

export default function Layout({ children }) {
  return (
    <div className="container">
      <header className="header">
        <LoginBtn/>
      </header>
      {children}
    </div>
  )
}

<LoginBtn>のところで、

  • ログイン中は<span className="btn-s is-orange">ログアウト</span>
  • ログアウト中は<Link href="/login/"><a className="btn-s is-orange">ログイン</a></Link>

を出力したい。
※2023/2 追記:Next.js最新では<Link>の中にaタグを入れなくてもよい仕様になっています

以下、コンポーネント側です。

NG1

import * as React from 'react'
import Link from 'next/link'
import { auth } from '../src/utils/firebase';

const Btn = () => {
  auth.onAuthStateChanged((user) => {
    if (user) {
      return <span className="btn-s is-orange">ログアウト</span>
    } else {
      return <Link href="/login/"><a className="btn-s is-orange">ログイン</a></Link>
    }
  })
};

export default Btn()

エラー内容

Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.

〜 コンポーネントが定義されているファイルからコンポーネントをエクスポートするのを忘れたか、デフォルトのインポートと名前付きのインポートを混同している可能性があります。

Btn関数でリターンしてない。確かにエラーの通り…ですが、ここではまだ気づきませんでした。returnしてるのに…と思っていた…

NG2

Next.jsの関数コンポーネントを使ってみようと思い、下記書いてみる。

import { auth } from '../src/utils/firebase';
import Link from 'next/link'

const Btn: React.FC = () => (
  <>
    {
      (function() {
        return(
          auth.onAuthStateChanged((user) => {
            if (user) {
              return <span className="btn-s is-orange">ログアウト</span>
            } else {
              return <Link href="/login/"><a className="btn-s is-orange">ログイン</a></Link>
            }
          })
        )
      }())
    }
  </>
)

export default Btn

ここで、参考にしたサイトがTypeScriptだったのでtsxに変更。(でもその後TypeScript使ってない部分も多い)

エラー内容

コンソールパネルにこのエラー。

react-dom.development.js?61bb:67 Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.

関数はReactの子としては無効です。これは、レンダリングから<Component />ではなくComponentを返す場合に発生する可能性があります。あるいは、この関数を返すのではなく呼び出すつもりだったのかもしれません。

・・・関数がそのまま返ってきた。即時間数で実行してるつもりだったけど(無理矢理すぎる)、これもだめだった…

NG3

クラスコンポーネントを使ってみる。

import * as React from 'react'
import Link from 'next/link'
import { auth } from '../src/utils/firebase';

class Btn extends React.Component {
  btn() {
    auth.onAuthStateChanged((user) => {
      if (user) {
        return <span className="btn-s is-orange">ログアウト</span>
      } else {
        return <Link href="/login/"><a className="btn-s is-orange">ログイン</a></Link>
      }
    })
  }
  render() {
    return (
      { this.btn }
    )
  }
}

export default Btn

エラー内容

シンタックスエラー!

Syntax error: Unexpected token, expected ","

  15 |   render() {
  16 |     return(
> 17 |       {this.btn}
     |            ^
  18 |     )
  19 |   }

ただ単に書き方が間違ってたので、こうすると↓シンタックスエラーは消える…けど、

render() {
    return (
      <>{ this.btn() }</>
    )
  }

エラーは出ないがタグも出ない。

なぜかというと、this.btn()にはundefinedが入っているから。
btn() で何もリターンできていないところが問題なのだけど、この時点でまだきづいてない。

エラーの原因が複数重なっているので、ちょっと混乱気味に次。

NG4

NG3の時に、returnの外で変数に入れてからにした方がいいのか?とやってみたもの。
上記のrender()の部分を下記にしてみると

  render() {
    const b = this.btn
    return {b}
  }

エラー内容

Error: Objects are not valid as a React child (found: object with keys {b}). If you meant to render a collection of children, use an array instead.

オブジェクトはReactの子として無効です(見つかった:キー{b}を持つオブジェクト)。子のコレクションをレンダリングする場合は、代わりに配列を使用してください。

{ }がオブジェクトと認識されてしまった…。

このあたりで、やっとbtn()で何もreturnされてないことに気づく。

試しに

btn(){
  return 'aaa'
}
render() {
  return this.btn()
}

としてみるとaaaが返ってきた。

ならば、tagという変数に一度格納して、最後にそれをリターンしてみることに。

NG5

import * as React from 'react'
import Link from 'next/link'
import { auth } from '../src/utils/firebase';

class Btn extends React.Component {
  btn() {
    let tag;
    auth.onAuthStateChanged((user) => {
      if (user) {
        tag = <span className="btn-s is-orange">ログアウト</span>
      } else {
        tag = <Link href="/login/"><a className="btn-s is-orange">ログイン</a></Link>
      }
    })
    return tag
  }
  render() {
    return this.btn()
  }
}

export default Btn

エラー内容

Error: Btn(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.

Btn(…):レンダリングから何も返されませんでした。これは通常、returnステートメントが欠落していることを意味します。または、何もレンダリングしない場合は、nullを返します。

またリターンできてないようなエラーが。今回はちゃんとできてるはずなのに、さっきのaaaと何が違う?

と考えたところ、onAuthStateChangedが非同期処理だということに気づく。

試しにtagに初期値を設定してみると

btn(){
  let tag = 'ee';
  auth.onAuthStateChanged((user) => {
    if (user) {
      tag = <span className="btn-s is-orange">ログアウト</span>
    } else {
      tag = <Link href="/login/"><a className="btn-s is-orange">ログイン</a></Link>
    }
  })
  return tag
}

eeが表示された!

NG6

ならばuseEffectで関数の実行タイミングをReactのレンダリング後まで遅らせることにしようと思い、useEffectを組み込んでみるが…

import * as React from 'react'
import Link from 'next/link'
import { useEffect } from 'react'
import { auth } from '../src/utils/firebase';

class Btn extends React.Component {
  btn() {
    useEffect(() => {
      let tag = 'ee';
      auth.onAuthStateChanged((user) => {
        if (user) {
          tag = <span className="btn-s is-orange">ログアウト</span>
        } else {
          tag = <Link href="/login/"><a className="btn-s is-orange">ログイン</a></Link>
        }
      })
      return tag
    })
  }
  render() {
    return this.btn()
  }
}

export default Btn

エラー内容

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.

エラー:無効なフック呼び出し。フックは、関数コンポーネントの本体の内部でのみ呼び出すことができます。これは、次のいずれかの理由で発生する可能性があります。 1. Reactとレンダラー(React DOMなど)のバージョンが一致していない可能性があります 2.フックのルールに違反している可能性があります 3.同じアプリにReactのコピーが複数ある可能性があります この問題をデバッグして修正する方法のヒントについては、https://reactjs.org/link/invalid-hook-callを参照してください。

さらにコンソールパネルにもエラー

Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app

キャッチされないエラー:無効なフック呼び出し。フックは、関数コンポーネントの本体の内部でのみ呼び出すことができます。これは、次のいずれかの理由で発生する可能性があります。
1.Reactとレンダラー(React DOMなど)のバージョンが一致していない可能性があります
2.フックのルールに違反している可能性があります
3.同じアプリにReactのコピーが複数ある可能性があります

useEffect()は、ここで使ってはいけないようです。
btn()の外に出さないといけないな…と思って、やってみる。

NG7

useEffectでbtn()を包んだ。明らかにおかしいけど、この時点でもう大分疲れてる。

import * as React from 'react'
import Link from 'next/link'
import { useEffect } from 'react'
import { auth } from '../src/utils/firebase';

class Btn extends React.Component {
  useEffect(() => {
    const btn = () => {
      let tag = 'ee';
      auth.onAuthStateChanged((user) => {
        if (user) {
          tag = <span className="btn-s is-orange">ログアウト</span>
        } else {
          tag = <Link href="/login/"><a className="btn-s is-orange">ログイン</a></Link>
        }
      })
      return tag
    }
  })
  render() {
    return this.btn()
  }
}

エラーの内容

シンタックスエラー。

Syntax error: Unexpected token

   5 | 
   6 | class Btn extends React.Component {
>  7 |   useEffect(() => {

とりあえずここまでを整理すると、

auth.onAuthStateChanged の結果待ちをしないと、コンポーネントのexportに結果が入ってこない。
useEffectを使おうと思ったが、この書き方では条件分岐したものをbtn()のreturnで返すことができない。

なので、useEffectを使うために、btn()を捨てることにする。

NG8(一応成功)

import * as React from 'react'
import { useEffect, FC, useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { auth } from '../src/utils/firebase';

const loginBtn: FC = (props: any) => {
  const router = useRouter();
  const [isLogin, setIsLogin] = useState< null | boolean > (null)
  useEffect(() => {
    auth.onAuthStateChanged((user) => {
      if (user) {
        setIsLogin(true)
      } else {
        setIsLogin(false)
      }
    })
  }, [])

  if (isLogin){
    return (
      <>
        <span className="btn-s is-orange">ログアウト</span>
      </>
    )
  }else{
    return (
      <>
        <Link href="/login/"><a className="btn-s is-orange">ログイン</a></Link>
      </>
    )
  }
}
export default loginBtn

ボタンの出し分けに成功!

  1. useStateを使い、ログインしてるならisLoginがtrue、してなければisLoginがfalseという状態にする
  2. returnをifで出し分ける

しかしここでもまだコンソールパネルにエラーがでました。

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

警告:マウントされていないコンポーネントでReact状態の更新を実行することはできません。これは何もしませんが、アプリケーションのメモリリークを示しています。修正するには、useEffectクリーンアップ関数のすべてのサブスクリプションと非同期タスクをキャンセルします。

調べていくと、useEffectを使用する時は、クリーンアップというのものが必要になることがあるらしいです。

React公式 Using the Effect Hook

For example, we might want to set up a subscription to some external data source. In that case, it is important to clean up so that we don’t introduce a memory leak! 

https://reactjs.org/docs/hooks-effect.html


「たとえば、外部データソースへのサブスクリプションを設定したい場合があります。その場合、メモリリークが発生しないようにクリーンアップすることが重要です。」

…ということですが、クリーンアップについていろいろ調べてみるも、いまいちどういうことかわからなかったので、もうちょっと経験値が必要だと思い、とりあえず解決法だけ試してみることに。

OKパターン(まだ惜しい)

ここを参考に、useEffectの部分をこうしました。

useEffect(() => {
  let isMounted = true;
  auth.onAuthStateChanged((user) => {
    if (isMounted){
      user ? setIsLogin(true) : setIsLogin(false);
    }
  })
  return () => { isMounted = false };
}, [])

useEffect内でlet isMounted = trueを宣言。これは、コンポーネントがアンマウントされるとすぐに、クリーンアップコールバックで変更されます。状態を更新する前に、この変数を条件付きでチェックします。

これが正解なのかはわからないけど、コンソールパネルのエラーはなくなりました。

そして、ちょっと気になっていたのがこの部分

  if (isLogin) {
    return (
      <>
        <span className="btn-s is-orange">ログアウト</span>
      </>
    )
  } else {
    return (
      <>
        <Link href="/login/"><a className="btn-s is-orange">ログイン</a></Link>
      </>
    )
  }

return2回はちょっと微妙な感じがしたので、書き直しました。

最終的に

logOutの間数も付け足して、最終コードはこうなりました。

import * as React from 'react'
import { useEffect, FC, useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { auth } from '../src/utils/firebase';


const loginBtn: FC = (props: any) => {
  const router = useRouter();
  const [isLogin, setIsLogin] = useState<null | boolean>(null)
  useEffect(() => {
    let isMounted = true;
    auth.onAuthStateChanged((user) => {
      if (isMounted) {
        user ? setIsLogin(true) : setIsLogin(false);
      }
    })
    return () => { isMounted = false };
  }, [])
  const logOut = async () => {
    try {
      await auth.signOut()
      router.push('/login')
    } catch (error) {
      alert(error.message)
    }
  }
  return (
    <>
      {isLogin
        ? <span className="btn-s is-orange" onClick={logOut}>ログアウト</span>
        : <Link href="/login/"><a className="btn-s is-orange">ログイン</a></Link>
      }
    </>
  )
}

export default loginBtn

TSどころかJSの基本的な書き方でミスしたり、間違いの積み重なり方がすごかったですが、結構細かくエラーを出してくれるところがありがたかったです。

最終コードがこれでいいのかはわからないですが、勉強しながら続きもやっていこうと思います。

おすすめの記事 recommend blog

新着 new blog

github