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でエラーが表示される。