どうも、uzimaru です。
最近、SPAを書いていて404をいい感じに出したい〜〜〜ってなったのでそれについてまとめる
はじめに
前提の共有から
- React
- SPA
- SSRとかはマジでやらないCSRのページ
- API で取得するリソースには権限があったりなかったりする
- 権限が無いとき・マジでリソースが無いときは NotFound の表示にする
という感じ。
また、fetch の段階で 404 等のエラーになっていたら Error を throw するようにします。
例えばエラーはこんな感じのやつがあるとする
tsx// 型の判別ができればいいので中身は空
class NotFoundError {}
解決方法
エラーを見て if で出し分け
おそらく一番簡単なやつ
if 文でエラーの種類を見て 404 だったら NotFound の表示をする
tsxconst CatchErrorComponent = () => {
const { data, error } = useSWR('/api/not-found', fetch)
if (!data) {
return "loading..."
}
if (error instanceof NotFoundError) {
return
}
return {JSON.stringify(data)}
}
一番シンプル、良さそう
ただ、複数のAPIを組み合わせて使うページの場合こうなる
tsxconst CatchErrorComponent = () => {
const { data: data1, error: error1 } = useSWR('/api/not-found', fetch)
const { data: data2, error: error2 } = useSWR('/api/not-found2', fetch)
// ︙
const { data: data100, error: error100 } = useSWR('/api/not-found100', fetch)
if (!data1 || !data2 || ... || !data100) {
return "loading..."
}
if (
error1 instanceof NotFoundError ||
error2 instanceof NotFoundError ||
...
error100 instanceof NotFoundError
) {
return
}
return {JSON.stringify(data)}
}
エラーの数だけ error instanceof NotFoundError
を書かないといけない
まぁ書くだけ何だけど…
NotFound を識別する関数を作る
instanceof NotFoundError
を書くのがダルいならそれをまとめてやってくれる関数を作る
tsxconst assertNotFound = (...errors: unknown[]): boolean => {
for (const error of errors) {
if (error instanceof NotFoundError) {
return true
}
}
return false
}
tsxconst CatchErrorComponent = () => {
const { data: data1, error: error1 } = useSWR('/api/not-found', fetch)
const { data: data2, error: error2 } = useSWR('/api/not-found2', fetch)
// ︙
const { data: data100, error: error100 } = useSWR('/api/not-found100', fetch)
if (!data1 || !data2 || ... || !data100) {
return "loading..."
}
if (assertNotFound(
error1,
error2,
...,
error100
)) {
return
}
return {JSON.stringify(data)}
}
記述量が減って嬉しい
若干微妙なのは if
で見てるところと <NotFound>
コンポーネントが各コンポーネントに出てくるところ
こんなケースになるとこれだとマズい
tsxconst ComponentA = () => {
// 何か色々処理
return
{/* div地獄 */}
}
これだと他の要素に関係なしでNotFoundが出ちゃうので良くない
ErrorBoundary を使う
ここから本編
ErrorBoundary を使って NotFound の表示を制御する
tsxexport class NotFoundErrorBoundary extends Component {
constructor(props: any) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error: unknown) {
if (error instanceof NotFoundError) {
return { hasError: true }
}
}
handleReload() {
this.setState({ hasError: false })
}
render() {
if (this.state.hasError) {
return (
<>
>
)
}
return this.props.children
}
}
const Reload = ({ onClick }: { onClick: () => void }) => {
const navigate = useNavigate()
const handleReload = useCallback(() => {
navigate('/')
onClick()
}, [navigate])
return
}
getDerivedStateFromError
で降ってきたエラーが NotFoundError
か確認している
注意するところは復帰導線を作った際に State の hasError
を false
にしないといけない部分
また、さっき作った assertNotFound
を修正する
tsxconst assertNotFound = (...errors: unknown[]): void | never => {
for (const error of errors) {
if (error instanceof NotFoundError) {
throw error
}
}
}
NotFoundError
だったらそのまま throw するようにする
これによりコンポーネントではこうなる
tsxconst CatchErrorComponent = () => {
const { data: data1, error: error1 } = useSWR('/api/not-found', fetch)
const { data: data2, error: error2 } = useSWR('/api/not-found2', fetch)
// ︙
const { data: data100, error: error100 } = useSWR('/api/not-found100', fetch)
if (!data1 || !data2 || ... || !data100) {
return "loading..."
}
assertNotFound(
error1,
error2,
...,
error100
)
return {JSON.stringify(data)}
}
だいぶスッキリした印象
この方法のいいところはエラーの種類に応じて拡張可能なところ
NotFoundErrorBoundary
の getDerivedStateFromError
を別のエラーについて検査するようにしたら別のエラーのときの表示をすることができる。
tsxexport class ForbiddenErrorBoundary extends Component {
...
static getDerivedStateFromError(error: unknown) {
if (error instanceof ForbiddenError) {
return { hasError: true }
}
}
...
}
const assertForbidden = (...errors: unknown[]): void | never => {
for (const error of errors) {
if (error instanceof ForbiddenError) {
throw error
}
}
}
const CatchErrorComponent = () => {
const { data: data1, error: error1 } = useSWR('/api/not-found', fetch)
const { data: data2, error: error2 } = useSWR('/api/not/permission', fetch)
if (!data1 || !data2) {
return "loading..."
}
assertForbidden(
error1,
error2
)
assertNotFound(
error1,
error2
)
return {JSON.stringify(data)}
}
まとめ
ErrorBoundary の利用が Suspense のときくらいだったが、今回の例でもいい感じに扱えた
assertNotFound
みたいな関数がコンポーネント中に突然出てくるのが若干気になるのでもうちょっといい方法を模索していきたい