かもメモ

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

Next.js router.push でリダイレクトしようとしたら No router instance found. なエラーになった

プロトタイプをたくさん作る事になりついに Next.js に入信しました。

今回 user id ごとに表示を切り替える Dynamic Routing をしているページで user id が無効な時 404 にリダイレクトさせようとして次のようなコードを作成しました。
/pages/posts/[uid].tsx

import { useRouter } from 'next/router';

export default function Post() {
  const router = useRouter();
  const { uid } = router.query;
  const posts: null | PostData[] = getPostListByUserId(uid);

  if (!posts) {
    router.push('/404');
    return null;
  }

  return <DisplayUserPosts posts={posts} />;
}

cf. Routing: Dynamic Routes | Next.js

このコードを npm run dev で確認してみた所、 user id が正しい場合でも無効な場合でも次のようなエラーが表示されてしまいました。
No router instance found. you should only use "next/router" inside the client side of your app.

初回ロード時に query の uid が undefined になっている

export default function Post() {
  const router = useRouter();
  const { uid } = router.query;
  console.log({ router, uid })
  // => { router: {...}, uid: undefined }
  // => { router: {...}, uid: 'XXXX' }
}

console.log を追加して確認してみた所、コンポーネントがロードされた直後 router オブジェクトは存在していたけど、router.query で取得している uidundefined になっていました。
初回の出力が Next がサーバーサイドで実行しているものなのかどうか判断はついてないのですが、 useRouter は hooks で router オブジェクトを取ってくれば直ぐに使える訳ではないようです。初回 router.query の値が undefined になるのと同様のタイミングで router.push メソッドを呼ぼうとしても準備ができておらずエラーは発生したものと思われます。

router.isReady を使う

Add a `ready: boolean` to `Router` returned by `useRouter` · Issue #8259 · vercel/next.js · GitHub ここで同じような問題が議論されていて、isReady というプロパティが追加されたっぽい。

/pages/posts/[uid].tsx

import { useRouter } from 'next/router';

export default function Post() {
  const router = useRouter();
  const isReady = router.isReady;
  
  if (!isReady) {
    return <Loading />;
  }

  const { uid } = router.query;
  const posts: null | PostData[] = getPostListByUserId(uid);

  if (!posts) {
    router.push('/404');
    return null;
  }

  return <DisplayUserPosts posts={posts} />;
}

これでエラー無く router の準備がまだな時は <Loading /> が表示され、 user id が無効な時に /404 にリダイレクトされるようになりました!

router.isReady は useEffect の中で使わなければならない

これでヨシ!と思ったのですが、公式ドキュメントには下記のように書かれていました。

isReady: boolean - Whether the router fields are updated client-side and ready for use. Should only be used inside of useEffect methods and not for conditionally rendering on the server.
cf. next/router | Next.js

isReady はクライアントサイドで router が使用できる状態になっているかを表すが、 useEffect 内では使えるが、サーバーサイドの条件付きレンダリングでは使用できない。トノコト。
SSR(?) では利用できず、useEffect がクライアントサイドでだけ実行されるので useEffect 無いだけで使ってくれということっぽい。恐らくサーバーサイドでは window オブジェクトが無いから isReady が正しく動作しないってことなのかなと想像。 (Next.js まだ始めたばかりで深く理解できてないので想像で…)

SSR 利用してなければ上記な書き方でも問題なさそうですが、公式ドキュメントに useEffect 内で使うように書かれているのでコードを書き直します。

/pages/posts/[uid].tsx

import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';

export default function Post() {
  const [loading, setLoading] = useState<boolean>(false);
  const [posts, setPosts] = useState<null | PostData[]>(null);
  const { query, isReady } = useRouter();
  const { uid } = query;
  
  // ⚠ ここに if (!loading) { return <Loading /> } を書くと useEffect が永遠に実行されなくなる

  useEffect(() => {
    if (isReady) {
      setPosts(getPostListByUserId(uid));
      setLoading(true);
    }
  }, [isReady, uid]);

  
  if (!loading) {
    return <Loading />;
  }

  if (!posts) {
    router.push('/404');
    return null;
  }

  return <DisplayUserPosts posts={posts} />;
}

loadingposts と state が増えてしまいましたが、これなら useEffect 内で router.isReady の完了を正しく確認できるので良さそうです。

所管

Next.js 始めたばかりですが、色々とサクット実装できてるしそのまま vercel にデプロイもできるしスゴイ〜と感じています。
その反面、どれは非同期的なの?とか CSR, SSR, SSG とか色々あってどういう時にどの書き方だとどの処理が行われるの?とか理解しきれてない部分が多く。今回の useRouter のように画面で見てるのに何で you should only use "next/router" inside the client side で使ってというエラーが出てるの?とハマってしまうことも多いです。
もう少し使い込んで慣れていきたいと思いました。おわり


[参考]

NEXT FRONTIER… うまぴょい以外にも曲あったのですね!! (ネタ画像見つけるのが大変だった…)