1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.jsでuseGLTFのエラーハンドリングをしたい

Posted at

pmndrs/dreiのuseGLTFに、
存在しないファイルのURLを使用すると、
Next.jsのエラー画面が表示されてしまう。

Reactフックはtry/catchで囲んではいけない

ReactフックであるuseGLTFをtry/catchで囲むと、
エラーはキャッチできるものの、
Reactフックはtry/catchで囲んではいけないため、
Next.jsアプリをビルドしようとするとエラーが発生するためできない。

原因

useGLTFは、
react-three-fiberでuseLoaderに、
Three.jsのGLTFLoaderを渡している。
GLTFLoaderなどのThree.jsのLoaderはコールバック関数を使う。

useLoaderを調べてみると、
(ReactのSuspenceとは異なる)suspend-reactというライブラリを使って、
Promiseを介してLoaderとデータをやり取りしている。

Next.jsでPromiseを使うと、
rejectに代入されたエラーがNext.jsに伝わってしまい、
エラー画面が表示される。

対処1

Next.jsはビルドとdev環境で動作が異なり、
dev環境ではエラーログが画面に表示され、
ビルドではエラーが発生するとエラー画面になる。

dev環境のエラーログは閉じることができるが、
ビルド環境のエラー画面が表示されると何もできなくなる。

suspend-reactのREADMEには「エラー状態は親レベルで処理される」("error-states are handled at the parental level")と記されている。

そのため、suspend-reactに依存するuseGLTFなどのフックは次のような使い方となる:

import * as React from "react";
import { useGLTF } from "@react-three/drei";
import { ErrorBoundary } from "react-error-boundary";

export default function Model() {
  // 意図的に存在しないURLを指定する
  const gltf = useGLTF("https://raw.githubusercontent.com/Densyakun/assets/main/.gltf");

  return <>
    Resolved
  </>;
}

export default function App() {
  return (
    <ErrorBoundary fallback={<>Rejected</>}>
      <React.Suspense fallback={<>Loading...</>}>
        <Model />
      </React.Suspense>
    </ErrorBoundary>
  )
}

親レベルでSuspenseを使っており、
ロード中は別のコンポーネントを表示している。
r3fのチュートリアルでもSuspenseを使っており、
Suspenseがないとロード中はCanvasごと表示されなくなる。

react-error-boundaryを使うと、
子コンポーネントでエラーが発生したときに、
Suspenceのように代わりのコンポーネントに置き換え、
アプリ全体が使えなくなることを避けることができる。

この方法では、dev環境ではエラーログが表示されるが、
ビルド環境のエラー画面は表示されなくなる。

対処2

直接suspendをtry/catchで囲む場合、ビルドすることができる。

import * as React from "react";
import { suspend } from "suspend-react";
import { GLTF, GLTFLoader } from "three-stdlib";

export default function Model({ path }: { path: string }) {
  try {
    const gltf = suspend(async () => {
      return new Promise<GLTF>((resolve, reject) => {
        const loader = new GLTFLoader();

        loader.load(path,
          function (gltf) {
            resolve(gltf);
          },
          undefined,
          function (error) {
            reject(error);
          }
        );
      });
    }, [path]);

    return <>
      Resolved
    </>;
  } catch {
    return <>
      Rejected!
    </>;
  }
}

export default function App() {
  return (
    <React.Suspense fallback={<>Loading...</>}>
      <Model path="https://raw.githubusercontent.com/Densyakun/assets/main/.gltf" />
    </React.Suspense>
  )
}

対処1と異なり、エラーがNext.jsに伝わらないようにすることができる。
ただし、useLoaderを使わないため、キャッシュされない。
そのため、useLoaderもuseSWRのようにエラーも取得できると良い。

対処?3

Next.jsではPromiseがRejectedされたときに、エラーが理由となる場合、エラー画面を表示するため、

loader.loadのonErrorでrejectの引数にエラーではなく、文字列を入れることで、
Next.jsのエラー画面が表示されるのを防ぐことができる。

loader.load(path,
  function (gltf) {
    resolve(gltf);
  },
  undefined,
  function (error) {
    reject("test");
  }
);

また、loader.loadのonErrorがundefinedの場合は、console.errorでエラーが表示される。

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?